|
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 // @see http://mxr.mozilla.org/mozilla-central/source/js/src/xpconnect/loader/mozJSComponentLoader.cpp |
|
6 |
|
7 'use strict'; |
|
8 |
|
9 // IMPORTANT: Avoid adding any initialization tasks here, if you need to do |
|
10 // something before add-on is loaded consider addon/runner module instead! |
|
11 |
|
12 const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu, |
|
13 results: Cr, manager: Cm } = Components; |
|
14 const ioService = Cc['@mozilla.org/network/io-service;1']. |
|
15 getService(Ci.nsIIOService); |
|
16 const resourceHandler = ioService.getProtocolHandler('resource'). |
|
17 QueryInterface(Ci.nsIResProtocolHandler); |
|
18 const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(); |
|
19 const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1']. |
|
20 getService(Ci.mozIJSSubScriptLoader); |
|
21 const prefService = Cc['@mozilla.org/preferences-service;1']. |
|
22 getService(Ci.nsIPrefService). |
|
23 QueryInterface(Ci.nsIPrefBranch); |
|
24 const appInfo = Cc["@mozilla.org/xre/app-info;1"]. |
|
25 getService(Ci.nsIXULAppInfo); |
|
26 const vc = Cc["@mozilla.org/xpcom/version-comparator;1"]. |
|
27 getService(Ci.nsIVersionComparator); |
|
28 |
|
29 |
|
30 const REASON = [ 'unknown', 'startup', 'shutdown', 'enable', 'disable', |
|
31 'install', 'uninstall', 'upgrade', 'downgrade' ]; |
|
32 |
|
33 const bind = Function.call.bind(Function.bind); |
|
34 |
|
35 let loader = null; |
|
36 let unload = null; |
|
37 let cuddlefishSandbox = null; |
|
38 let nukeTimer = null; |
|
39 |
|
40 let resourceDomains = []; |
|
41 function setResourceSubstitution(domain, uri) { |
|
42 resourceDomains.push(domain); |
|
43 resourceHandler.setSubstitution(domain, uri); |
|
44 } |
|
45 |
|
46 // Utility function that synchronously reads local resource from the given |
|
47 // `uri` and returns content string. |
|
48 function readURI(uri) { |
|
49 let ioservice = Cc['@mozilla.org/network/io-service;1']. |
|
50 getService(Ci.nsIIOService); |
|
51 let channel = ioservice.newChannel(uri, 'UTF-8', null); |
|
52 let stream = channel.open(); |
|
53 |
|
54 let cstream = Cc['@mozilla.org/intl/converter-input-stream;1']. |
|
55 createInstance(Ci.nsIConverterInputStream); |
|
56 cstream.init(stream, 'UTF-8', 0, 0); |
|
57 |
|
58 let str = {}; |
|
59 let data = ''; |
|
60 let read = 0; |
|
61 do { |
|
62 read = cstream.readString(0xffffffff, str); |
|
63 data += str.value; |
|
64 } while (read != 0); |
|
65 |
|
66 cstream.close(); |
|
67 |
|
68 return data; |
|
69 } |
|
70 |
|
71 // We don't do anything on install & uninstall yet, but in a future |
|
72 // we should allow add-ons to cleanup after uninstall. |
|
73 function install(data, reason) {} |
|
74 function uninstall(data, reason) {} |
|
75 |
|
76 function startup(data, reasonCode) { |
|
77 try { |
|
78 let reason = REASON[reasonCode]; |
|
79 // URI for the root of the XPI file. |
|
80 // 'jar:' URI if the addon is packed, 'file:' URI otherwise. |
|
81 // (Used by l10n module in order to fetch `locale` folder) |
|
82 let rootURI = data.resourceURI.spec; |
|
83 |
|
84 // TODO: Maybe we should perform read harness-options.json asynchronously, |
|
85 // since we can't do anything until 'sessionstore-windows-restored' anyway. |
|
86 let options = JSON.parse(readURI(rootURI + './harness-options.json')); |
|
87 |
|
88 let id = options.jetpackID; |
|
89 let name = options.name; |
|
90 |
|
91 // Clean the metadata |
|
92 options.metadata[name]['permissions'] = options.metadata[name]['permissions'] || {}; |
|
93 |
|
94 // freeze the permissionss |
|
95 Object.freeze(options.metadata[name]['permissions']); |
|
96 // freeze the metadata |
|
97 Object.freeze(options.metadata[name]); |
|
98 |
|
99 // Register a new resource 'domain' for this addon which is mapping to |
|
100 // XPI's `resources` folder. |
|
101 // Generate the domain name by using jetpack ID, which is the extension ID |
|
102 // by stripping common characters that doesn't work as a domain name: |
|
103 let uuidRe = |
|
104 /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/; |
|
105 |
|
106 let domain = id. |
|
107 toLowerCase(). |
|
108 replace(/@/g, '-at-'). |
|
109 replace(/\./g, '-dot-'). |
|
110 replace(uuidRe, '$1'); |
|
111 |
|
112 let prefixURI = 'resource://' + domain + '/'; |
|
113 let resourcesURI = ioService.newURI(rootURI + '/resources/', null, null); |
|
114 setResourceSubstitution(domain, resourcesURI); |
|
115 |
|
116 // Create path to URLs mapping supported by loader. |
|
117 let paths = { |
|
118 // Relative modules resolve to add-on package lib |
|
119 './': prefixURI + name + '/lib/', |
|
120 './tests/': prefixURI + name + '/tests/', |
|
121 '': 'resource://gre/modules/commonjs/' |
|
122 }; |
|
123 |
|
124 // Maps addon lib and tests ressource folders for each package |
|
125 paths = Object.keys(options.metadata).reduce(function(result, name) { |
|
126 result[name + '/'] = prefixURI + name + '/lib/' |
|
127 result[name + '/tests/'] = prefixURI + name + '/tests/' |
|
128 return result; |
|
129 }, paths); |
|
130 |
|
131 // We need to map tests folder when we run sdk tests whose package name |
|
132 // is stripped |
|
133 if (name == 'addon-sdk') |
|
134 paths['tests/'] = prefixURI + name + '/tests/'; |
|
135 |
|
136 let useBundledSDK = options['force-use-bundled-sdk']; |
|
137 if (!useBundledSDK) { |
|
138 try { |
|
139 useBundledSDK = prefService.getBoolPref("extensions.addon-sdk.useBundledSDK"); |
|
140 } |
|
141 catch (e) { |
|
142 // Pref doesn't exist, allow using Firefox shipped SDK |
|
143 } |
|
144 } |
|
145 |
|
146 // Starting with Firefox 21.0a1, we start using modules shipped into firefox |
|
147 // Still allow using modules from the xpi if the manifest tell us to do so. |
|
148 // And only try to look for sdk modules in xpi if the xpi actually ship them |
|
149 if (options['is-sdk-bundled'] && |
|
150 (vc.compare(appInfo.version, '21.0a1') < 0 || useBundledSDK)) { |
|
151 // Maps sdk module folders to their resource folder |
|
152 paths[''] = prefixURI + 'addon-sdk/lib/'; |
|
153 // test.js is usually found in root commonjs or SDK_ROOT/lib/ folder, |
|
154 // so that it isn't shipped in the xpi. Keep a copy of it in sdk/ folder |
|
155 // until we no longer support SDK modules in XPI: |
|
156 paths['test'] = prefixURI + 'addon-sdk/lib/sdk/test.js'; |
|
157 } |
|
158 |
|
159 // Retrieve list of module folder overloads based on preferences in order to |
|
160 // eventually used a local modules instead of files shipped into Firefox. |
|
161 let branch = prefService.getBranch('extensions.modules.' + id + '.path'); |
|
162 paths = branch.getChildList('', {}).reduce(function (result, name) { |
|
163 // Allows overloading of any sub folder by replacing . by / in pref name |
|
164 let path = name.substr(1).split('.').join('/'); |
|
165 // Only accept overloading folder by ensuring always ending with `/` |
|
166 if (path) path += '/'; |
|
167 let fileURI = branch.getCharPref(name); |
|
168 |
|
169 // On mobile, file URI has to end with a `/` otherwise, setSubstitution |
|
170 // takes the parent folder instead. |
|
171 if (fileURI[fileURI.length-1] !== '/') |
|
172 fileURI += '/'; |
|
173 |
|
174 // Maps the given file:// URI to a resource:// in order to avoid various |
|
175 // failure that happens with file:// URI and be close to production env |
|
176 let resourcesURI = ioService.newURI(fileURI, null, null); |
|
177 let resName = 'extensions.modules.' + domain + '.commonjs.path' + name; |
|
178 setResourceSubstitution(resName, resourcesURI); |
|
179 |
|
180 result[path] = 'resource://' + resName + '/'; |
|
181 return result; |
|
182 }, paths); |
|
183 |
|
184 // Make version 2 of the manifest |
|
185 let manifest = options.manifest; |
|
186 |
|
187 // Import `cuddlefish.js` module using a Sandbox and bootstrap loader. |
|
188 let cuddlefishPath = 'loader/cuddlefish.js'; |
|
189 let cuddlefishURI = 'resource://gre/modules/commonjs/sdk/' + cuddlefishPath; |
|
190 if (paths['sdk/']) { // sdk folder has been overloaded |
|
191 // (from pref, or cuddlefish is still in the xpi) |
|
192 cuddlefishURI = paths['sdk/'] + cuddlefishPath; |
|
193 } |
|
194 else if (paths['']) { // root modules folder has been overloaded |
|
195 cuddlefishURI = paths[''] + 'sdk/' + cuddlefishPath; |
|
196 } |
|
197 |
|
198 cuddlefishSandbox = loadSandbox(cuddlefishURI); |
|
199 let cuddlefish = cuddlefishSandbox.exports; |
|
200 |
|
201 // Normalize `options.mainPath` so that it looks like one that will come |
|
202 // in a new version of linker. |
|
203 let main = options.mainPath; |
|
204 |
|
205 unload = cuddlefish.unload; |
|
206 loader = cuddlefish.Loader({ |
|
207 paths: paths, |
|
208 // modules manifest. |
|
209 manifest: manifest, |
|
210 |
|
211 // Add-on ID used by different APIs as a unique identifier. |
|
212 id: id, |
|
213 // Add-on name. |
|
214 name: name, |
|
215 // Add-on version. |
|
216 version: options.metadata[name].version, |
|
217 // Add-on package descriptor. |
|
218 metadata: options.metadata[name], |
|
219 // Add-on load reason. |
|
220 loadReason: reason, |
|
221 |
|
222 prefixURI: prefixURI, |
|
223 // Add-on URI. |
|
224 rootURI: rootURI, |
|
225 // options used by system module. |
|
226 // File to write 'OK' or 'FAIL' (exit code emulation). |
|
227 resultFile: options.resultFile, |
|
228 // Arguments passed as --static-args |
|
229 staticArgs: options.staticArgs, |
|
230 // Add-on preferences branch name |
|
231 preferencesBranch: options.preferencesBranch, |
|
232 |
|
233 // Arguments related to test runner. |
|
234 modules: { |
|
235 '@test/options': { |
|
236 allTestModules: options.allTestModules, |
|
237 iterations: options.iterations, |
|
238 filter: options.filter, |
|
239 profileMemory: options.profileMemory, |
|
240 stopOnError: options.stopOnError, |
|
241 verbose: options.verbose, |
|
242 parseable: options.parseable, |
|
243 checkMemory: options.check_memory, |
|
244 } |
|
245 } |
|
246 }); |
|
247 |
|
248 let module = cuddlefish.Module('sdk/loader/cuddlefish', cuddlefishURI); |
|
249 let require = cuddlefish.Require(loader, module); |
|
250 |
|
251 require('sdk/addon/runner').startup(reason, { |
|
252 loader: loader, |
|
253 main: main, |
|
254 prefsURI: rootURI + 'defaults/preferences/prefs.js' |
|
255 }); |
|
256 } catch (error) { |
|
257 dump('Bootstrap error: ' + |
|
258 (error.message ? error.message : String(error)) + '\n' + |
|
259 (error.stack || error.fileName + ': ' + error.lineNumber) + '\n'); |
|
260 throw error; |
|
261 } |
|
262 }; |
|
263 |
|
264 function loadSandbox(uri) { |
|
265 let proto = { |
|
266 sandboxPrototype: { |
|
267 loadSandbox: loadSandbox, |
|
268 ChromeWorker: ChromeWorker |
|
269 } |
|
270 }; |
|
271 let sandbox = Cu.Sandbox(systemPrincipal, proto); |
|
272 // Create a fake commonjs environnement just to enable loading loader.js |
|
273 // correctly |
|
274 sandbox.exports = {}; |
|
275 sandbox.module = { uri: uri, exports: sandbox.exports }; |
|
276 sandbox.require = function (id) { |
|
277 if (id !== "chrome") |
|
278 throw new Error("Bootstrap sandbox `require` method isn't implemented."); |
|
279 |
|
280 return Object.freeze({ Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm, |
|
281 CC: bind(CC, Components), components: Components, |
|
282 ChromeWorker: ChromeWorker }); |
|
283 }; |
|
284 scriptLoader.loadSubScript(uri, sandbox, 'UTF-8'); |
|
285 return sandbox; |
|
286 } |
|
287 |
|
288 function unloadSandbox(sandbox) { |
|
289 if ("nukeSandbox" in Cu) |
|
290 Cu.nukeSandbox(sandbox); |
|
291 } |
|
292 |
|
293 function setTimeout(callback, delay) { |
|
294 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
|
295 timer.initWithCallback({ notify: callback }, delay, |
|
296 Ci.nsITimer.TYPE_ONE_SHOT); |
|
297 return timer; |
|
298 } |
|
299 |
|
300 function shutdown(data, reasonCode) { |
|
301 let reason = REASON[reasonCode]; |
|
302 if (loader) { |
|
303 unload(loader, reason); |
|
304 unload = null; |
|
305 |
|
306 // Don't waste time cleaning up if the application is shutting down |
|
307 if (reason != "shutdown") { |
|
308 // Avoid leaking all modules when something goes wrong with one particular |
|
309 // module. Do not clean it up immediatly in order to allow executing some |
|
310 // actions on addon disabling. |
|
311 // We need to keep a reference to the timer, otherwise it is collected |
|
312 // and won't ever fire. |
|
313 nukeTimer = setTimeout(nukeModules, 1000); |
|
314 |
|
315 // Bug 944951 - bootstrap.js must remove the added resource: URIs on unload |
|
316 resourceDomains.forEach(domain => { |
|
317 resourceHandler.setSubstitution(domain, null); |
|
318 }) |
|
319 } |
|
320 } |
|
321 }; |
|
322 |
|
323 function nukeModules() { |
|
324 nukeTimer = null; |
|
325 // module objects store `exports` which comes from sandboxes |
|
326 // We should avoid keeping link to these object to avoid leaking sandboxes |
|
327 for (let key in loader.modules) { |
|
328 delete loader.modules[key]; |
|
329 } |
|
330 // Direct links to sandboxes should be removed too |
|
331 for (let key in loader.sandboxes) { |
|
332 let sandbox = loader.sandboxes[key]; |
|
333 delete loader.sandboxes[key]; |
|
334 // Bug 775067: From FF17 we can kill all CCW from a given sandbox |
|
335 unloadSandbox(sandbox); |
|
336 } |
|
337 loader = null; |
|
338 |
|
339 // both `toolkit/loader` and `system/xul-app` are loaded as JSM's via |
|
340 // `cuddlefish.js`, and needs to be unloaded to avoid memory leaks, when |
|
341 // the addon is unload. |
|
342 |
|
343 unloadSandbox(cuddlefishSandbox.loaderSandbox); |
|
344 unloadSandbox(cuddlefishSandbox.xulappSandbox); |
|
345 |
|
346 // Bug 764840: We need to unload cuddlefish otherwise it will stay alive |
|
347 // and keep a reference to this compartment. |
|
348 unloadSandbox(cuddlefishSandbox); |
|
349 cuddlefishSandbox = null; |
|
350 } |