server.js (15644B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 // We expect these to be defined in the global scope by runtest.py. 8 /* global __LOCATION__, _PROFILE_PATH, _SERVER_PORT, _SERVER_ADDR, _DISPLAY_RESULTS, 9 _TEST_PREFIX, _HTTPD_PATH */ 10 // Defined by xpcshell 11 /* global quit */ 12 13 /* eslint-disable mozilla/use-chromeutils-generateqi */ 14 15 // Set up a protocol substituion so that we can load the httpd.js file. 16 let protocolHandler = Services.io 17 .getProtocolHandler("resource") 18 .QueryInterface(Ci.nsIResProtocolHandler); 19 let httpdJSPath = PathUtils.toFileURI(_HTTPD_PATH); 20 21 protocolHandler.setSubstitution( 22 "httpd-server", 23 Services.io.newURI(httpdJSPath) 24 ); 25 const { HttpServer, dumpn, setDebuggingStatus } = ChromeUtils.importESModule( 26 "resource://httpd-server/httpd.sys.mjs" 27 ); 28 29 protocolHandler.setSubstitution( 30 "mochitest-server", 31 Services.io.newFileURI(__LOCATION__.parent) 32 ); 33 /* import-globals-from mochitestListingsUtils.js */ 34 Services.scriptloader.loadSubScript( 35 "resource://mochitest-server/mochitestListingsUtils.js", 36 this 37 ); 38 39 const CC = Components.Constructor; 40 41 const FileInputStream = CC( 42 "@mozilla.org/network/file-input-stream;1", 43 "nsIFileInputStream", 44 "init" 45 ); 46 const ConverterInputStream = CC( 47 "@mozilla.org/intl/converter-input-stream;1", 48 "nsIConverterInputStream", 49 "init" 50 ); 51 52 // Disable automatic network detection, so tests work correctly when 53 // not connected to a network. 54 // eslint-disable-next-line mozilla/use-services 55 var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); 56 ios.manageOfflineStatus = false; 57 ios.offline = false; 58 59 var server; // for use in the shutdown handler, if necessary 60 61 var _quitting = false; 62 63 /** Quit when all activity has completed. */ 64 function serverStopped() { 65 _quitting = true; 66 } 67 68 // 69 // SCRIPT CODE 70 // 71 runServer(); 72 73 // We can only have gotten here if the /server/shutdown path was requested. 74 if (_quitting) { 75 dumpn("HTTP server stopped, all pending requests complete"); 76 quit(0); 77 } 78 79 // Impossible as the stop callback should have been called, but to be safe... 80 dumpn("TEST-UNEXPECTED-FAIL | failure to correctly shut down HTTP server"); 81 quit(1); 82 83 var serverBasePath; 84 var displayResults = true; 85 86 var gServerAddress; 87 var SERVER_PORT; 88 89 // 90 // SERVER SETUP 91 // 92 function runServer() { 93 serverBasePath = __LOCATION__.parent; 94 server = createMochitestServer(serverBasePath); 95 96 // verify server address 97 // if a.b.c.d or 'localhost' 98 if (typeof _SERVER_ADDR != "undefined") { 99 if (_SERVER_ADDR == "localhost") { 100 gServerAddress = _SERVER_ADDR; 101 } else { 102 var quads = _SERVER_ADDR.split("."); 103 if (quads.length == 4) { 104 var invalid = false; 105 for (var i = 0; i < 4; i++) { 106 if (quads[i] < 0 || quads[i] > 255) { 107 invalid = true; 108 } 109 } 110 if (!invalid) { 111 gServerAddress = _SERVER_ADDR; 112 } else { 113 throw new Error( 114 "invalid _SERVER_ADDR, please specify a valid IP Address" 115 ); 116 } 117 } 118 } 119 } else { 120 throw new Error( 121 "please define _SERVER_ADDR (as an ip address) before running server.js" 122 ); 123 } 124 125 if (typeof _SERVER_PORT != "undefined") { 126 if (parseInt(_SERVER_PORT) > 0 && parseInt(_SERVER_PORT) < 65536) { 127 SERVER_PORT = _SERVER_PORT; 128 } 129 } else { 130 throw new Error( 131 "please define _SERVER_PORT (as a port number) before running server.js" 132 ); 133 } 134 135 // If DISPLAY_RESULTS is not specified, it defaults to true 136 if (typeof _DISPLAY_RESULTS != "undefined") { 137 displayResults = _DISPLAY_RESULTS; 138 } 139 140 server._start(SERVER_PORT, gServerAddress); 141 142 // touch a file in the profile directory to indicate we're alive 143 var foStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( 144 Ci.nsIFileOutputStream 145 ); 146 var serverAlive = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 147 148 if (typeof _PROFILE_PATH == "undefined") { 149 serverAlive.initWithFile(serverBasePath); 150 serverAlive.append("mochitesttestingprofile"); 151 } else { 152 serverAlive.initWithPath(_PROFILE_PATH); 153 } 154 155 // Create a file to inform the harness that the server is ready 156 if (serverAlive.exists()) { 157 serverAlive.append("server_alive.txt"); 158 foStream.init(serverAlive, 0x02 | 0x08 | 0x20, 436, 0); // write, create, truncate 159 var data = "It's alive!"; 160 foStream.write(data, data.length); 161 foStream.close(); 162 } else { 163 throw new Error( 164 "Failed to create server_alive.txt because " + 165 serverAlive.path + 166 " could not be found." 167 ); 168 } 169 170 makeTags(); 171 172 // 173 // The following is threading magic to spin an event loop -- this has to 174 // happen manually in xpcshell for the server to actually work. 175 // 176 var thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread; 177 while (!server.isStopped()) { 178 thread.processNextEvent(true); 179 } 180 181 // Server stopped by /server/shutdown handler -- go through pending events 182 // and return. 183 184 // get rid of any pending requests 185 while (thread.hasPendingEvents()) { 186 thread.processNextEvent(true); 187 } 188 } 189 190 /** Creates and returns an HTTP server configured to serve Mochitests. */ 191 function createMochitestServer(serverBasePath) { 192 var server = new HttpServer(); 193 194 server.registerDirectory("/", serverBasePath); 195 server.registerPathHandler("/server/shutdown", serverShutdown); 196 server.registerPathHandler("/server/debug", serverDebug); 197 server.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality 198 server.registerContentType("jar", "application/x-jar"); 199 server.registerContentType("ogg", "application/ogg"); 200 server.registerContentType("pdf", "application/pdf"); 201 server.registerContentType("ogv", "video/ogg"); 202 server.registerContentType("oga", "audio/ogg"); 203 server.registerContentType("opus", "audio/ogg; codecs=opus"); 204 server.registerContentType("dat", "text/plain; charset=utf-8"); 205 server.registerContentType("frag", "text/plain"); // .frag == WebGL fragment shader 206 server.registerContentType("vert", "text/plain"); // .vert == WebGL vertex shader 207 server.registerContentType("wasm", "application/wasm"); 208 server.setIndexHandler(defaultDirHandler); 209 210 var serverRoot = { 211 getFile: function getFile(path) { 212 var file = serverBasePath.clone().QueryInterface(Ci.nsIFile); 213 path.split("/").forEach(function (p) { 214 file.appendRelativePath(p); 215 }); 216 return file; 217 }, 218 QueryInterface() { 219 return this; 220 }, 221 }; 222 223 server.setObjectState("SERVER_ROOT", serverRoot); 224 225 processLocations(server); 226 227 return server; 228 } 229 230 /** 231 * Notifies the HTTP server about all the locations at which it might receive 232 * requests, so that it can properly respond to requests on any of the hosts it 233 * serves. 234 */ 235 function processLocations(server) { 236 var serverLocations = serverBasePath.clone(); 237 serverLocations.append("server-locations.txt"); 238 239 const PR_RDONLY = 0x01; 240 var fis = new FileInputStream( 241 serverLocations, 242 PR_RDONLY, 243 292 /* 0444 */, 244 Ci.nsIFileInputStream.CLOSE_ON_EOF 245 ); 246 247 var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); 248 lis.QueryInterface(Ci.nsIUnicharLineInputStream); 249 250 const LINE_REGEXP = new RegExp( 251 "^([a-z][-a-z0-9+.]*)" + 252 "://" + 253 "(" + 254 "\\d+\\.\\d+\\.\\d+\\.\\d+" + 255 "|" + 256 "(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)*" + 257 "[a-z](?:[-a-z0-9]*[a-z0-9])?" + 258 ")" + 259 ":" + 260 "(\\d+)" + 261 "(?:" + 262 "\\s+" + 263 "(\\S+(?:,\\S+)*)" + 264 ")?$" 265 ); 266 267 var line = {}; 268 var lineno = 0; 269 var seenPrimary = false; 270 do { 271 var more = lis.readLine(line); 272 lineno++; 273 274 var lineValue = line.value; 275 if (lineValue.charAt(0) == "#" || lineValue == "") { 276 continue; 277 } 278 279 var match = LINE_REGEXP.exec(lineValue); 280 if (!match) { 281 throw new Error("Syntax error in server-locations.txt, line " + lineno); 282 } 283 284 var [, scheme, host, port, options] = match; 285 if (options) { 286 if (options.split(",").includes("primary")) { 287 if (seenPrimary) { 288 throw new Error( 289 "Multiple primary locations in server-locations.txt, " + 290 "line " + 291 lineno 292 ); 293 } 294 295 server.identity.setPrimary(scheme, host, port); 296 seenPrimary = true; 297 continue; 298 } 299 } 300 301 server.identity.add(scheme, host, port); 302 } while (more); 303 } 304 305 // PATH HANDLERS 306 307 // /server/shutdown 308 function serverShutdown(metadata, response) { 309 response.setStatusLine("1.1", 200, "OK"); 310 response.setHeader("Content-type", "text/plain", false); 311 312 var body = "Server shut down."; 313 response.bodyOutputStream.write(body, body.length); 314 315 dumpn("Server shutting down now..."); 316 server.stop(serverStopped); 317 } 318 319 // /server/debug?[012] 320 function serverDebug(metadata, response) { 321 response.setStatusLine(metadata.httpVersion, 400, "Bad debugging level"); 322 if (metadata.queryString.length !== 1) { 323 return; 324 } 325 326 var mode; 327 if (metadata.queryString === "0") { 328 // do this now so it gets logged with the old mode 329 dumpn("Server debug logs disabled."); 330 setDebuggingStatus(false, false); 331 mode = "disabled"; 332 } else if (metadata.queryString === "1") { 333 setDebuggingStatus(true, false); 334 mode = "enabled"; 335 } else if (metadata.queryString === "2") { 336 setDebuggingStatus(true, true); 337 mode = "enabled, with timestamps"; 338 } else { 339 return; 340 } 341 342 response.setStatusLine(metadata.httpVersion, 200, "OK"); 343 response.setHeader("Content-type", "text/plain", false); 344 var body = "Server debug logs " + mode + "."; 345 response.bodyOutputStream.write(body, body.length); 346 dumpn(body); 347 } 348 349 /** 350 * Produce a normal directory listing. 351 */ 352 function regularListing(metadata, response) { 353 var [links] = list(metadata.path, metadata.getProperty("directory"), false); 354 response.write( 355 "<!DOCTYPE html>\n" + 356 HTML( 357 HEAD(TITLE("mochitest index ", metadata.path)), 358 BODY(BR(), A({ href: ".." }, "Up a level"), UL(linksToListItems(links))) 359 ) 360 ); 361 } 362 363 /** 364 * Read a manifestFile located at the root of the server's directory and turn 365 * it into an object for creating a table of clickable links for each test. 366 */ 367 function convertManifestToTestLinks(root, manifest) { 368 const { NetUtil } = ChromeUtils.importESModule( 369 "resource://gre/modules/NetUtil.sys.mjs" 370 ); 371 372 var manifestFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 373 manifestFile.initWithFile(serverBasePath); 374 manifestFile.append(manifest); 375 376 var manifestStream = Cc[ 377 "@mozilla.org/network/file-input-stream;1" 378 ].createInstance(Ci.nsIFileInputStream); 379 manifestStream.init(manifestFile, -1, 0, 0); 380 381 var manifestObj = JSON.parse( 382 NetUtil.readInputStreamToString(manifestStream, manifestStream.available()) 383 ); 384 var paths = manifestObj.tests; 385 var pathPrefix = "/" + root + "/"; 386 return [ 387 paths.reduce(function (t, p) { 388 t[pathPrefix + p.path] = true; 389 return t; 390 }, {}), 391 paths.length, 392 ]; 393 } 394 395 /** 396 * Produce a test harness page containing all the test cases 397 * below it, recursively. 398 */ 399 function testListing(metadata, response) { 400 var links = {}; 401 var count = 0; 402 if (!metadata.queryString.includes("manifestFile")) { 403 [links, count] = list( 404 metadata.path, 405 metadata.getProperty("directory"), 406 true 407 ); 408 } else if (typeof Components != "undefined") { 409 var manifest = metadata.queryString.match(/manifestFile=([^&]+)/)[1]; 410 411 [links, count] = convertManifestToTestLinks( 412 metadata.path.split("/")[1], 413 manifest 414 ); 415 } 416 417 var table_class = 418 metadata.queryString.indexOf("hideResultsTable=1") > -1 ? "invisible" : ""; 419 420 let testname = 421 metadata.queryString.indexOf("testname=") > -1 422 ? metadata.queryString.match(/testname=([^&]+)/)[1] 423 : ""; 424 425 dumpn("count: " + count); 426 var tests = testname ? "['/" + testname + "']" : jsonArrayOfTestFiles(links); 427 response.write( 428 HTML( 429 HEAD( 430 TITLE("MochiTest | ", metadata.path), 431 LINK({ 432 rel: "stylesheet", 433 type: "text/css", 434 href: "/static/harness.css", 435 }), 436 SCRIPT({ 437 type: "text/javascript", 438 src: "/tests/SimpleTest/LogController.js", 439 }), 440 SCRIPT({ 441 type: "text/javascript", 442 src: "/tests/SimpleTest/MemoryStats.js", 443 }), 444 SCRIPT({ 445 type: "text/javascript", 446 src: "/tests/SimpleTest/TestRunner.js", 447 }), 448 SCRIPT({ 449 type: "text/javascript", 450 src: "/tests/SimpleTest/MozillaLogger.js", 451 }), 452 SCRIPT({ type: "text/javascript", src: "/chunkifyTests.js" }), 453 SCRIPT({ type: "text/javascript", src: "/manifestLibrary.js" }), 454 SCRIPT({ type: "text/javascript", src: "/tests/SimpleTest/setup.js" }), 455 SCRIPT( 456 { type: "text/javascript" }, 457 "window.onload = hookup; gTestList=" + tests + ";" 458 ) 459 ), 460 BODY( 461 DIV( 462 { class: "container" }, 463 H2("--> ", A({ href: "#", id: "runtests" }, "Run Tests"), " <--"), 464 P( 465 { style: "float: right;" }, 466 SMALL( 467 "Based on the ", 468 A({ href: "http://www.mochikit.com/" }, "MochiKit"), 469 " unit tests." 470 ) 471 ), 472 DIV( 473 { class: "status" }, 474 H1({ id: "indicator" }, "Status"), 475 H2({ id: "pass" }, "Passed: ", SPAN({ id: "pass-count" }, "0")), 476 H2({ id: "fail" }, "Failed: ", SPAN({ id: "fail-count" }, "0")), 477 H2({ id: "fail" }, "Todo: ", SPAN({ id: "todo-count" }, "0")) 478 ), 479 DIV({ class: "clear" }), 480 DIV( 481 { id: "current-test" }, 482 B("Currently Executing: ", SPAN({ id: "current-test-path" }, "_")) 483 ), 484 DIV({ class: "clear" }), 485 DIV( 486 { class: "frameholder" }, 487 IFRAME({ 488 scrolling: "no", 489 id: "testframe", 490 allow: "geolocation 'src'", 491 allowfullscreen: true, 492 }) 493 ), 494 DIV({ class: "clear" }), 495 DIV( 496 { class: "toggle" }, 497 A({ href: "#", id: "toggleNonTests" }, "Show Non-Tests"), 498 BR() 499 ), 500 501 displayResults 502 ? TABLE( 503 { 504 cellpadding: 0, 505 cellspacing: 0, 506 class: table_class, 507 id: "test-table", 508 }, 509 TR(TD("Passed"), TD("Failed"), TD("Todo"), TD("Test Files")), 510 linksToTableRows(links, 0) 511 ) 512 : "", 513 BR(), 514 TABLE({ 515 cellpadding: 0, 516 cellspacing: 0, 517 border: 1, 518 bordercolor: "red", 519 id: "fail-table", 520 }), 521 522 DIV({ class: "clear" }) 523 ) 524 ) 525 ) 526 ); 527 } 528 529 /** 530 * Respond to requests that match a file system directory. 531 * Under the tests/ directory, return a test harness page. 532 */ 533 function defaultDirHandler(metadata, response) { 534 response.setStatusLine("1.1", 200, "OK"); 535 response.setHeader("Content-type", "text/html;charset=utf-8", false); 536 try { 537 if (metadata.path.indexOf("/tests") != 0) { 538 regularListing(metadata, response); 539 } else { 540 testListing(metadata, response); 541 } 542 } catch (ex) { 543 response.write(ex); 544 } 545 }