mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-02 07:05:24 +00:00
473 lines
17 KiB
JavaScript
473 lines
17 KiB
JavaScript
/* 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/. */
|
|
/* globals NetUtil, FileUtils, OS */
|
|
|
|
"use strict";
|
|
|
|
/**
|
|
* Manages the addon-sdk loader instance used to load the developer tools.
|
|
*/
|
|
|
|
var { Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
|
|
|
|
var { Loader } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
|
|
var promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
|
|
|
|
this.EXPORTED_SYMBOLS = ["DevToolsLoader", "devtools", "BuiltinProvider",
|
|
"SrcdirProvider", "require", "loader"];
|
|
|
|
/**
|
|
* Providers are different strategies for loading the devtools.
|
|
*/
|
|
|
|
var loaderModules = {
|
|
"Services": Object.create(Services),
|
|
"toolkit/loader": Loader,
|
|
PromiseDebugging,
|
|
ThreadSafeChromeUtils,
|
|
HeapSnapshot,
|
|
};
|
|
XPCOMUtils.defineLazyGetter(loaderModules, "Debugger", () => {
|
|
// addDebuggerToGlobal only allows adding the Debugger object to a global. The
|
|
// this object is not guaranteed to be a global (in particular on B2G, due to
|
|
// compartment sharing), so add the Debugger object to a sandbox instead.
|
|
let sandbox = Cu.Sandbox(CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')());
|
|
Cu.evalInSandbox(
|
|
"Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
|
|
"addDebuggerToGlobal(this);",
|
|
sandbox
|
|
);
|
|
return sandbox.Debugger;
|
|
});
|
|
XPCOMUtils.defineLazyGetter(loaderModules, "Timer", () => {
|
|
let {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
|
|
// Do not return Cu.import result, as SDK loader would freeze Timer.jsm globals...
|
|
return {
|
|
setTimeout,
|
|
clearTimeout
|
|
};
|
|
});
|
|
XPCOMUtils.defineLazyGetter(loaderModules, "xpcInspector", () => {
|
|
return Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
|
|
});
|
|
XPCOMUtils.defineLazyGetter(loaderModules, "indexedDB", () => {
|
|
// On xpcshell, we can't instantiate indexedDB without crashing
|
|
try {
|
|
return Cu.Sandbox(this, {wantGlobalProperties:["indexedDB"]}).indexedDB;
|
|
} catch(e) {
|
|
return {};
|
|
}
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(loaderModules, "CSS", () => {
|
|
return Cu.Sandbox(this, {wantGlobalProperties: ["CSS"]}).CSS;
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(loaderModules, "URL", () => {
|
|
return Cu.Sandbox(this, {wantGlobalProperties: ["URL"]}).URL;
|
|
});
|
|
|
|
var sharedGlobalBlocklist = ["sdk/indexed-db"];
|
|
|
|
/**
|
|
* Used when the tools should be loaded from the Firefox package itself.
|
|
* This is the default case.
|
|
*/
|
|
function BuiltinProvider() {}
|
|
BuiltinProvider.prototype = {
|
|
load: function() {
|
|
this.loader = new Loader.Loader({
|
|
id: "fx-devtools",
|
|
modules: loaderModules,
|
|
paths: {
|
|
// When you add a line to this mapping, don't forget to make a
|
|
// corresponding addition to the SrcdirProvider mapping below as well.
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"": "resource://gre/modules/commonjs/",
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"devtools": "resource://devtools",
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"gcli": "resource://devtools/shared/gcli/source/lib/gcli",
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"promise": "resource://gre/modules/Promise-backend.js",
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"acorn": "resource://devtools/acorn",
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"acorn/util/walk": "resource://devtools/acorn/walk.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 ⚠
|
|
// Allow access to xpcshell test items from the loader.
|
|
"xpcshell-test": "resource://test"
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
},
|
|
globals: this.globals,
|
|
invisibleToDebugger: this.invisibleToDebugger,
|
|
sharedGlobal: true,
|
|
sharedGlobalBlocklist,
|
|
});
|
|
|
|
return promise.resolve(undefined);
|
|
},
|
|
|
|
unload: function(reason) {
|
|
Loader.unload(this.loader, reason);
|
|
delete this.loader;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Used when the tools should be loaded from a mozilla-central checkout. In
|
|
* addition to different paths, it needs to write chrome.manifest files to
|
|
* override chrome urls from the builtin tools.
|
|
*/
|
|
function SrcdirProvider() {}
|
|
SrcdirProvider.prototype = {
|
|
fileURI: function(path) {
|
|
let file = new FileUtils.File(path);
|
|
return Services.io.newFileURI(file).spec;
|
|
},
|
|
|
|
load: function() {
|
|
let srcDir = Services.prefs.getComplexValue("devtools.loader.srcdir",
|
|
Ci.nsISupportsString);
|
|
srcDir = OS.Path.normalize(srcDir.data.trim());
|
|
let devtoolsDir = OS.Path.join(srcDir, "devtools");
|
|
let sharedDir = OS.Path.join(devtoolsDir, "shared");
|
|
let modulesDir = OS.Path.join(srcDir, "toolkit", "modules");
|
|
let devtoolsURI = this.fileURI(devtoolsDir);
|
|
let gcliURI = this.fileURI(OS.Path.join(sharedDir,
|
|
"gcli", "source", "lib", "gcli"));
|
|
let promiseURI = this.fileURI(OS.Path.join(modulesDir,
|
|
"Promise-backend.js"));
|
|
let acornURI = this.fileURI(OS.Path.join(sharedDir, "acorn"));
|
|
let acornWalkURI = OS.Path.join(acornURI, "walk.js");
|
|
let sourceMapURI = this.fileURI(OS.Path.join(sharedDir,
|
|
"sourcemap", "source-map.js"));
|
|
this.loader = new Loader.Loader({
|
|
id: "fx-devtools",
|
|
modules: loaderModules,
|
|
paths: {
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"": "resource://gre/modules/commonjs/",
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"devtools": devtoolsURI,
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"gcli": gcliURI,
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"promise": promiseURI,
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"acorn": acornURI,
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"acorn/util/walk": acornWalkURI,
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
"source-map": sourceMapURI,
|
|
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
|
|
},
|
|
globals: this.globals,
|
|
invisibleToDebugger: this.invisibleToDebugger,
|
|
sharedGlobal: true,
|
|
sharedGlobalBlocklist,
|
|
});
|
|
|
|
return this._writeManifest(srcDir).then(null, Cu.reportError);
|
|
},
|
|
|
|
unload: function(reason) {
|
|
Loader.unload(this.loader, reason);
|
|
delete this.loader;
|
|
},
|
|
|
|
_readFile: function(filename) {
|
|
let deferred = promise.defer();
|
|
let file = new FileUtils.File(filename);
|
|
NetUtil.asyncFetch({
|
|
uri: NetUtil.newURI(file),
|
|
loadUsingSystemPrincipal: true
|
|
}, (inputStream, status) => {
|
|
if (!Components.isSuccessCode(status)) {
|
|
deferred.reject(new Error("Couldn't load manifest: " + filename + "\n"));
|
|
return;
|
|
}
|
|
var data = NetUtil.readInputStreamToString(inputStream, inputStream.available());
|
|
deferred.resolve(data);
|
|
});
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
_writeFile: function(filename, data) {
|
|
let promise = OS.File.writeAtomic(filename, data, {encoding: "utf-8"});
|
|
return promise.then(null, (ex) => new Error("Couldn't write manifest: " + ex + "\n"));
|
|
},
|
|
|
|
_writeManifest: function(srcDir) {
|
|
let clientDir = OS.Path.join(srcDir, "devtools", "client");
|
|
return this._readFile(OS.Path.join(clientDir, "jar.mn")).then((data) => {
|
|
// The file data is contained within inputStream.
|
|
// You can read it into a string with
|
|
let entries = [];
|
|
let lines = data.split(/\n/);
|
|
let preprocessed = /^\s*\*/;
|
|
let contentEntry = /^\s+content\/(\S+)\s+\((\S+)\)/;
|
|
for (let line of lines) {
|
|
if (preprocessed.test(line)) {
|
|
dump("Unable to override preprocessed file: " + line + "\n");
|
|
continue;
|
|
}
|
|
let match = contentEntry.exec(line);
|
|
if (match) {
|
|
let pathComponents = match[2].split("/");
|
|
pathComponents.unshift(clientDir);
|
|
let path = OS.Path.join.apply(OS.Path, pathComponents);
|
|
let uri = this.fileURI(path);
|
|
let chromeURI = "chrome://devtools/content/" + match[1];
|
|
let entry = "override " + chromeURI + "\t" + uri;
|
|
entries.push(entry);
|
|
}
|
|
}
|
|
return this._writeFile(OS.Path.join(clientDir, "chrome.manifest"),
|
|
entries.join("\n"));
|
|
}).then(() => {
|
|
let clientDirFile = new FileUtils.File(clientDir);
|
|
Components.manager.addBootstrappedManifestLocation(clientDirFile);
|
|
});
|
|
}
|
|
};
|
|
|
|
var gNextLoaderID = 0;
|
|
|
|
/**
|
|
* The main devtools API.
|
|
* In addition to a few loader-related details, this object will also include all
|
|
* exports from the main module. The standard instance of this loader is
|
|
* exported as |devtools| below, but if a fresh copy of the loader is needed,
|
|
* then a new one can also be created.
|
|
*/
|
|
this.DevToolsLoader = function DevToolsLoader() {
|
|
this.require = this.require.bind(this);
|
|
this.lazyGetter = XPCOMUtils.defineLazyGetter.bind(XPCOMUtils);
|
|
this.lazyImporter = XPCOMUtils.defineLazyModuleGetter.bind(XPCOMUtils);
|
|
this.lazyServiceGetter = XPCOMUtils.defineLazyServiceGetter.bind(XPCOMUtils);
|
|
this.lazyRequireGetter = this.lazyRequireGetter.bind(this);
|
|
this.main = this.main.bind(this);
|
|
};
|
|
|
|
DevToolsLoader.prototype = {
|
|
get provider() {
|
|
if (!this._provider) {
|
|
this._chooseProvider();
|
|
}
|
|
return this._provider;
|
|
},
|
|
|
|
_provider: null,
|
|
|
|
get id() {
|
|
if (this._id) {
|
|
return this._id;
|
|
} else {
|
|
return this._id = ++gNextLoaderID;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* A dummy version of require, in case a provider hasn't been chosen yet when
|
|
* this is first called. This will then be replaced by the real version.
|
|
* @see setProvider
|
|
*/
|
|
require: function() {
|
|
if (!this._provider) {
|
|
this._chooseProvider();
|
|
}
|
|
return this.require.apply(this, arguments);
|
|
},
|
|
|
|
/**
|
|
* Define a getter property on the given object that requires the given
|
|
* module. This enables delaying importing modules until the module is
|
|
* actually used.
|
|
*
|
|
* @param Object obj
|
|
* The object to define the property on.
|
|
* @param String property
|
|
* The property name.
|
|
* @param String module
|
|
* The module path.
|
|
* @param Boolean destructure
|
|
* Pass true if the property name is a member of the module's exports.
|
|
*/
|
|
lazyRequireGetter: function (obj, property, module, destructure) {
|
|
Object.defineProperty(obj, property, {
|
|
get: () => {
|
|
// Redefine this accessor property as a data property.
|
|
// Delete it first, to rule out "too much recursion" in case obj is
|
|
// a proxy whose defineProperty handler might unwittingly trigger this
|
|
// getter again.
|
|
delete obj[property];
|
|
let value = destructure
|
|
? this.require(module)[property]
|
|
: this.require(module || property);
|
|
Object.defineProperty(obj, property, {
|
|
value,
|
|
writable: true,
|
|
configurable: true,
|
|
enumerable: true
|
|
});
|
|
return value;
|
|
},
|
|
configurable: true,
|
|
enumerable: true
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Add a URI to the loader.
|
|
* @param string id
|
|
* The module id that can be used within the loader to refer to this module.
|
|
* @param string uri
|
|
* The URI to load as a module.
|
|
* @returns The module's exports.
|
|
*/
|
|
loadURI: function(id, uri) {
|
|
let module = Loader.Module(id, uri);
|
|
return Loader.load(this.provider.loader, module).exports;
|
|
},
|
|
|
|
/**
|
|
* Let the loader know the ID of the main module to load.
|
|
*
|
|
* The loader doesn't need a main module, but it's nice to have. This
|
|
* will be called by the browser devtools to load the devtools/main module.
|
|
*
|
|
* When only using the server, there's no main module, and this method
|
|
* can be ignored.
|
|
*/
|
|
main: function(id) {
|
|
// Ensure the main module isn't loaded twice, because it may have observable
|
|
// side-effects.
|
|
if (this._mainid) {
|
|
return;
|
|
}
|
|
this._mainid = id;
|
|
this._main = Loader.main(this.provider.loader, id);
|
|
|
|
// Mirror the main module's exports on this object.
|
|
Object.getOwnPropertyNames(this._main).forEach(key => {
|
|
XPCOMUtils.defineLazyGetter(this, key, () => this._main[key]);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Override the provider used to load the tools.
|
|
*/
|
|
setProvider: function(provider) {
|
|
if (provider === this._provider) {
|
|
return;
|
|
}
|
|
|
|
if (this._provider) {
|
|
var events = this.require("sdk/system/events");
|
|
events.emit("devtools-unloaded", {});
|
|
delete this.require;
|
|
this._provider.unload("newprovider");
|
|
}
|
|
this._provider = provider;
|
|
|
|
// Pass through internal loader settings specific to this loader instance
|
|
this._provider.invisibleToDebugger = this.invisibleToDebugger;
|
|
this._provider.globals = {
|
|
isWorker: false,
|
|
reportError: Cu.reportError,
|
|
atob: atob,
|
|
btoa: btoa,
|
|
_Iterator: Iterator,
|
|
loader: {
|
|
lazyGetter: this.lazyGetter,
|
|
lazyImporter: this.lazyImporter,
|
|
lazyServiceGetter: this.lazyServiceGetter,
|
|
lazyRequireGetter: this.lazyRequireGetter,
|
|
id: this.id,
|
|
main: this.main
|
|
},
|
|
// Make sure `define` function exists. This allows defining some modules
|
|
// in AMD format while retaining CommonJS compatibility through this hook.
|
|
// JSON Viewer needs modules in AMD format, as it currently uses RequireJS
|
|
// from a content document and can't access our usual loaders. So, any
|
|
// modules shared with the JSON Viewer should include a define wrapper:
|
|
//
|
|
// // Make this available to both AMD and CJS environments
|
|
// define(function(require, exports, module) {
|
|
// ... code ...
|
|
// });
|
|
//
|
|
// Bug 1248830 will work out a better plan here for our content module
|
|
// loading needs, especially as we head towards devtools.html.
|
|
define(factory) {
|
|
factory(this.require, this.exports, this.module);
|
|
},
|
|
};
|
|
// Lazy define console in order to load Console.jsm only when it is used
|
|
XPCOMUtils.defineLazyGetter(this._provider.globals, "console", () => {
|
|
return Cu.import("resource://gre/modules/Console.jsm", {}).console;
|
|
});
|
|
|
|
this._provider.load();
|
|
this.require = Loader.Require(this._provider.loader, { id: "devtools" });
|
|
|
|
if (this._mainid) {
|
|
this.main(this._mainid);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Choose a default tools provider based on the preferences.
|
|
*/
|
|
_chooseProvider: function() {
|
|
if (Services.prefs.prefHasUserValue("devtools.loader.srcdir")) {
|
|
this.setProvider(new SrcdirProvider());
|
|
} else {
|
|
this.setProvider(new BuiltinProvider());
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Reload the current provider.
|
|
*/
|
|
reload: function() {
|
|
var events = this.require("sdk/system/events");
|
|
events.emit("startupcache-invalidate", {});
|
|
events.emit("devtools-unloaded", {});
|
|
|
|
this._provider.unload("reload");
|
|
delete this._provider;
|
|
delete this._mainid;
|
|
this._chooseProvider();
|
|
this.main("devtools/client/main");
|
|
},
|
|
|
|
/**
|
|
* Sets whether the compartments loaded by this instance should be invisible
|
|
* to the debugger. Invisibility is needed for loaders that support debugging
|
|
* of chrome code. This is true of remote target environments, like Fennec or
|
|
* B2G. It is not the default case for desktop Firefox because we offer the
|
|
* Browser Toolbox for chrome debugging there, which uses its own, separate
|
|
* loader instance.
|
|
* @see devtools/client/framework/ToolboxProcess.jsm
|
|
*/
|
|
invisibleToDebugger: Services.appinfo.name !== "Firefox"
|
|
};
|
|
|
|
// Export the standard instance of DevToolsLoader used by the tools.
|
|
this.devtools = this.loader = new DevToolsLoader();
|
|
|
|
this.require = this.devtools.require.bind(this.devtools);
|