michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: /** michael@0: * Manages the addon-sdk loader instance used to load the developer tools. michael@0: */ michael@0: michael@0: let { Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu } = Components; michael@0: michael@0: // addDebuggerToGlobal only allows adding the Debugger object to a global. The michael@0: // this object is not guaranteed to be a global (in particular on B2G, due to michael@0: // compartment sharing), so add the Debugger object to a sandbox instead. michael@0: let sandbox = Cu.Sandbox(CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')()); michael@0: Cu.evalInSandbox( michael@0: "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" + michael@0: "addDebuggerToGlobal(this);", michael@0: sandbox michael@0: ); michael@0: let Debugger = sandbox.Debugger; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: let Timer = Cu.import("resource://gre/modules/Timer.jsm", {}); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm"); michael@0: michael@0: let SourceMap = {}; michael@0: Cu.import("resource://gre/modules/devtools/SourceMap.jsm", SourceMap); michael@0: michael@0: let loader = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}).Loader; michael@0: let promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["DevToolsLoader", "devtools", "BuiltinProvider", michael@0: "SrcdirProvider"]; michael@0: michael@0: /** michael@0: * Providers are different strategies for loading the devtools. michael@0: */ michael@0: michael@0: let loaderGlobals = { michael@0: btoa: btoa, michael@0: console: console, michael@0: promise: promise, michael@0: _Iterator: Iterator, michael@0: ChromeWorker: ChromeWorker, michael@0: loader: { michael@0: lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils), michael@0: lazyImporter: XPCOMUtils.defineLazyModuleGetter.bind(XPCOMUtils), michael@0: lazyServiceGetter: XPCOMUtils.defineLazyServiceGetter.bind(XPCOMUtils) michael@0: } michael@0: }; michael@0: michael@0: // Used when the tools should be loaded from the Firefox package itself (the default) michael@0: function BuiltinProvider() {} michael@0: BuiltinProvider.prototype = { michael@0: load: function() { michael@0: this.loader = new loader.Loader({ michael@0: modules: { michael@0: "Debugger": Debugger, michael@0: "Services": Object.create(Services), michael@0: "Timer": Object.create(Timer), michael@0: "toolkit/loader": loader, michael@0: "source-map": SourceMap, michael@0: }, michael@0: paths: { michael@0: // When you add a line to this mapping, don't forget to make a michael@0: // corresponding addition to the SrcdirProvider mapping below as well. michael@0: "": "resource://gre/modules/commonjs/", michael@0: "main": "resource:///modules/devtools/main.js", michael@0: "devtools": "resource:///modules/devtools", michael@0: "devtools/toolkit": "resource://gre/modules/devtools", michael@0: "devtools/server": "resource://gre/modules/devtools/server", michael@0: "devtools/toolkit/webconsole": "resource://gre/modules/devtools/toolkit/webconsole", michael@0: "devtools/app-actor-front": "resource://gre/modules/devtools/app-actor-front.js", michael@0: "devtools/styleinspector/css-logic": "resource://gre/modules/devtools/styleinspector/css-logic", michael@0: "devtools/css-color": "resource://gre/modules/devtools/css-color", michael@0: "devtools/output-parser": "resource://gre/modules/devtools/output-parser", michael@0: "devtools/touch-events": "resource://gre/modules/devtools/touch-events", michael@0: "devtools/client": "resource://gre/modules/devtools/client", michael@0: "devtools/pretty-fast": "resource://gre/modules/devtools/pretty-fast.js", michael@0: "devtools/async-utils": "resource://gre/modules/devtools/async-utils", michael@0: "devtools/content-observer": "resource://gre/modules/devtools/content-observer", michael@0: "gcli": "resource://gre/modules/devtools/gcli", michael@0: "acorn": "resource://gre/modules/devtools/acorn", michael@0: "acorn/util/walk": "resource://gre/modules/devtools/acorn/walk.js", michael@0: michael@0: // Allow access to xpcshell test items from the loader. michael@0: "xpcshell-test": "resource://test" michael@0: }, michael@0: globals: loaderGlobals, michael@0: invisibleToDebugger: this.invisibleToDebugger michael@0: }); michael@0: michael@0: return promise.resolve(undefined); michael@0: }, michael@0: michael@0: unload: function(reason) { michael@0: loader.unload(this.loader, reason); michael@0: delete this.loader; michael@0: }, michael@0: }; michael@0: michael@0: // Used when the tools should be loaded from a mozilla-central checkout. In addition michael@0: // to different paths, it needs to write chrome.manifest files to override chrome urls michael@0: // from the builtin tools. michael@0: function SrcdirProvider() {} michael@0: SrcdirProvider.prototype = { michael@0: fileURI: function(path) { michael@0: let file = new FileUtils.File(path); michael@0: return Services.io.newFileURI(file).spec; michael@0: }, michael@0: michael@0: load: function() { michael@0: let srcdir = Services.prefs.getComplexValue("devtools.loader.srcdir", michael@0: Ci.nsISupportsString); michael@0: srcdir = OS.Path.normalize(srcdir.data.trim()); michael@0: let devtoolsDir = OS.Path.join(srcdir, "browser", "devtools"); michael@0: let toolkitDir = OS.Path.join(srcdir, "toolkit", "devtools"); michael@0: let mainURI = this.fileURI(OS.Path.join(devtoolsDir, "main.js")); michael@0: let devtoolsURI = this.fileURI(devtoolsDir); michael@0: let toolkitURI = this.fileURI(toolkitDir); michael@0: let serverURI = this.fileURI(OS.Path.join(toolkitDir, "server")); michael@0: let webconsoleURI = this.fileURI(OS.Path.join(toolkitDir, "webconsole")); michael@0: let appActorURI = this.fileURI(OS.Path.join(toolkitDir, "apps", "app-actor-front.js")); michael@0: let cssLogicURI = this.fileURI(OS.Path.join(toolkitDir, "styleinspector", "css-logic")); michael@0: let cssColorURI = this.fileURI(OS.Path.join(toolkitDir, "css-color")); michael@0: let outputParserURI = this.fileURI(OS.Path.join(toolkitDir, "output-parser")); michael@0: let touchEventsURI = this.fileURI(OS.Path.join(toolkitDir, "touch-events")); michael@0: let clientURI = this.fileURI(OS.Path.join(toolkitDir, "client")); michael@0: let prettyFastURI = this.fileURI(OS.Path.join(toolkitDir), "pretty-fast.js"); michael@0: let asyncUtilsURI = this.fileURI(OS.Path.join(toolkitDir), "async-utils.js"); michael@0: let contentObserverURI = this.fileURI(OS.Path.join(toolkitDir), "content-observer.js"); michael@0: let gcliURI = this.fileURI(OS.Path.join(toolkitDir, "gcli", "source", "lib", "gcli")); michael@0: let acornURI = this.fileURI(OS.Path.join(toolkitDir, "acorn")); michael@0: let acornWalkURI = OS.Path.join(acornURI, "walk.js"); michael@0: this.loader = new loader.Loader({ michael@0: modules: { michael@0: "Debugger": Debugger, michael@0: "Services": Object.create(Services), michael@0: "Timer": Object.create(Timer), michael@0: "toolkit/loader": loader, michael@0: "source-map": SourceMap, michael@0: }, michael@0: paths: { michael@0: "": "resource://gre/modules/commonjs/", michael@0: "main": mainURI, michael@0: "devtools": devtoolsURI, michael@0: "devtools/toolkit": toolkitURI, michael@0: "devtools/server": serverURI, michael@0: "devtools/toolkit/webconsole": webconsoleURI, michael@0: "devtools/app-actor-front": appActorURI, michael@0: "devtools/styleinspector/css-logic": cssLogicURI, michael@0: "devtools/css-color": cssColorURI, michael@0: "devtools/output-parser": outputParserURI, michael@0: "devtools/touch-events": touchEventsURI, michael@0: "devtools/client": clientURI, michael@0: "devtools/pretty-fast": prettyFastURI, michael@0: "devtools/async-utils": asyncUtilsURI, michael@0: "devtools/content-observer": contentObserverURI, michael@0: "gcli": gcliURI, michael@0: "acorn": acornURI, michael@0: "acorn/util/walk": acornWalkURI michael@0: }, michael@0: globals: loaderGlobals, michael@0: invisibleToDebugger: this.invisibleToDebugger michael@0: }); michael@0: michael@0: return this._writeManifest(devtoolsDir).then(null, Cu.reportError); michael@0: }, michael@0: michael@0: unload: function(reason) { michael@0: loader.unload(this.loader, reason); michael@0: delete this.loader; michael@0: }, michael@0: michael@0: _readFile: function(filename) { michael@0: let deferred = promise.defer(); michael@0: let file = new FileUtils.File(filename); michael@0: NetUtil.asyncFetch(file, (inputStream, status) => { michael@0: if (!Components.isSuccessCode(status)) { michael@0: deferred.reject(new Error("Couldn't load manifest: " + filename + "\n")); michael@0: return; michael@0: } michael@0: var data = NetUtil.readInputStreamToString(inputStream, inputStream.available()); michael@0: deferred.resolve(data); michael@0: }); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: _writeFile: function(filename, data) { michael@0: let deferred = promise.defer(); michael@0: let file = new FileUtils.File(filename); michael@0: michael@0: var ostream = FileUtils.openSafeFileOutputStream(file) michael@0: michael@0: var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. michael@0: createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: var istream = converter.convertToInputStream(data); michael@0: NetUtil.asyncCopy(istream, ostream, (status) => { michael@0: if (!Components.isSuccessCode(status)) { michael@0: deferred.reject(new Error("Couldn't write manifest: " + filename + "\n")); michael@0: return; michael@0: } michael@0: michael@0: deferred.resolve(null); michael@0: }); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: _writeManifest: function(dir) { michael@0: return this._readFile(OS.Path.join(dir, "jar.mn")).then((data) => { michael@0: // The file data is contained within inputStream. michael@0: // You can read it into a string with michael@0: let entries = []; michael@0: let lines = data.split(/\n/); michael@0: let preprocessed = /^\s*\*/; michael@0: let contentEntry = new RegExp("^\\s+content/(\\w+)/(\\S+)\\s+\\((\\S+)\\)"); michael@0: for (let line of lines) { michael@0: if (preprocessed.test(line)) { michael@0: dump("Unable to override preprocessed file: " + line + "\n"); michael@0: continue; michael@0: } michael@0: let match = contentEntry.exec(line); michael@0: if (match) { michael@0: let pathComponents = match[3].split("/"); michael@0: pathComponents.unshift(dir); michael@0: let path = OS.Path.join.apply(OS.Path, pathComponents); michael@0: let uri = this.fileURI(path); michael@0: let entry = "override chrome://" + match[1] + "/content/" + match[2] + "\t" + uri; michael@0: entries.push(entry); michael@0: } michael@0: } michael@0: return this._writeFile(OS.Path.join(dir, "chrome.manifest"), entries.join("\n")); michael@0: }).then(() => { michael@0: Components.manager.addBootstrappedManifestLocation(new FileUtils.File(dir)); michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * The main devtools API. michael@0: * In addition to a few loader-related details, this object will also include all michael@0: * exports from the main module. The standard instance of this loader is michael@0: * exported as |devtools| below, but if a fresh copy of the loader is needed, michael@0: * then a new one can also be created. michael@0: */ michael@0: this.DevToolsLoader = function DevToolsLoader() { michael@0: this.require = this.require.bind(this); michael@0: }; michael@0: michael@0: DevToolsLoader.prototype = { michael@0: get provider() { michael@0: if (!this._provider) { michael@0: this._chooseProvider(); michael@0: } michael@0: return this._provider; michael@0: }, michael@0: michael@0: _provider: null, michael@0: michael@0: /** michael@0: * A dummy version of require, in case a provider hasn't been chosen yet when michael@0: * this is first called. This will then be replaced by the real version. michael@0: * @see setProvider michael@0: */ michael@0: require: function() { michael@0: this._chooseProvider(); michael@0: return this.require.apply(this, arguments); michael@0: }, michael@0: michael@0: /** michael@0: * Define a getter property on the given object that requires the given michael@0: * module. This enables delaying importing modules until the module is michael@0: * actually used. michael@0: * michael@0: * @param Object obj michael@0: * The object to define the property on. michael@0: * @param String property michael@0: * The property name. michael@0: * @param String module michael@0: * The module path. michael@0: */ michael@0: lazyRequireGetter: function (obj, property, module) { michael@0: Object.defineProperty(obj, property, { michael@0: get: () => this.require(module) michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Add a URI to the loader. michael@0: * @param string id michael@0: * The module id that can be used within the loader to refer to this module. michael@0: * @param string uri michael@0: * The URI to load as a module. michael@0: * @returns The module's exports. michael@0: */ michael@0: loadURI: function(id, uri) { michael@0: let module = loader.Module(id, uri); michael@0: return loader.load(this.provider.loader, module).exports; michael@0: }, michael@0: michael@0: /** michael@0: * Let the loader know the ID of the main module to load. michael@0: * michael@0: * The loader doesn't need a main module, but it's nice to have. This michael@0: * will be called by the browser devtools to load the devtools/main module. michael@0: * michael@0: * When only using the server, there's no main module, and this method michael@0: * can be ignored. michael@0: */ michael@0: main: function(id) { michael@0: // Ensure the main module isn't loaded twice, because it may have observable michael@0: // side-effects. michael@0: if (this._mainid) { michael@0: return; michael@0: } michael@0: this._mainid = id; michael@0: this._main = loader.main(this.provider.loader, id); michael@0: michael@0: // Mirror the main module's exports on this object. michael@0: Object.getOwnPropertyNames(this._main).forEach(key => { michael@0: XPCOMUtils.defineLazyGetter(this, key, () => this._main[key]); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Override the provider used to load the tools. michael@0: */ michael@0: setProvider: function(provider) { michael@0: if (provider === this._provider) { michael@0: return; michael@0: } michael@0: michael@0: if (this._provider) { michael@0: var events = this.require("sdk/system/events"); michael@0: events.emit("devtools-unloaded", {}); michael@0: delete this.require; michael@0: this._provider.unload("newprovider"); michael@0: } michael@0: this._provider = provider; michael@0: this._provider.invisibleToDebugger = this.invisibleToDebugger; michael@0: this._provider.load(); michael@0: this.require = loader.Require(this._provider.loader, { id: "devtools" }); michael@0: michael@0: if (this._mainid) { michael@0: this.main(this._mainid); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Choose a default tools provider based on the preferences. michael@0: */ michael@0: _chooseProvider: function() { michael@0: if (Services.prefs.prefHasUserValue("devtools.loader.srcdir")) { michael@0: this.setProvider(new SrcdirProvider()); michael@0: } else { michael@0: this.setProvider(new BuiltinProvider()); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Reload the current provider. michael@0: */ michael@0: reload: function() { michael@0: var events = this.require("sdk/system/events"); michael@0: events.emit("startupcache-invalidate", {}); michael@0: events.emit("devtools-unloaded", {}); michael@0: michael@0: this._provider.unload("reload"); michael@0: delete this._provider; michael@0: this._chooseProvider(); michael@0: }, michael@0: michael@0: /** michael@0: * Sets whether the compartments loaded by this instance should be invisible michael@0: * to the debugger. Invisibility is needed for loaders that support debugging michael@0: * of chrome code. This is true of remote target environments, like Fennec or michael@0: * B2G. It is not the default case for desktop Firefox because we offer the michael@0: * Browser Toolbox for chrome debugging there, which uses its own, separate michael@0: * loader instance. michael@0: * @see browser/devtools/framework/ToolboxProcess.jsm michael@0: */ michael@0: invisibleToDebugger: Services.appinfo.name !== "Firefox" michael@0: }; michael@0: michael@0: // Export the standard instance of DevToolsLoader used by the tools. michael@0: this.devtools = new DevToolsLoader();