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: // @see http://mxr.mozilla.org/mozilla-central/source/js/src/xpconnect/loader/mozJSComponentLoader.cpp michael@0: michael@0: 'use strict'; michael@0: michael@0: // IMPORTANT: Avoid adding any initialization tasks here, if you need to do michael@0: // something before add-on is loaded consider addon/runner module instead! michael@0: michael@0: const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu, michael@0: results: Cr, manager: Cm } = Components; michael@0: const ioService = Cc['@mozilla.org/network/io-service;1']. michael@0: getService(Ci.nsIIOService); michael@0: const resourceHandler = ioService.getProtocolHandler('resource'). michael@0: QueryInterface(Ci.nsIResProtocolHandler); michael@0: const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(); michael@0: const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1']. michael@0: getService(Ci.mozIJSSubScriptLoader); michael@0: const prefService = Cc['@mozilla.org/preferences-service;1']. michael@0: getService(Ci.nsIPrefService). michael@0: QueryInterface(Ci.nsIPrefBranch); michael@0: const appInfo = Cc["@mozilla.org/xre/app-info;1"]. michael@0: getService(Ci.nsIXULAppInfo); michael@0: const vc = Cc["@mozilla.org/xpcom/version-comparator;1"]. michael@0: getService(Ci.nsIVersionComparator); michael@0: michael@0: michael@0: const REASON = [ 'unknown', 'startup', 'shutdown', 'enable', 'disable', michael@0: 'install', 'uninstall', 'upgrade', 'downgrade' ]; michael@0: michael@0: const bind = Function.call.bind(Function.bind); michael@0: michael@0: let loader = null; michael@0: let unload = null; michael@0: let cuddlefishSandbox = null; michael@0: let nukeTimer = null; michael@0: michael@0: let resourceDomains = []; michael@0: function setResourceSubstitution(domain, uri) { michael@0: resourceDomains.push(domain); michael@0: resourceHandler.setSubstitution(domain, uri); michael@0: } michael@0: michael@0: // Utility function that synchronously reads local resource from the given michael@0: // `uri` and returns content string. michael@0: function readURI(uri) { michael@0: let ioservice = Cc['@mozilla.org/network/io-service;1']. michael@0: getService(Ci.nsIIOService); michael@0: let channel = ioservice.newChannel(uri, 'UTF-8', null); michael@0: let stream = channel.open(); michael@0: michael@0: let cstream = Cc['@mozilla.org/intl/converter-input-stream;1']. michael@0: createInstance(Ci.nsIConverterInputStream); michael@0: cstream.init(stream, 'UTF-8', 0, 0); michael@0: michael@0: let str = {}; michael@0: let data = ''; michael@0: let read = 0; michael@0: do { michael@0: read = cstream.readString(0xffffffff, str); michael@0: data += str.value; michael@0: } while (read != 0); michael@0: michael@0: cstream.close(); michael@0: michael@0: return data; michael@0: } michael@0: michael@0: // We don't do anything on install & uninstall yet, but in a future michael@0: // we should allow add-ons to cleanup after uninstall. michael@0: function install(data, reason) {} michael@0: function uninstall(data, reason) {} michael@0: michael@0: function startup(data, reasonCode) { michael@0: try { michael@0: let reason = REASON[reasonCode]; michael@0: // URI for the root of the XPI file. michael@0: // 'jar:' URI if the addon is packed, 'file:' URI otherwise. michael@0: // (Used by l10n module in order to fetch `locale` folder) michael@0: let rootURI = data.resourceURI.spec; michael@0: michael@0: // TODO: Maybe we should perform read harness-options.json asynchronously, michael@0: // since we can't do anything until 'sessionstore-windows-restored' anyway. michael@0: let options = JSON.parse(readURI(rootURI + './harness-options.json')); michael@0: michael@0: let id = options.jetpackID; michael@0: let name = options.name; michael@0: michael@0: // Clean the metadata michael@0: options.metadata[name]['permissions'] = options.metadata[name]['permissions'] || {}; michael@0: michael@0: // freeze the permissionss michael@0: Object.freeze(options.metadata[name]['permissions']); michael@0: // freeze the metadata michael@0: Object.freeze(options.metadata[name]); michael@0: michael@0: // Register a new resource 'domain' for this addon which is mapping to michael@0: // XPI's `resources` folder. michael@0: // Generate the domain name by using jetpack ID, which is the extension ID michael@0: // by stripping common characters that doesn't work as a domain name: michael@0: let uuidRe = michael@0: /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/; michael@0: michael@0: let domain = id. michael@0: toLowerCase(). michael@0: replace(/@/g, '-at-'). michael@0: replace(/\./g, '-dot-'). michael@0: replace(uuidRe, '$1'); michael@0: michael@0: let prefixURI = 'resource://' + domain + '/'; michael@0: let resourcesURI = ioService.newURI(rootURI + '/resources/', null, null); michael@0: setResourceSubstitution(domain, resourcesURI); michael@0: michael@0: // Create path to URLs mapping supported by loader. michael@0: let paths = { michael@0: // Relative modules resolve to add-on package lib michael@0: './': prefixURI + name + '/lib/', michael@0: './tests/': prefixURI + name + '/tests/', michael@0: '': 'resource://gre/modules/commonjs/' michael@0: }; michael@0: michael@0: // Maps addon lib and tests ressource folders for each package michael@0: paths = Object.keys(options.metadata).reduce(function(result, name) { michael@0: result[name + '/'] = prefixURI + name + '/lib/' michael@0: result[name + '/tests/'] = prefixURI + name + '/tests/' michael@0: return result; michael@0: }, paths); michael@0: michael@0: // We need to map tests folder when we run sdk tests whose package name michael@0: // is stripped michael@0: if (name == 'addon-sdk') michael@0: paths['tests/'] = prefixURI + name + '/tests/'; michael@0: michael@0: let useBundledSDK = options['force-use-bundled-sdk']; michael@0: if (!useBundledSDK) { michael@0: try { michael@0: useBundledSDK = prefService.getBoolPref("extensions.addon-sdk.useBundledSDK"); michael@0: } michael@0: catch (e) { michael@0: // Pref doesn't exist, allow using Firefox shipped SDK michael@0: } michael@0: } michael@0: michael@0: // Starting with Firefox 21.0a1, we start using modules shipped into firefox michael@0: // Still allow using modules from the xpi if the manifest tell us to do so. michael@0: // And only try to look for sdk modules in xpi if the xpi actually ship them michael@0: if (options['is-sdk-bundled'] && michael@0: (vc.compare(appInfo.version, '21.0a1') < 0 || useBundledSDK)) { michael@0: // Maps sdk module folders to their resource folder michael@0: paths[''] = prefixURI + 'addon-sdk/lib/'; michael@0: // test.js is usually found in root commonjs or SDK_ROOT/lib/ folder, michael@0: // so that it isn't shipped in the xpi. Keep a copy of it in sdk/ folder michael@0: // until we no longer support SDK modules in XPI: michael@0: paths['test'] = prefixURI + 'addon-sdk/lib/sdk/test.js'; michael@0: } michael@0: michael@0: // Retrieve list of module folder overloads based on preferences in order to michael@0: // eventually used a local modules instead of files shipped into Firefox. michael@0: let branch = prefService.getBranch('extensions.modules.' + id + '.path'); michael@0: paths = branch.getChildList('', {}).reduce(function (result, name) { michael@0: // Allows overloading of any sub folder by replacing . by / in pref name michael@0: let path = name.substr(1).split('.').join('/'); michael@0: // Only accept overloading folder by ensuring always ending with `/` michael@0: if (path) path += '/'; michael@0: let fileURI = branch.getCharPref(name); michael@0: michael@0: // On mobile, file URI has to end with a `/` otherwise, setSubstitution michael@0: // takes the parent folder instead. michael@0: if (fileURI[fileURI.length-1] !== '/') michael@0: fileURI += '/'; michael@0: michael@0: // Maps the given file:// URI to a resource:// in order to avoid various michael@0: // failure that happens with file:// URI and be close to production env michael@0: let resourcesURI = ioService.newURI(fileURI, null, null); michael@0: let resName = 'extensions.modules.' + domain + '.commonjs.path' + name; michael@0: setResourceSubstitution(resName, resourcesURI); michael@0: michael@0: result[path] = 'resource://' + resName + '/'; michael@0: return result; michael@0: }, paths); michael@0: michael@0: // Make version 2 of the manifest michael@0: let manifest = options.manifest; michael@0: michael@0: // Import `cuddlefish.js` module using a Sandbox and bootstrap loader. michael@0: let cuddlefishPath = 'loader/cuddlefish.js'; michael@0: let cuddlefishURI = 'resource://gre/modules/commonjs/sdk/' + cuddlefishPath; michael@0: if (paths['sdk/']) { // sdk folder has been overloaded michael@0: // (from pref, or cuddlefish is still in the xpi) michael@0: cuddlefishURI = paths['sdk/'] + cuddlefishPath; michael@0: } michael@0: else if (paths['']) { // root modules folder has been overloaded michael@0: cuddlefishURI = paths[''] + 'sdk/' + cuddlefishPath; michael@0: } michael@0: michael@0: cuddlefishSandbox = loadSandbox(cuddlefishURI); michael@0: let cuddlefish = cuddlefishSandbox.exports; michael@0: michael@0: // Normalize `options.mainPath` so that it looks like one that will come michael@0: // in a new version of linker. michael@0: let main = options.mainPath; michael@0: michael@0: unload = cuddlefish.unload; michael@0: loader = cuddlefish.Loader({ michael@0: paths: paths, michael@0: // modules manifest. michael@0: manifest: manifest, michael@0: michael@0: // Add-on ID used by different APIs as a unique identifier. michael@0: id: id, michael@0: // Add-on name. michael@0: name: name, michael@0: // Add-on version. michael@0: version: options.metadata[name].version, michael@0: // Add-on package descriptor. michael@0: metadata: options.metadata[name], michael@0: // Add-on load reason. michael@0: loadReason: reason, michael@0: michael@0: prefixURI: prefixURI, michael@0: // Add-on URI. michael@0: rootURI: rootURI, michael@0: // options used by system module. michael@0: // File to write 'OK' or 'FAIL' (exit code emulation). michael@0: resultFile: options.resultFile, michael@0: // Arguments passed as --static-args michael@0: staticArgs: options.staticArgs, michael@0: // Add-on preferences branch name michael@0: preferencesBranch: options.preferencesBranch, michael@0: michael@0: // Arguments related to test runner. michael@0: modules: { michael@0: '@test/options': { michael@0: allTestModules: options.allTestModules, michael@0: iterations: options.iterations, michael@0: filter: options.filter, michael@0: profileMemory: options.profileMemory, michael@0: stopOnError: options.stopOnError, michael@0: verbose: options.verbose, michael@0: parseable: options.parseable, michael@0: checkMemory: options.check_memory, michael@0: } michael@0: } michael@0: }); michael@0: michael@0: let module = cuddlefish.Module('sdk/loader/cuddlefish', cuddlefishURI); michael@0: let require = cuddlefish.Require(loader, module); michael@0: michael@0: require('sdk/addon/runner').startup(reason, { michael@0: loader: loader, michael@0: main: main, michael@0: prefsURI: rootURI + 'defaults/preferences/prefs.js' michael@0: }); michael@0: } catch (error) { michael@0: dump('Bootstrap error: ' + michael@0: (error.message ? error.message : String(error)) + '\n' + michael@0: (error.stack || error.fileName + ': ' + error.lineNumber) + '\n'); michael@0: throw error; michael@0: } michael@0: }; michael@0: michael@0: function loadSandbox(uri) { michael@0: let proto = { michael@0: sandboxPrototype: { michael@0: loadSandbox: loadSandbox, michael@0: ChromeWorker: ChromeWorker michael@0: } michael@0: }; michael@0: let sandbox = Cu.Sandbox(systemPrincipal, proto); michael@0: // Create a fake commonjs environnement just to enable loading loader.js michael@0: // correctly michael@0: sandbox.exports = {}; michael@0: sandbox.module = { uri: uri, exports: sandbox.exports }; michael@0: sandbox.require = function (id) { michael@0: if (id !== "chrome") michael@0: throw new Error("Bootstrap sandbox `require` method isn't implemented."); michael@0: michael@0: return Object.freeze({ Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm, michael@0: CC: bind(CC, Components), components: Components, michael@0: ChromeWorker: ChromeWorker }); michael@0: }; michael@0: scriptLoader.loadSubScript(uri, sandbox, 'UTF-8'); michael@0: return sandbox; michael@0: } michael@0: michael@0: function unloadSandbox(sandbox) { michael@0: if ("nukeSandbox" in Cu) michael@0: Cu.nukeSandbox(sandbox); michael@0: } michael@0: michael@0: function setTimeout(callback, delay) { michael@0: let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: timer.initWithCallback({ notify: callback }, delay, michael@0: Ci.nsITimer.TYPE_ONE_SHOT); michael@0: return timer; michael@0: } michael@0: michael@0: function shutdown(data, reasonCode) { michael@0: let reason = REASON[reasonCode]; michael@0: if (loader) { michael@0: unload(loader, reason); michael@0: unload = null; michael@0: michael@0: // Don't waste time cleaning up if the application is shutting down michael@0: if (reason != "shutdown") { michael@0: // Avoid leaking all modules when something goes wrong with one particular michael@0: // module. Do not clean it up immediatly in order to allow executing some michael@0: // actions on addon disabling. michael@0: // We need to keep a reference to the timer, otherwise it is collected michael@0: // and won't ever fire. michael@0: nukeTimer = setTimeout(nukeModules, 1000); michael@0: michael@0: // Bug 944951 - bootstrap.js must remove the added resource: URIs on unload michael@0: resourceDomains.forEach(domain => { michael@0: resourceHandler.setSubstitution(domain, null); michael@0: }) michael@0: } michael@0: } michael@0: }; michael@0: michael@0: function nukeModules() { michael@0: nukeTimer = null; michael@0: // module objects store `exports` which comes from sandboxes michael@0: // We should avoid keeping link to these object to avoid leaking sandboxes michael@0: for (let key in loader.modules) { michael@0: delete loader.modules[key]; michael@0: } michael@0: // Direct links to sandboxes should be removed too michael@0: for (let key in loader.sandboxes) { michael@0: let sandbox = loader.sandboxes[key]; michael@0: delete loader.sandboxes[key]; michael@0: // Bug 775067: From FF17 we can kill all CCW from a given sandbox michael@0: unloadSandbox(sandbox); michael@0: } michael@0: loader = null; michael@0: michael@0: // both `toolkit/loader` and `system/xul-app` are loaded as JSM's via michael@0: // `cuddlefish.js`, and needs to be unloaded to avoid memory leaks, when michael@0: // the addon is unload. michael@0: michael@0: unloadSandbox(cuddlefishSandbox.loaderSandbox); michael@0: unloadSandbox(cuddlefishSandbox.xulappSandbox); michael@0: michael@0: // Bug 764840: We need to unload cuddlefish otherwise it will stay alive michael@0: // and keep a reference to this compartment. michael@0: unloadSandbox(cuddlefishSandbox); michael@0: cuddlefishSandbox = null; michael@0: }