|
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 "use strict"; |
|
6 |
|
7 /** |
|
8 * Manages the addon-sdk loader instance used to load the developer tools. |
|
9 */ |
|
10 |
|
11 let { Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu } = Components; |
|
12 |
|
13 // addDebuggerToGlobal only allows adding the Debugger object to a global. The |
|
14 // this object is not guaranteed to be a global (in particular on B2G, due to |
|
15 // compartment sharing), so add the Debugger object to a sandbox instead. |
|
16 let sandbox = Cu.Sandbox(CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')()); |
|
17 Cu.evalInSandbox( |
|
18 "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" + |
|
19 "addDebuggerToGlobal(this);", |
|
20 sandbox |
|
21 ); |
|
22 let Debugger = sandbox.Debugger; |
|
23 |
|
24 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
25 Cu.import("resource://gre/modules/Services.jsm"); |
|
26 let Timer = Cu.import("resource://gre/modules/Timer.jsm", {}); |
|
27 |
|
28 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); |
|
29 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); |
|
30 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); |
|
31 XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm"); |
|
32 |
|
33 let SourceMap = {}; |
|
34 Cu.import("resource://gre/modules/devtools/SourceMap.jsm", SourceMap); |
|
35 |
|
36 let loader = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}).Loader; |
|
37 let promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; |
|
38 |
|
39 this.EXPORTED_SYMBOLS = ["DevToolsLoader", "devtools", "BuiltinProvider", |
|
40 "SrcdirProvider"]; |
|
41 |
|
42 /** |
|
43 * Providers are different strategies for loading the devtools. |
|
44 */ |
|
45 |
|
46 let loaderGlobals = { |
|
47 btoa: btoa, |
|
48 console: console, |
|
49 promise: promise, |
|
50 _Iterator: Iterator, |
|
51 ChromeWorker: ChromeWorker, |
|
52 loader: { |
|
53 lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils), |
|
54 lazyImporter: XPCOMUtils.defineLazyModuleGetter.bind(XPCOMUtils), |
|
55 lazyServiceGetter: XPCOMUtils.defineLazyServiceGetter.bind(XPCOMUtils) |
|
56 } |
|
57 }; |
|
58 |
|
59 // Used when the tools should be loaded from the Firefox package itself (the default) |
|
60 function BuiltinProvider() {} |
|
61 BuiltinProvider.prototype = { |
|
62 load: function() { |
|
63 this.loader = new loader.Loader({ |
|
64 modules: { |
|
65 "Debugger": Debugger, |
|
66 "Services": Object.create(Services), |
|
67 "Timer": Object.create(Timer), |
|
68 "toolkit/loader": loader, |
|
69 "source-map": SourceMap, |
|
70 }, |
|
71 paths: { |
|
72 // When you add a line to this mapping, don't forget to make a |
|
73 // corresponding addition to the SrcdirProvider mapping below as well. |
|
74 "": "resource://gre/modules/commonjs/", |
|
75 "main": "resource:///modules/devtools/main.js", |
|
76 "devtools": "resource:///modules/devtools", |
|
77 "devtools/toolkit": "resource://gre/modules/devtools", |
|
78 "devtools/server": "resource://gre/modules/devtools/server", |
|
79 "devtools/toolkit/webconsole": "resource://gre/modules/devtools/toolkit/webconsole", |
|
80 "devtools/app-actor-front": "resource://gre/modules/devtools/app-actor-front.js", |
|
81 "devtools/styleinspector/css-logic": "resource://gre/modules/devtools/styleinspector/css-logic", |
|
82 "devtools/css-color": "resource://gre/modules/devtools/css-color", |
|
83 "devtools/output-parser": "resource://gre/modules/devtools/output-parser", |
|
84 "devtools/touch-events": "resource://gre/modules/devtools/touch-events", |
|
85 "devtools/client": "resource://gre/modules/devtools/client", |
|
86 "devtools/pretty-fast": "resource://gre/modules/devtools/pretty-fast.js", |
|
87 "devtools/async-utils": "resource://gre/modules/devtools/async-utils", |
|
88 "devtools/content-observer": "resource://gre/modules/devtools/content-observer", |
|
89 "gcli": "resource://gre/modules/devtools/gcli", |
|
90 "acorn": "resource://gre/modules/devtools/acorn", |
|
91 "acorn/util/walk": "resource://gre/modules/devtools/acorn/walk.js", |
|
92 |
|
93 // Allow access to xpcshell test items from the loader. |
|
94 "xpcshell-test": "resource://test" |
|
95 }, |
|
96 globals: loaderGlobals, |
|
97 invisibleToDebugger: this.invisibleToDebugger |
|
98 }); |
|
99 |
|
100 return promise.resolve(undefined); |
|
101 }, |
|
102 |
|
103 unload: function(reason) { |
|
104 loader.unload(this.loader, reason); |
|
105 delete this.loader; |
|
106 }, |
|
107 }; |
|
108 |
|
109 // Used when the tools should be loaded from a mozilla-central checkout. In addition |
|
110 // to different paths, it needs to write chrome.manifest files to override chrome urls |
|
111 // from the builtin tools. |
|
112 function SrcdirProvider() {} |
|
113 SrcdirProvider.prototype = { |
|
114 fileURI: function(path) { |
|
115 let file = new FileUtils.File(path); |
|
116 return Services.io.newFileURI(file).spec; |
|
117 }, |
|
118 |
|
119 load: function() { |
|
120 let srcdir = Services.prefs.getComplexValue("devtools.loader.srcdir", |
|
121 Ci.nsISupportsString); |
|
122 srcdir = OS.Path.normalize(srcdir.data.trim()); |
|
123 let devtoolsDir = OS.Path.join(srcdir, "browser", "devtools"); |
|
124 let toolkitDir = OS.Path.join(srcdir, "toolkit", "devtools"); |
|
125 let mainURI = this.fileURI(OS.Path.join(devtoolsDir, "main.js")); |
|
126 let devtoolsURI = this.fileURI(devtoolsDir); |
|
127 let toolkitURI = this.fileURI(toolkitDir); |
|
128 let serverURI = this.fileURI(OS.Path.join(toolkitDir, "server")); |
|
129 let webconsoleURI = this.fileURI(OS.Path.join(toolkitDir, "webconsole")); |
|
130 let appActorURI = this.fileURI(OS.Path.join(toolkitDir, "apps", "app-actor-front.js")); |
|
131 let cssLogicURI = this.fileURI(OS.Path.join(toolkitDir, "styleinspector", "css-logic")); |
|
132 let cssColorURI = this.fileURI(OS.Path.join(toolkitDir, "css-color")); |
|
133 let outputParserURI = this.fileURI(OS.Path.join(toolkitDir, "output-parser")); |
|
134 let touchEventsURI = this.fileURI(OS.Path.join(toolkitDir, "touch-events")); |
|
135 let clientURI = this.fileURI(OS.Path.join(toolkitDir, "client")); |
|
136 let prettyFastURI = this.fileURI(OS.Path.join(toolkitDir), "pretty-fast.js"); |
|
137 let asyncUtilsURI = this.fileURI(OS.Path.join(toolkitDir), "async-utils.js"); |
|
138 let contentObserverURI = this.fileURI(OS.Path.join(toolkitDir), "content-observer.js"); |
|
139 let gcliURI = this.fileURI(OS.Path.join(toolkitDir, "gcli", "source", "lib", "gcli")); |
|
140 let acornURI = this.fileURI(OS.Path.join(toolkitDir, "acorn")); |
|
141 let acornWalkURI = OS.Path.join(acornURI, "walk.js"); |
|
142 this.loader = new loader.Loader({ |
|
143 modules: { |
|
144 "Debugger": Debugger, |
|
145 "Services": Object.create(Services), |
|
146 "Timer": Object.create(Timer), |
|
147 "toolkit/loader": loader, |
|
148 "source-map": SourceMap, |
|
149 }, |
|
150 paths: { |
|
151 "": "resource://gre/modules/commonjs/", |
|
152 "main": mainURI, |
|
153 "devtools": devtoolsURI, |
|
154 "devtools/toolkit": toolkitURI, |
|
155 "devtools/server": serverURI, |
|
156 "devtools/toolkit/webconsole": webconsoleURI, |
|
157 "devtools/app-actor-front": appActorURI, |
|
158 "devtools/styleinspector/css-logic": cssLogicURI, |
|
159 "devtools/css-color": cssColorURI, |
|
160 "devtools/output-parser": outputParserURI, |
|
161 "devtools/touch-events": touchEventsURI, |
|
162 "devtools/client": clientURI, |
|
163 "devtools/pretty-fast": prettyFastURI, |
|
164 "devtools/async-utils": asyncUtilsURI, |
|
165 "devtools/content-observer": contentObserverURI, |
|
166 "gcli": gcliURI, |
|
167 "acorn": acornURI, |
|
168 "acorn/util/walk": acornWalkURI |
|
169 }, |
|
170 globals: loaderGlobals, |
|
171 invisibleToDebugger: this.invisibleToDebugger |
|
172 }); |
|
173 |
|
174 return this._writeManifest(devtoolsDir).then(null, Cu.reportError); |
|
175 }, |
|
176 |
|
177 unload: function(reason) { |
|
178 loader.unload(this.loader, reason); |
|
179 delete this.loader; |
|
180 }, |
|
181 |
|
182 _readFile: function(filename) { |
|
183 let deferred = promise.defer(); |
|
184 let file = new FileUtils.File(filename); |
|
185 NetUtil.asyncFetch(file, (inputStream, status) => { |
|
186 if (!Components.isSuccessCode(status)) { |
|
187 deferred.reject(new Error("Couldn't load manifest: " + filename + "\n")); |
|
188 return; |
|
189 } |
|
190 var data = NetUtil.readInputStreamToString(inputStream, inputStream.available()); |
|
191 deferred.resolve(data); |
|
192 }); |
|
193 return deferred.promise; |
|
194 }, |
|
195 |
|
196 _writeFile: function(filename, data) { |
|
197 let deferred = promise.defer(); |
|
198 let file = new FileUtils.File(filename); |
|
199 |
|
200 var ostream = FileUtils.openSafeFileOutputStream(file) |
|
201 |
|
202 var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. |
|
203 createInstance(Ci.nsIScriptableUnicodeConverter); |
|
204 converter.charset = "UTF-8"; |
|
205 var istream = converter.convertToInputStream(data); |
|
206 NetUtil.asyncCopy(istream, ostream, (status) => { |
|
207 if (!Components.isSuccessCode(status)) { |
|
208 deferred.reject(new Error("Couldn't write manifest: " + filename + "\n")); |
|
209 return; |
|
210 } |
|
211 |
|
212 deferred.resolve(null); |
|
213 }); |
|
214 return deferred.promise; |
|
215 }, |
|
216 |
|
217 _writeManifest: function(dir) { |
|
218 return this._readFile(OS.Path.join(dir, "jar.mn")).then((data) => { |
|
219 // The file data is contained within inputStream. |
|
220 // You can read it into a string with |
|
221 let entries = []; |
|
222 let lines = data.split(/\n/); |
|
223 let preprocessed = /^\s*\*/; |
|
224 let contentEntry = new RegExp("^\\s+content/(\\w+)/(\\S+)\\s+\\((\\S+)\\)"); |
|
225 for (let line of lines) { |
|
226 if (preprocessed.test(line)) { |
|
227 dump("Unable to override preprocessed file: " + line + "\n"); |
|
228 continue; |
|
229 } |
|
230 let match = contentEntry.exec(line); |
|
231 if (match) { |
|
232 let pathComponents = match[3].split("/"); |
|
233 pathComponents.unshift(dir); |
|
234 let path = OS.Path.join.apply(OS.Path, pathComponents); |
|
235 let uri = this.fileURI(path); |
|
236 let entry = "override chrome://" + match[1] + "/content/" + match[2] + "\t" + uri; |
|
237 entries.push(entry); |
|
238 } |
|
239 } |
|
240 return this._writeFile(OS.Path.join(dir, "chrome.manifest"), entries.join("\n")); |
|
241 }).then(() => { |
|
242 Components.manager.addBootstrappedManifestLocation(new FileUtils.File(dir)); |
|
243 }); |
|
244 } |
|
245 }; |
|
246 |
|
247 /** |
|
248 * The main devtools API. |
|
249 * In addition to a few loader-related details, this object will also include all |
|
250 * exports from the main module. The standard instance of this loader is |
|
251 * exported as |devtools| below, but if a fresh copy of the loader is needed, |
|
252 * then a new one can also be created. |
|
253 */ |
|
254 this.DevToolsLoader = function DevToolsLoader() { |
|
255 this.require = this.require.bind(this); |
|
256 }; |
|
257 |
|
258 DevToolsLoader.prototype = { |
|
259 get provider() { |
|
260 if (!this._provider) { |
|
261 this._chooseProvider(); |
|
262 } |
|
263 return this._provider; |
|
264 }, |
|
265 |
|
266 _provider: null, |
|
267 |
|
268 /** |
|
269 * A dummy version of require, in case a provider hasn't been chosen yet when |
|
270 * this is first called. This will then be replaced by the real version. |
|
271 * @see setProvider |
|
272 */ |
|
273 require: function() { |
|
274 this._chooseProvider(); |
|
275 return this.require.apply(this, arguments); |
|
276 }, |
|
277 |
|
278 /** |
|
279 * Define a getter property on the given object that requires the given |
|
280 * module. This enables delaying importing modules until the module is |
|
281 * actually used. |
|
282 * |
|
283 * @param Object obj |
|
284 * The object to define the property on. |
|
285 * @param String property |
|
286 * The property name. |
|
287 * @param String module |
|
288 * The module path. |
|
289 */ |
|
290 lazyRequireGetter: function (obj, property, module) { |
|
291 Object.defineProperty(obj, property, { |
|
292 get: () => this.require(module) |
|
293 }); |
|
294 }, |
|
295 |
|
296 /** |
|
297 * Add a URI to the loader. |
|
298 * @param string id |
|
299 * The module id that can be used within the loader to refer to this module. |
|
300 * @param string uri |
|
301 * The URI to load as a module. |
|
302 * @returns The module's exports. |
|
303 */ |
|
304 loadURI: function(id, uri) { |
|
305 let module = loader.Module(id, uri); |
|
306 return loader.load(this.provider.loader, module).exports; |
|
307 }, |
|
308 |
|
309 /** |
|
310 * Let the loader know the ID of the main module to load. |
|
311 * |
|
312 * The loader doesn't need a main module, but it's nice to have. This |
|
313 * will be called by the browser devtools to load the devtools/main module. |
|
314 * |
|
315 * When only using the server, there's no main module, and this method |
|
316 * can be ignored. |
|
317 */ |
|
318 main: function(id) { |
|
319 // Ensure the main module isn't loaded twice, because it may have observable |
|
320 // side-effects. |
|
321 if (this._mainid) { |
|
322 return; |
|
323 } |
|
324 this._mainid = id; |
|
325 this._main = loader.main(this.provider.loader, id); |
|
326 |
|
327 // Mirror the main module's exports on this object. |
|
328 Object.getOwnPropertyNames(this._main).forEach(key => { |
|
329 XPCOMUtils.defineLazyGetter(this, key, () => this._main[key]); |
|
330 }); |
|
331 }, |
|
332 |
|
333 /** |
|
334 * Override the provider used to load the tools. |
|
335 */ |
|
336 setProvider: function(provider) { |
|
337 if (provider === this._provider) { |
|
338 return; |
|
339 } |
|
340 |
|
341 if (this._provider) { |
|
342 var events = this.require("sdk/system/events"); |
|
343 events.emit("devtools-unloaded", {}); |
|
344 delete this.require; |
|
345 this._provider.unload("newprovider"); |
|
346 } |
|
347 this._provider = provider; |
|
348 this._provider.invisibleToDebugger = this.invisibleToDebugger; |
|
349 this._provider.load(); |
|
350 this.require = loader.Require(this._provider.loader, { id: "devtools" }); |
|
351 |
|
352 if (this._mainid) { |
|
353 this.main(this._mainid); |
|
354 } |
|
355 }, |
|
356 |
|
357 /** |
|
358 * Choose a default tools provider based on the preferences. |
|
359 */ |
|
360 _chooseProvider: function() { |
|
361 if (Services.prefs.prefHasUserValue("devtools.loader.srcdir")) { |
|
362 this.setProvider(new SrcdirProvider()); |
|
363 } else { |
|
364 this.setProvider(new BuiltinProvider()); |
|
365 } |
|
366 }, |
|
367 |
|
368 /** |
|
369 * Reload the current provider. |
|
370 */ |
|
371 reload: function() { |
|
372 var events = this.require("sdk/system/events"); |
|
373 events.emit("startupcache-invalidate", {}); |
|
374 events.emit("devtools-unloaded", {}); |
|
375 |
|
376 this._provider.unload("reload"); |
|
377 delete this._provider; |
|
378 this._chooseProvider(); |
|
379 }, |
|
380 |
|
381 /** |
|
382 * Sets whether the compartments loaded by this instance should be invisible |
|
383 * to the debugger. Invisibility is needed for loaders that support debugging |
|
384 * of chrome code. This is true of remote target environments, like Fennec or |
|
385 * B2G. It is not the default case for desktop Firefox because we offer the |
|
386 * Browser Toolbox for chrome debugging there, which uses its own, separate |
|
387 * loader instance. |
|
388 * @see browser/devtools/framework/ToolboxProcess.jsm |
|
389 */ |
|
390 invisibleToDebugger: Services.appinfo.name !== "Firefox" |
|
391 }; |
|
392 |
|
393 // Export the standard instance of DevToolsLoader used by the tools. |
|
394 this.devtools = new DevToolsLoader(); |