1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/Loader.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,394 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +/** 1.11 + * Manages the addon-sdk loader instance used to load the developer tools. 1.12 + */ 1.13 + 1.14 +let { Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu } = Components; 1.15 + 1.16 +// addDebuggerToGlobal only allows adding the Debugger object to a global. The 1.17 +// this object is not guaranteed to be a global (in particular on B2G, due to 1.18 +// compartment sharing), so add the Debugger object to a sandbox instead. 1.19 +let sandbox = Cu.Sandbox(CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')()); 1.20 +Cu.evalInSandbox( 1.21 + "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" + 1.22 + "addDebuggerToGlobal(this);", 1.23 + sandbox 1.24 +); 1.25 +let Debugger = sandbox.Debugger; 1.26 + 1.27 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.28 +Cu.import("resource://gre/modules/Services.jsm"); 1.29 +let Timer = Cu.import("resource://gre/modules/Timer.jsm", {}); 1.30 + 1.31 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); 1.32 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); 1.33 +XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); 1.34 +XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm"); 1.35 + 1.36 +let SourceMap = {}; 1.37 +Cu.import("resource://gre/modules/devtools/SourceMap.jsm", SourceMap); 1.38 + 1.39 +let loader = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}).Loader; 1.40 +let promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; 1.41 + 1.42 +this.EXPORTED_SYMBOLS = ["DevToolsLoader", "devtools", "BuiltinProvider", 1.43 + "SrcdirProvider"]; 1.44 + 1.45 +/** 1.46 + * Providers are different strategies for loading the devtools. 1.47 + */ 1.48 + 1.49 +let loaderGlobals = { 1.50 + btoa: btoa, 1.51 + console: console, 1.52 + promise: promise, 1.53 + _Iterator: Iterator, 1.54 + ChromeWorker: ChromeWorker, 1.55 + loader: { 1.56 + lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils), 1.57 + lazyImporter: XPCOMUtils.defineLazyModuleGetter.bind(XPCOMUtils), 1.58 + lazyServiceGetter: XPCOMUtils.defineLazyServiceGetter.bind(XPCOMUtils) 1.59 + } 1.60 +}; 1.61 + 1.62 +// Used when the tools should be loaded from the Firefox package itself (the default) 1.63 +function BuiltinProvider() {} 1.64 +BuiltinProvider.prototype = { 1.65 + load: function() { 1.66 + this.loader = new loader.Loader({ 1.67 + modules: { 1.68 + "Debugger": Debugger, 1.69 + "Services": Object.create(Services), 1.70 + "Timer": Object.create(Timer), 1.71 + "toolkit/loader": loader, 1.72 + "source-map": SourceMap, 1.73 + }, 1.74 + paths: { 1.75 + // When you add a line to this mapping, don't forget to make a 1.76 + // corresponding addition to the SrcdirProvider mapping below as well. 1.77 + "": "resource://gre/modules/commonjs/", 1.78 + "main": "resource:///modules/devtools/main.js", 1.79 + "devtools": "resource:///modules/devtools", 1.80 + "devtools/toolkit": "resource://gre/modules/devtools", 1.81 + "devtools/server": "resource://gre/modules/devtools/server", 1.82 + "devtools/toolkit/webconsole": "resource://gre/modules/devtools/toolkit/webconsole", 1.83 + "devtools/app-actor-front": "resource://gre/modules/devtools/app-actor-front.js", 1.84 + "devtools/styleinspector/css-logic": "resource://gre/modules/devtools/styleinspector/css-logic", 1.85 + "devtools/css-color": "resource://gre/modules/devtools/css-color", 1.86 + "devtools/output-parser": "resource://gre/modules/devtools/output-parser", 1.87 + "devtools/touch-events": "resource://gre/modules/devtools/touch-events", 1.88 + "devtools/client": "resource://gre/modules/devtools/client", 1.89 + "devtools/pretty-fast": "resource://gre/modules/devtools/pretty-fast.js", 1.90 + "devtools/async-utils": "resource://gre/modules/devtools/async-utils", 1.91 + "devtools/content-observer": "resource://gre/modules/devtools/content-observer", 1.92 + "gcli": "resource://gre/modules/devtools/gcli", 1.93 + "acorn": "resource://gre/modules/devtools/acorn", 1.94 + "acorn/util/walk": "resource://gre/modules/devtools/acorn/walk.js", 1.95 + 1.96 + // Allow access to xpcshell test items from the loader. 1.97 + "xpcshell-test": "resource://test" 1.98 + }, 1.99 + globals: loaderGlobals, 1.100 + invisibleToDebugger: this.invisibleToDebugger 1.101 + }); 1.102 + 1.103 + return promise.resolve(undefined); 1.104 + }, 1.105 + 1.106 + unload: function(reason) { 1.107 + loader.unload(this.loader, reason); 1.108 + delete this.loader; 1.109 + }, 1.110 +}; 1.111 + 1.112 +// Used when the tools should be loaded from a mozilla-central checkout. In addition 1.113 +// to different paths, it needs to write chrome.manifest files to override chrome urls 1.114 +// from the builtin tools. 1.115 +function SrcdirProvider() {} 1.116 +SrcdirProvider.prototype = { 1.117 + fileURI: function(path) { 1.118 + let file = new FileUtils.File(path); 1.119 + return Services.io.newFileURI(file).spec; 1.120 + }, 1.121 + 1.122 + load: function() { 1.123 + let srcdir = Services.prefs.getComplexValue("devtools.loader.srcdir", 1.124 + Ci.nsISupportsString); 1.125 + srcdir = OS.Path.normalize(srcdir.data.trim()); 1.126 + let devtoolsDir = OS.Path.join(srcdir, "browser", "devtools"); 1.127 + let toolkitDir = OS.Path.join(srcdir, "toolkit", "devtools"); 1.128 + let mainURI = this.fileURI(OS.Path.join(devtoolsDir, "main.js")); 1.129 + let devtoolsURI = this.fileURI(devtoolsDir); 1.130 + let toolkitURI = this.fileURI(toolkitDir); 1.131 + let serverURI = this.fileURI(OS.Path.join(toolkitDir, "server")); 1.132 + let webconsoleURI = this.fileURI(OS.Path.join(toolkitDir, "webconsole")); 1.133 + let appActorURI = this.fileURI(OS.Path.join(toolkitDir, "apps", "app-actor-front.js")); 1.134 + let cssLogicURI = this.fileURI(OS.Path.join(toolkitDir, "styleinspector", "css-logic")); 1.135 + let cssColorURI = this.fileURI(OS.Path.join(toolkitDir, "css-color")); 1.136 + let outputParserURI = this.fileURI(OS.Path.join(toolkitDir, "output-parser")); 1.137 + let touchEventsURI = this.fileURI(OS.Path.join(toolkitDir, "touch-events")); 1.138 + let clientURI = this.fileURI(OS.Path.join(toolkitDir, "client")); 1.139 + let prettyFastURI = this.fileURI(OS.Path.join(toolkitDir), "pretty-fast.js"); 1.140 + let asyncUtilsURI = this.fileURI(OS.Path.join(toolkitDir), "async-utils.js"); 1.141 + let contentObserverURI = this.fileURI(OS.Path.join(toolkitDir), "content-observer.js"); 1.142 + let gcliURI = this.fileURI(OS.Path.join(toolkitDir, "gcli", "source", "lib", "gcli")); 1.143 + let acornURI = this.fileURI(OS.Path.join(toolkitDir, "acorn")); 1.144 + let acornWalkURI = OS.Path.join(acornURI, "walk.js"); 1.145 + this.loader = new loader.Loader({ 1.146 + modules: { 1.147 + "Debugger": Debugger, 1.148 + "Services": Object.create(Services), 1.149 + "Timer": Object.create(Timer), 1.150 + "toolkit/loader": loader, 1.151 + "source-map": SourceMap, 1.152 + }, 1.153 + paths: { 1.154 + "": "resource://gre/modules/commonjs/", 1.155 + "main": mainURI, 1.156 + "devtools": devtoolsURI, 1.157 + "devtools/toolkit": toolkitURI, 1.158 + "devtools/server": serverURI, 1.159 + "devtools/toolkit/webconsole": webconsoleURI, 1.160 + "devtools/app-actor-front": appActorURI, 1.161 + "devtools/styleinspector/css-logic": cssLogicURI, 1.162 + "devtools/css-color": cssColorURI, 1.163 + "devtools/output-parser": outputParserURI, 1.164 + "devtools/touch-events": touchEventsURI, 1.165 + "devtools/client": clientURI, 1.166 + "devtools/pretty-fast": prettyFastURI, 1.167 + "devtools/async-utils": asyncUtilsURI, 1.168 + "devtools/content-observer": contentObserverURI, 1.169 + "gcli": gcliURI, 1.170 + "acorn": acornURI, 1.171 + "acorn/util/walk": acornWalkURI 1.172 + }, 1.173 + globals: loaderGlobals, 1.174 + invisibleToDebugger: this.invisibleToDebugger 1.175 + }); 1.176 + 1.177 + return this._writeManifest(devtoolsDir).then(null, Cu.reportError); 1.178 + }, 1.179 + 1.180 + unload: function(reason) { 1.181 + loader.unload(this.loader, reason); 1.182 + delete this.loader; 1.183 + }, 1.184 + 1.185 + _readFile: function(filename) { 1.186 + let deferred = promise.defer(); 1.187 + let file = new FileUtils.File(filename); 1.188 + NetUtil.asyncFetch(file, (inputStream, status) => { 1.189 + if (!Components.isSuccessCode(status)) { 1.190 + deferred.reject(new Error("Couldn't load manifest: " + filename + "\n")); 1.191 + return; 1.192 + } 1.193 + var data = NetUtil.readInputStreamToString(inputStream, inputStream.available()); 1.194 + deferred.resolve(data); 1.195 + }); 1.196 + return deferred.promise; 1.197 + }, 1.198 + 1.199 + _writeFile: function(filename, data) { 1.200 + let deferred = promise.defer(); 1.201 + let file = new FileUtils.File(filename); 1.202 + 1.203 + var ostream = FileUtils.openSafeFileOutputStream(file) 1.204 + 1.205 + var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. 1.206 + createInstance(Ci.nsIScriptableUnicodeConverter); 1.207 + converter.charset = "UTF-8"; 1.208 + var istream = converter.convertToInputStream(data); 1.209 + NetUtil.asyncCopy(istream, ostream, (status) => { 1.210 + if (!Components.isSuccessCode(status)) { 1.211 + deferred.reject(new Error("Couldn't write manifest: " + filename + "\n")); 1.212 + return; 1.213 + } 1.214 + 1.215 + deferred.resolve(null); 1.216 + }); 1.217 + return deferred.promise; 1.218 + }, 1.219 + 1.220 + _writeManifest: function(dir) { 1.221 + return this._readFile(OS.Path.join(dir, "jar.mn")).then((data) => { 1.222 + // The file data is contained within inputStream. 1.223 + // You can read it into a string with 1.224 + let entries = []; 1.225 + let lines = data.split(/\n/); 1.226 + let preprocessed = /^\s*\*/; 1.227 + let contentEntry = new RegExp("^\\s+content/(\\w+)/(\\S+)\\s+\\((\\S+)\\)"); 1.228 + for (let line of lines) { 1.229 + if (preprocessed.test(line)) { 1.230 + dump("Unable to override preprocessed file: " + line + "\n"); 1.231 + continue; 1.232 + } 1.233 + let match = contentEntry.exec(line); 1.234 + if (match) { 1.235 + let pathComponents = match[3].split("/"); 1.236 + pathComponents.unshift(dir); 1.237 + let path = OS.Path.join.apply(OS.Path, pathComponents); 1.238 + let uri = this.fileURI(path); 1.239 + let entry = "override chrome://" + match[1] + "/content/" + match[2] + "\t" + uri; 1.240 + entries.push(entry); 1.241 + } 1.242 + } 1.243 + return this._writeFile(OS.Path.join(dir, "chrome.manifest"), entries.join("\n")); 1.244 + }).then(() => { 1.245 + Components.manager.addBootstrappedManifestLocation(new FileUtils.File(dir)); 1.246 + }); 1.247 + } 1.248 +}; 1.249 + 1.250 +/** 1.251 + * The main devtools API. 1.252 + * In addition to a few loader-related details, this object will also include all 1.253 + * exports from the main module. The standard instance of this loader is 1.254 + * exported as |devtools| below, but if a fresh copy of the loader is needed, 1.255 + * then a new one can also be created. 1.256 + */ 1.257 +this.DevToolsLoader = function DevToolsLoader() { 1.258 + this.require = this.require.bind(this); 1.259 +}; 1.260 + 1.261 +DevToolsLoader.prototype = { 1.262 + get provider() { 1.263 + if (!this._provider) { 1.264 + this._chooseProvider(); 1.265 + } 1.266 + return this._provider; 1.267 + }, 1.268 + 1.269 + _provider: null, 1.270 + 1.271 + /** 1.272 + * A dummy version of require, in case a provider hasn't been chosen yet when 1.273 + * this is first called. This will then be replaced by the real version. 1.274 + * @see setProvider 1.275 + */ 1.276 + require: function() { 1.277 + this._chooseProvider(); 1.278 + return this.require.apply(this, arguments); 1.279 + }, 1.280 + 1.281 + /** 1.282 + * Define a getter property on the given object that requires the given 1.283 + * module. This enables delaying importing modules until the module is 1.284 + * actually used. 1.285 + * 1.286 + * @param Object obj 1.287 + * The object to define the property on. 1.288 + * @param String property 1.289 + * The property name. 1.290 + * @param String module 1.291 + * The module path. 1.292 + */ 1.293 + lazyRequireGetter: function (obj, property, module) { 1.294 + Object.defineProperty(obj, property, { 1.295 + get: () => this.require(module) 1.296 + }); 1.297 + }, 1.298 + 1.299 + /** 1.300 + * Add a URI to the loader. 1.301 + * @param string id 1.302 + * The module id that can be used within the loader to refer to this module. 1.303 + * @param string uri 1.304 + * The URI to load as a module. 1.305 + * @returns The module's exports. 1.306 + */ 1.307 + loadURI: function(id, uri) { 1.308 + let module = loader.Module(id, uri); 1.309 + return loader.load(this.provider.loader, module).exports; 1.310 + }, 1.311 + 1.312 + /** 1.313 + * Let the loader know the ID of the main module to load. 1.314 + * 1.315 + * The loader doesn't need a main module, but it's nice to have. This 1.316 + * will be called by the browser devtools to load the devtools/main module. 1.317 + * 1.318 + * When only using the server, there's no main module, and this method 1.319 + * can be ignored. 1.320 + */ 1.321 + main: function(id) { 1.322 + // Ensure the main module isn't loaded twice, because it may have observable 1.323 + // side-effects. 1.324 + if (this._mainid) { 1.325 + return; 1.326 + } 1.327 + this._mainid = id; 1.328 + this._main = loader.main(this.provider.loader, id); 1.329 + 1.330 + // Mirror the main module's exports on this object. 1.331 + Object.getOwnPropertyNames(this._main).forEach(key => { 1.332 + XPCOMUtils.defineLazyGetter(this, key, () => this._main[key]); 1.333 + }); 1.334 + }, 1.335 + 1.336 + /** 1.337 + * Override the provider used to load the tools. 1.338 + */ 1.339 + setProvider: function(provider) { 1.340 + if (provider === this._provider) { 1.341 + return; 1.342 + } 1.343 + 1.344 + if (this._provider) { 1.345 + var events = this.require("sdk/system/events"); 1.346 + events.emit("devtools-unloaded", {}); 1.347 + delete this.require; 1.348 + this._provider.unload("newprovider"); 1.349 + } 1.350 + this._provider = provider; 1.351 + this._provider.invisibleToDebugger = this.invisibleToDebugger; 1.352 + this._provider.load(); 1.353 + this.require = loader.Require(this._provider.loader, { id: "devtools" }); 1.354 + 1.355 + if (this._mainid) { 1.356 + this.main(this._mainid); 1.357 + } 1.358 + }, 1.359 + 1.360 + /** 1.361 + * Choose a default tools provider based on the preferences. 1.362 + */ 1.363 + _chooseProvider: function() { 1.364 + if (Services.prefs.prefHasUserValue("devtools.loader.srcdir")) { 1.365 + this.setProvider(new SrcdirProvider()); 1.366 + } else { 1.367 + this.setProvider(new BuiltinProvider()); 1.368 + } 1.369 + }, 1.370 + 1.371 + /** 1.372 + * Reload the current provider. 1.373 + */ 1.374 + reload: function() { 1.375 + var events = this.require("sdk/system/events"); 1.376 + events.emit("startupcache-invalidate", {}); 1.377 + events.emit("devtools-unloaded", {}); 1.378 + 1.379 + this._provider.unload("reload"); 1.380 + delete this._provider; 1.381 + this._chooseProvider(); 1.382 + }, 1.383 + 1.384 + /** 1.385 + * Sets whether the compartments loaded by this instance should be invisible 1.386 + * to the debugger. Invisibility is needed for loaders that support debugging 1.387 + * of chrome code. This is true of remote target environments, like Fennec or 1.388 + * B2G. It is not the default case for desktop Firefox because we offer the 1.389 + * Browser Toolbox for chrome debugging there, which uses its own, separate 1.390 + * loader instance. 1.391 + * @see browser/devtools/framework/ToolboxProcess.jsm 1.392 + */ 1.393 + invisibleToDebugger: Services.appinfo.name !== "Firefox" 1.394 +}; 1.395 + 1.396 +// Export the standard instance of DevToolsLoader used by the tools. 1.397 +this.devtools = new DevToolsLoader();