worker-loader.sys.mjs (16120B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // A CommonJS module loader that is designed to run inside a worker debugger. 6 // We can't simply use the SDK module loader, because it relies heavily on 7 // Components, which isn't available in workers. 8 // 9 // In principle, the standard instance of the worker loader should provide the 10 // same built-in modules as its devtools counterpart, so that both loaders are 11 // interchangable on the main thread, making them easier to test. 12 // 13 // On the worker thread, some of these modules, in particular those that rely on 14 // the use of Components, and for which the worker debugger doesn't provide an 15 // alternative API, will be replaced by vacuous objects. Consequently, they can 16 // still be required, but any attempts to use them will lead to an exception. 17 // 18 // Note: to see dump output when running inside the worker thread, you might 19 // need to enable the browser.dom.window.dump.enabled pref. 20 21 // Some notes on module ids and URLs: 22 // 23 // An id is either a relative id or an absolute id. An id is relative if and 24 // only if it starts with a dot. An absolute id is a normalized id if and only 25 // if it contains no redundant components. 26 // 27 // Every normalized id is a URL. A URL is either an absolute URL or a relative 28 // URL. A URL is absolute if and only if it starts with a scheme name followed 29 // by a colon and 2 or 3 slashes. 30 31 /** 32 * Convert the given relative id to an absolute id. 33 * 34 * @param String id 35 * The relative id to be resolved. 36 * @param String baseId 37 * The absolute base id to resolve the relative id against. 38 * 39 * @return String 40 * An absolute id 41 */ 42 function resolveId(id, baseId) { 43 return baseId + "/../" + id; 44 } 45 46 /** 47 * Convert the given absolute id to a normalized id. 48 * 49 * @param String id 50 * The absolute id to be normalized. 51 * 52 * @return String 53 * A normalized id. 54 */ 55 function normalizeId(id) { 56 // An id consists of an optional root and a path. A root consists of either 57 // a scheme name followed by 2 or 3 slashes, or a single slash. Slashes in the 58 // root are not used as separators, so only normalize the path. 59 const [, root, path] = id.match(/^(\w+:\/\/\/?|\/)?(.*)/); 60 61 const stack = []; 62 path.split("/").forEach(function (component) { 63 switch (component) { 64 case "": 65 case ".": 66 break; 67 case "..": 68 if (stack.length === 0) { 69 if (root !== undefined) { 70 throw new Error("Can't normalize absolute id '" + id + "'!"); 71 } else { 72 stack.push(".."); 73 } 74 } else if (stack[stack.length - 1] == "..") { 75 stack.push(".."); 76 } else { 77 stack.pop(); 78 } 79 break; 80 default: 81 stack.push(component); 82 break; 83 } 84 }); 85 86 return (root ? root : "") + stack.join("/"); 87 } 88 89 /** 90 * Create a module object with the given normalized id. 91 * 92 * @param String 93 * The normalized id of the module to be created. 94 * 95 * @return Object 96 * A module with the given id. 97 */ 98 function createModule(id) { 99 return Object.create(null, { 100 // CommonJS specifies the id property to be non-configurable and 101 // non-writable. 102 id: { 103 configurable: false, 104 enumerable: true, 105 value: id, 106 writable: false, 107 }, 108 109 // CommonJS does not specify an exports property, so follow the NodeJS 110 // convention, which is to make it non-configurable and writable. 111 exports: { 112 configurable: false, 113 enumerable: true, 114 value: Object.create(null), 115 writable: true, 116 }, 117 }); 118 } 119 120 /** 121 * Create a CommonJS loader with the following options: 122 * - createSandbox: 123 * A function that will be used to create sandboxes. It should take the name 124 * and prototype of the sandbox to be created, and return the newly created 125 * sandbox as result. This option is required. 126 * - globals: 127 * A map of names to built-in globals that will be exposed to every module. 128 * Defaults to the empty map. 129 * - loadSubScript: 130 * A function that will be used to load scripts in sandboxes. It should take 131 * the URL from and the sandbox in which the script is to be loaded, and not 132 * return a result. This option is required. 133 * - modules: 134 * A map from normalized ids to built-in modules that will be added to the 135 * module cache. Defaults to the empty map. 136 * - paths: 137 * A map of paths to base URLs that will be used to resolve relative URLs to 138 * absolute URLS. Defaults to the empty map. 139 * - resolve: 140 * A function that will be used to resolve relative ids to absolute ids. It 141 * should take the relative id of a module to be required and the absolute 142 * id of the requiring module as arguments, and return the absolute id of 143 * the module to be required as result. Defaults to resolveId above. 144 */ 145 function WorkerDebuggerLoader(options) { 146 /** 147 * Convert the given relative URL to an absolute URL, using the map of paths 148 * given below. 149 * 150 * @param String url 151 * The relative URL to be resolved. 152 * 153 * @return String 154 * An absolute URL. 155 */ 156 function resolveURL(url) { 157 let found = false; 158 for (const [path, baseURL] of paths) { 159 if (url.startsWith(path)) { 160 found = true; 161 url = url.replace(path, baseURL); 162 break; 163 } 164 } 165 if (!found) { 166 throw new Error("Can't resolve relative URL '" + url + "'!"); 167 } 168 169 // If the url has no extension, use ".js" by default. 170 return url.endsWith(".js") ? url : url + ".js"; 171 } 172 173 /** 174 * Load the given module with the given url. 175 * 176 * @param Object module 177 * The module object to be loaded. 178 * @param String url 179 * The URL to load the module from. 180 */ 181 function loadModule(module, url) { 182 // CommonJS specifies 3 free variables: require, exports, and module. These 183 // must be exposed to every module, so define these as properties on the 184 // sandbox prototype. Additional built-in globals are exposed by making 185 // the map of built-in globals the prototype of the sandbox prototype. 186 const prototype = Object.create(globals); 187 prototype.Components = {}; 188 prototype.require = createRequire(module); 189 prototype.exports = module.exports; 190 prototype.module = module; 191 192 const sandbox = createSandbox(url, prototype); 193 try { 194 loadSubScript(url, sandbox); 195 } catch (error) { 196 if (/^Error opening input stream/.test(String(error))) { 197 throw new Error( 198 "Can't load module '" + module.id + "' with url '" + url + "'!" 199 ); 200 } 201 throw error; 202 } 203 204 // The value of exports may have been changed by the module script, so 205 // freeze it if and only if it is still an object. 206 if (typeof module.exports === "object" && module.exports !== null) { 207 Object.freeze(module.exports); 208 } 209 } 210 211 /** 212 * Create a require function for the given module. If no module is given, 213 * create a require function for the top-level module instead. 214 * 215 * @param Object requirer 216 * The module for which the require function is to be created. 217 * 218 * @return Function 219 * A require function for the given module. 220 */ 221 function createRequire(requirer) { 222 return function require(id) { 223 // Make sure an id was passed. 224 if (id === undefined) { 225 throw new Error("Can't require module without id!"); 226 } 227 228 // Built-in modules are cached by id rather than URL, so try to find the 229 // module to be required by id first. 230 let module = modules[id]; 231 if (module === undefined) { 232 // Failed to find the module to be required by id, so convert the id to 233 // a URL and try again. 234 235 // If the id is relative, convert it to an absolute id. 236 if (id.startsWith(".")) { 237 if (requirer === undefined) { 238 throw new Error( 239 "Can't require top-level module with relative id " + 240 "'" + 241 id + 242 "'!" 243 ); 244 } 245 id = resolve(id, requirer.id); 246 } 247 248 // Convert the absolute id to a normalized id. 249 id = normalizeId(id); 250 251 // Convert the normalized id to a URL. 252 let url = id; 253 254 // If the URL is relative, resolve it to an absolute URL. 255 if (url.match(/^\w+:\/\//) === null) { 256 url = resolveURL(id); 257 } 258 259 // Try to find the module to be required by URL. 260 module = modules[url]; 261 if (module === undefined) { 262 // Failed to find the module to be required in the cache, so create 263 // a new module, load it from the given URL, and add it to the cache. 264 265 // Add modules to the cache early so that any recursive calls to 266 // require for the same module will return the partially-loaded module 267 // from the cache instead of triggering a new load. 268 module = modules[url] = createModule(id); 269 270 try { 271 loadModule(module, url); 272 } catch (error) { 273 // If the module failed to load, remove it from the cache so that 274 // subsequent calls to require for the same module will trigger a 275 // new load, instead of returning a partially-loaded module from 276 // the cache. 277 delete modules[url]; 278 throw error; 279 } 280 281 Object.freeze(module); 282 } 283 } 284 285 return module.exports; 286 }; 287 } 288 289 const createSandbox = options.createSandbox; 290 const globals = options.globals || Object.create(null); 291 const loadSubScript = options.loadSubScript; 292 293 // Create the module cache, by converting each entry in the map from 294 // normalized ids to built-in modules to a module object, with the exports 295 // property of each module set to a frozen version of the original entry. 296 const modules = options.modules || {}; 297 for (const id in modules) { 298 const module = createModule(id); 299 module.exports = Object.freeze(modules[id]); 300 modules[id] = module; 301 } 302 303 // Convert the map of paths to base URLs into an array for use by resolveURL. 304 // The array is sorted from longest to shortest path to ensure that the 305 // longest path is always the first to be found. 306 let paths = options.paths || Object.create(null); 307 paths = Object.keys(paths) 308 .sort((a, b) => b.length - a.length) 309 .map(path => [path, paths[path]]); 310 311 const resolve = options.resolve || resolveId; 312 313 this.require = createRequire(); 314 } 315 316 var loader = { 317 // There is only one loader in the worker thread. 318 // This will be used by DevToolsServer to build server prefix and actor IDs. 319 id: 0, 320 321 lazyGetter(object, name, lambda) { 322 Object.defineProperty(object, name, { 323 get() { 324 delete object[name]; 325 object[name] = lambda.apply(object); 326 return object[name]; 327 }, 328 configurable: true, 329 enumerable: true, 330 }); 331 }, 332 lazyServiceGetter() { 333 throw new Error("Can't import XPCOM service from worker thread!"); 334 }, 335 lazyRequireGetter(obj, properties, module, destructure) { 336 if (Array.isArray(properties) && !destructure) { 337 throw new Error( 338 "Pass destructure=true to call lazyRequireGetter with an array of properties" 339 ); 340 } 341 342 if (!Array.isArray(properties)) { 343 properties = [properties]; 344 } 345 346 for (const property of properties) { 347 Object.defineProperty(obj, property, { 348 get: () => 349 destructure 350 ? worker.require(module)[property] 351 : worker.require(module || property), 352 }); 353 } 354 }, 355 }; 356 357 // The following APIs are defined differently depending on whether we are on the 358 // main thread or a worker thread. On the main thread, we use the Components 359 // object to implement them. On worker threads, we use the APIs provided by 360 // the worker debugger. 361 362 /* eslint-disable no-shadow */ 363 var { 364 Debugger, 365 URL, 366 createSandbox, 367 dump, 368 rpc, 369 loadSubScript, 370 setImmediate, 371 xpcInspector, 372 } = (function () { 373 // Main thread 374 if (typeof Components === "object") { 375 const principal = Components.Constructor( 376 "@mozilla.org/systemprincipal;1", 377 "nsIPrincipal" 378 )(); 379 380 // To ensure that the this passed to addDebuggerToGlobal is a global, the 381 // Debugger object needs to be defined in a sandbox. 382 const sandbox = Cu.Sandbox(principal, { 383 wantGlobalProperties: ["ChromeUtils"], 384 }); 385 Cu.evalInSandbox( 386 ` 387 const { addDebuggerToGlobal } = ChromeUtils.importESModule( 388 'resource://gre/modules/jsdebugger.sys.mjs' 389 ); 390 addDebuggerToGlobal(globalThis); 391 `, 392 sandbox 393 ); 394 const Debugger = sandbox.Debugger; 395 396 const createSandbox = function (name, prototype) { 397 return Cu.Sandbox(principal, { 398 invisibleToDebugger: true, 399 sandboxName: name, 400 sandboxPrototype: prototype, 401 wantComponents: false, 402 wantXrays: false, 403 }); 404 }; 405 406 const rpc = undefined; 407 408 // eslint-disable-next-line mozilla/use-services 409 const subScriptLoader = Cc[ 410 "@mozilla.org/moz/jssubscript-loader;1" 411 ].getService(Ci.mozIJSSubScriptLoader); 412 413 const loadSubScript = function (url, sandbox) { 414 subScriptLoader.loadSubScript(url, sandbox); 415 }; 416 417 const Timer = ChromeUtils.importESModule( 418 "resource://gre/modules/Timer.sys.mjs" 419 ); 420 421 const setImmediate = function (callback) { 422 Timer.setTimeout(callback, 0); 423 }; 424 425 const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService( 426 Ci.nsIJSInspector 427 ); 428 429 const { URL } = Cu.Sandbox(principal, { 430 wantGlobalProperties: ["URL"], 431 }); 432 433 return { 434 Debugger, 435 URL, 436 createSandbox, 437 dump: globalThis.dump, 438 rpc, 439 loadSubScript, 440 setImmediate, 441 xpcInspector, 442 }; 443 } 444 // Worker thread 445 const requestors = []; 446 447 const scope = globalThis; 448 449 const xpcInspector = { 450 get eventLoopNestLevel() { 451 return requestors.length; 452 }, 453 454 get lastNestRequestor() { 455 return requestors.length === 0 ? null : requestors[requestors.length - 1]; 456 }, 457 458 enterNestedEventLoop(requestor) { 459 requestors.push(requestor); 460 scope.enterEventLoop(); 461 return requestors.length; 462 }, 463 464 exitNestedEventLoop() { 465 requestors.pop(); 466 scope.leaveEventLoop(); 467 return requestors.length; 468 }, 469 }; 470 471 return { 472 Debugger: globalThis.Debugger, 473 URL: globalThis.URL, 474 createSandbox: globalThis.createSandbox, 475 dump: globalThis.dump, 476 rpc: globalThis.rpc, 477 loadSubScript: globalThis.loadSubScript, 478 setImmediate: globalThis.setImmediate, 479 xpcInspector, 480 }; 481 })(); 482 /* eslint-enable no-shadow */ 483 484 // Create the default instance of the worker loader, using the APIs we defined 485 // above. 486 487 export const worker = new WorkerDebuggerLoader({ 488 createSandbox, 489 globals: { 490 isWorker: true, 491 dump, 492 loader, 493 rpc, 494 URL, 495 setImmediate, 496 retrieveConsoleEvents: globalThis.retrieveConsoleEvents, 497 setConsoleEventHandler: globalThis.setConsoleEventHandler, 498 clearConsoleEvents: globalThis.clearConsoleEvents, 499 console, 500 btoa: globalThis.btoa, 501 atob: globalThis.atob, 502 Services: Object.create(null), 503 ChromeUtils, 504 DebuggerNotificationObserver, 505 506 // The following APIs rely on the use of Components, and the worker debugger 507 // does not provide alternative definitions for them. Consequently, they are 508 // stubbed out both on the main thread and worker threads. 509 Cc: undefined, 510 ChromeWorker: undefined, 511 Ci: undefined, 512 Cu: undefined, 513 Cr: undefined, 514 Components: undefined, 515 }, 516 loadSubScript, 517 modules: { 518 Debugger, 519 xpcInspector, 520 }, 521 paths: { 522 // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠ 523 devtools: "resource://devtools", 524 // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠ 525 "xpcshell-test": "resource://test", 526 // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠ 527 }, 528 });