diff options
Diffstat (limited to 'devtools/shared/worker/loader.js')
-rw-r--r-- | devtools/shared/worker/loader.js | 517 |
1 files changed, 517 insertions, 0 deletions
diff --git a/devtools/shared/worker/loader.js b/devtools/shared/worker/loader.js new file mode 100644 index 000000000..1de72963d --- /dev/null +++ b/devtools/shared/worker/loader.js @@ -0,0 +1,517 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// A CommonJS module loader that is designed to run inside a worker debugger. +// We can't simply use the SDK module loader, because it relies heavily on +// Components, which isn't available in workers. +// +// In principle, the standard instance of the worker loader should provide the +// same built-in modules as its devtools counterpart, so that both loaders are +// interchangable on the main thread, making them easier to test. +// +// On the worker thread, some of these modules, in particular those that rely on +// the use of Components, and for which the worker debugger doesn't provide an +// alternative API, will be replaced by vacuous objects. Consequently, they can +// still be required, but any attempts to use them will lead to an exception. + +this.EXPORTED_SYMBOLS = ["WorkerDebuggerLoader", "worker"]; + +// Some notes on module ids and URLs: +// +// An id is either a relative id or an absolute id. An id is relative if and +// only if it starts with a dot. An absolute id is a normalized id if and only +// if it contains no redundant components. +// +// Every normalized id is a URL. A URL is either an absolute URL or a relative +// URL. A URL is absolute if and only if it starts with a scheme name followed +// by a colon and 2 or 3 slashes. + +/** + * Convert the given relative id to an absolute id. + * + * @param String id + * The relative id to be resolved. + * @param String baseId + * The absolute base id to resolve the relative id against. + * + * @return String + * An absolute id + */ +function resolveId(id, baseId) { + return baseId + "/../" + id; +} + +/** + * Convert the given absolute id to a normalized id. + * + * @param String id + * The absolute id to be normalized. + * + * @return String + * A normalized id. + */ +function normalizeId(id) { + // An id consists of an optional root and a path. A root consists of either + // a scheme name followed by 2 or 3 slashes, or a single slash. Slashes in the + // root are not used as separators, so only normalize the path. + let [_, root, path] = id.match(/^(\w+:\/\/\/?|\/)?(.*)/); + + let stack = []; + path.split("/").forEach(function (component) { + switch (component) { + case "": + case ".": + break; + case "..": + if (stack.length === 0) { + if (root !== undefined) { + throw new Error("Can't normalize absolute id '" + id + "'!"); + } else { + stack.push(".."); + } + } else { + if (stack[stack.length - 1] == "..") { + stack.push(".."); + } else { + stack.pop(); + } + } + break; + default: + stack.push(component); + break; + } + }); + + return (root ? root : "") + stack.join("/"); +} + +/** + * Create a module object with the given normalized id. + * + * @param String + * The normalized id of the module to be created. + * + * @return Object + * A module with the given id. + */ +function createModule(id) { + return Object.create(null, { + // CommonJS specifies the id property to be non-configurable and + // non-writable. + id: { + configurable: false, + enumerable: true, + value: id, + writable: false + }, + + // CommonJS does not specify an exports property, so follow the NodeJS + // convention, which is to make it non-configurable and writable. + exports: { + configurable: false, + enumerable: true, + value: Object.create(null), + writable: true + } + }); +} + +/** + * Create a CommonJS loader with the following options: + * - createSandbox: + * A function that will be used to create sandboxes. It should take the name + * and prototype of the sandbox to be created, and return the newly created + * sandbox as result. This option is required. + * - globals: + * A map of names to built-in globals that will be exposed to every module. + * Defaults to the empty map. + * - loadSubScript: + * A function that will be used to load scripts in sandboxes. It should take + * the URL from and the sandbox in which the script is to be loaded, and not + * return a result. This option is required. + * - modules: + * A map from normalized ids to built-in modules that will be added to the + * module cache. Defaults to the empty map. + * - paths: + * A map of paths to base URLs that will be used to resolve relative URLs to + * absolute URLS. Defaults to the empty map. + * - resolve: + * A function that will be used to resolve relative ids to absolute ids. It + * should take the relative id of a module to be required and the absolute + * id of the requiring module as arguments, and return the absolute id of + * the module to be required as result. Defaults to resolveId above. + */ +function WorkerDebuggerLoader(options) { + /** + * Convert the given relative URL to an absolute URL, using the map of paths + * given below. + * + * @param String url + * The relative URL to be resolved. + * + * @return String + * An absolute URL. + */ + function resolveURL(url) { + let found = false; + for (let [path, baseURL] of paths) { + if (url.startsWith(path)) { + found = true; + url = url.replace(path, baseURL); + break; + } + } + if (!found) { + throw new Error("Can't resolve relative URL '" + url + "'!"); + } + + // If the url has no extension, use ".js" by default. + return url.endsWith(".js") ? url : url + ".js"; + } + + /** + * Load the given module with the given url. + * + * @param Object module + * The module object to be loaded. + * @param String url + * The URL to load the module from. + */ + function loadModule(module, url) { + // CommonJS specifies 3 free variables: require, exports, and module. These + // must be exposed to every module, so define these as properties on the + // sandbox prototype. Additional built-in globals are exposed by making + // the map of built-in globals the prototype of the sandbox prototype. + let prototype = Object.create(globals); + prototype.Components = {}; + prototype.require = createRequire(module); + prototype.exports = module.exports; + prototype.module = module; + + let sandbox = createSandbox(url, prototype); + try { + loadSubScript(url, sandbox); + } catch (error) { + if (/^Error opening input stream/.test(String(error))) { + throw new Error("Can't load module '" + module.id + "' with url '" + + url + "'!"); + } + throw error; + } + + // The value of exports may have been changed by the module script, so + // freeze it if and only if it is still an object. + if (typeof module.exports === "object" && module.exports !== null) { + Object.freeze(module.exports); + } + } + + /** + * Create a require function for the given module. If no module is given, + * create a require function for the top-level module instead. + * + * @param Object requirer + * The module for which the require function is to be created. + * + * @return Function + * A require function for the given module. + */ + function createRequire(requirer) { + return function require(id) { + // Make sure an id was passed. + if (id === undefined) { + throw new Error("Can't require module without id!"); + } + + // Built-in modules are cached by id rather than URL, so try to find the + // module to be required by id first. + let module = modules[id]; + if (module === undefined) { + // Failed to find the module to be required by id, so convert the id to + // a URL and try again. + + // If the id is relative, convert it to an absolute id. + if (id.startsWith(".")) { + if (requirer === undefined) { + throw new Error("Can't require top-level module with relative id " + + "'" + id + "'!"); + } + id = resolve(id, requirer.id); + } + + // Convert the absolute id to a normalized id. + id = normalizeId(id); + + // Convert the normalized id to a URL. + let url = id; + + // If the URL is relative, resolve it to an absolute URL. + if (url.match(/^\w+:\/\//) === null) { + url = resolveURL(id); + } + + // Try to find the module to be required by URL. + module = modules[url]; + if (module === undefined) { + // Failed to find the module to be required in the cache, so create + // a new module, load it from the given URL, and add it to the cache. + + // Add modules to the cache early so that any recursive calls to + // require for the same module will return the partially-loaded module + // from the cache instead of triggering a new load. + module = modules[url] = createModule(id); + + try { + loadModule(module, url); + } catch (error) { + // If the module failed to load, remove it from the cache so that + // subsequent calls to require for the same module will trigger a + // new load, instead of returning a partially-loaded module from + // the cache. + delete modules[url]; + throw error; + } + + Object.freeze(module); + } + } + + return module.exports; + }; + } + + let createSandbox = options.createSandbox; + let globals = options.globals || Object.create(null); + let loadSubScript = options.loadSubScript; + + // Create the module cache, by converting each entry in the map from + // normalized ids to built-in modules to a module object, with the exports + // property of each module set to a frozen version of the original entry. + let modules = options.modules || {}; + for (let id in modules) { + let module = createModule(id); + module.exports = Object.freeze(modules[id]); + modules[id] = module; + } + + // Convert the map of paths to base URLs into an array for use by resolveURL. + // The array is sorted from longest to shortest path to ensure that the + // longest path is always the first to be found. + let paths = options.paths || Object.create(null); + paths = Object.keys(paths) + .sort((a, b) => b.length - a.length) + .map(path => [path, paths[path]]); + + let resolve = options.resolve || resolveId; + + this.require = createRequire(); +} + +this.WorkerDebuggerLoader = WorkerDebuggerLoader; + +// The following APIs rely on the use of Components, and the worker debugger +// does not provide alternative definitions for them. Consequently, they are +// stubbed out both on the main thread and worker threads. + +var chrome = { + CC: undefined, + Cc: undefined, + ChromeWorker: undefined, + Cm: undefined, + Ci: undefined, + Cu: undefined, + Cr: undefined, + components: undefined +}; + +var loader = { + lazyGetter: function (object, name, lambda) { + Object.defineProperty(object, name, { + get: function () { + delete object[name]; + return object[name] = lambda.apply(object); + }, + configurable: true, + enumerable: true + }); + }, + lazyImporter: function () { + throw new Error("Can't import JSM from worker thread!"); + }, + lazyServiceGetter: function () { + throw new Error("Can't import XPCOM service from worker thread!"); + }, + lazyRequireGetter: function (obj, property, module, destructure) { + Object.defineProperty(obj, property, { + get: () => destructure ? worker.require(module)[property] + : worker.require(module || property) + }); + } +}; + +// The following APIs are defined differently depending on whether we are on the +// main thread or a worker thread. On the main thread, we use the Components +// object to implement them. On worker threads, we use the APIs provided by +// the worker debugger. + +var { + Debugger, + URL, + createSandbox, + dump, + rpc, + loadSubScript, + reportError, + setImmediate, + xpcInspector +} = (function () { + if (typeof Components === "object") { // Main thread + let { + Constructor: CC, + classes: Cc, + manager: Cm, + interfaces: Ci, + results: Cr, + utils: Cu + } = Components; + + let principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")(); + + // To ensure that the this passed to addDebuggerToGlobal is a global, the + // Debugger object needs to be defined in a sandbox. + let sandbox = Cu.Sandbox(principal, {}); + Cu.evalInSandbox( + "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" + + "addDebuggerToGlobal(this);", + sandbox + ); + let Debugger = sandbox.Debugger; + + let createSandbox = function (name, prototype) { + return Cu.Sandbox(principal, { + invisibleToDebugger: true, + sandboxName: name, + sandboxPrototype: prototype, + wantComponents: false, + wantXrays: false + }); + }; + + let rpc = undefined; + + let subScriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + + let loadSubScript = function (url, sandbox) { + subScriptLoader.loadSubScript(url, sandbox, "UTF-8"); + }; + + let reportError = Cu.reportError; + + let Timer = Cu.import("resource://gre/modules/Timer.jsm", {}); + + let setImmediate = function (callback) { + Timer.setTimeout(callback, 0); + }; + + let xpcInspector = Cc["@mozilla.org/jsinspector;1"]. + getService(Ci.nsIJSInspector); + + return { + Debugger, + URL: this.URL, + createSandbox, + dump: this.dump, + rpc, + loadSubScript, + reportError, + setImmediate, + xpcInspector + }; + } else { // Worker thread + let requestors = []; + + let scope = this; + + let xpcInspector = { + get eventLoopNestLevel() { + return requestors.length; + }, + + get lastNestRequestor() { + return requestors.length === 0 ? null : requestors[requestors.length - 1]; + }, + + enterNestedEventLoop: function (requestor) { + requestors.push(requestor); + scope.enterEventLoop(); + return requestors.length; + }, + + exitNestedEventLoop: function () { + requestors.pop(); + scope.leaveEventLoop(); + return requestors.length; + } + }; + + return { + Debugger: this.Debugger, + URL: this.URL, + createSandbox: this.createSandbox, + dump: this.dump, + rpc: this.rpc, + loadSubScript: this.loadSubScript, + reportError: this.reportError, + setImmediate: this.setImmediate, + xpcInspector: xpcInspector + }; + } +}).call(this); + +// Create the default instance of the worker loader, using the APIs we defined +// above. + +this.worker = new WorkerDebuggerLoader({ + createSandbox: createSandbox, + globals: { + "isWorker": true, + "dump": dump, + "loader": loader, + "reportError": reportError, + "rpc": rpc, + "setImmediate": setImmediate, + "URL": URL, + }, + loadSubScript: loadSubScript, + modules: { + "Debugger": Debugger, + "Services": Object.create(null), + "chrome": chrome, + "xpcInspector": xpcInspector + }, + paths: { + // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠ + "": "resource://gre/modules/commonjs/", + // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠ + // Modules here are intended to have one implementation for + // chrome, and a separate implementation for content. Here we + // map the directory to the chrome subdirectory, but the content + // loader will map to the content subdirectory. See the + // README.md in devtools/shared/platform. + "devtools/shared/platform": "resource://devtools/shared/platform/chrome", + // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠ + "devtools": "resource://devtools", + // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠ + "promise": "resource://gre/modules/Promise-backend.js", + // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠ + "source-map": "resource://devtools/shared/sourcemap/source-map.js", + // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠ + "xpcshell-test": "resource://test" + // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠ + } +}); |