/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Weave. * * The Initial Developer of the Original Code is Mozilla. * Portions created by the Initial Developer are Copyright (C) 2008 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Dan Mills * Atul Varma * Drew Willcoxon * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ // This file contains an XPCOM component which "bootstraps" a Jetpack // program. // // The main entry point, `NSGetModule()`, is data-driven, and obtains // a lot of its configuration information from a JSON file // called `harness-options.json` in the root directory of the extension // or application it's a part of. // // `NSGetModule()` then uses this configuration information to // dynamically create an XPCOM component called a "Harness Service", // which is responsible for setting up and shutting down the Jetpack // program's CommonJS environment. It's also the main mechanism through // which other parts of the application can communicate with the Jetpack // program. // // If we're on Gecko 1.9.3, which supports rebootless extensions, the // bootstrap.js file actually evaluates this file and calls parts of // it automatically. // // It should be noted that a lot of what's done by the Harness Service is // very similar to what's normally done by a `chrome.manifest` file: the // difference here is that everything the Harness Service does is // undoable during the lifetime of the application. This is the // foundation of what makes it possible for Jetpack-based extensions // to be installed and uninstalled without needing to reboot the // application being extended. "use strict"; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); const obSvc = Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService); const ioSvc = Cc["@mozilla.org/network/io-service;1"] .getService(Ci.nsIIOService); const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; const THUNDERBIRD_ID = "{3550f703-e582-4d05-9a08-453d09bdfdc6}"; const FENNEC_ID = "{a23983c0-fd0e-11dc-95ff-0800200c9a66}"; // This function builds and returns a Harness Service XPCOM component. // // Parameters: // // rootFileSpec - nsILocalFile corresponding to root of extension // (required). // // dump - function to output string to console (required). // // logError - function to log an exception (required). // // onQuit - function called when the app quits (required). // // options - JSON configuration information passed in from the // environment (required). function buildHarnessService(rootFileSpec, dump, logError, onQuit, options) { if (arguments.length == 1) { ({dump, logError, onQuit, options}) = getDefaults(rootFileSpec); } // The loader for securable modules, typically a Cuddlefish loader. var loader; // Singleton Harness Service. var harnessService; // Whether we've initialized or not yet. var isStarted; // Whether we've been asked to quit or not yet. var isQuitting; // The Jetpack program's main module. var program; var ioService = Cc["@mozilla.org/network/io-service;1"] .getService(Ci.nsIIOService); var resProt = ioService.getProtocolHandler("resource") .QueryInterface(Ci.nsIResProtocolHandler); function quit(status) { if (status === undefined) status = "OK"; if (status != "OK" && status != "FAIL") { dump("Warning: quit() expected 'OK' or 'FAIL' as an " + "argument, but got '" + status + "' instead."); status = "FAIL"; } if (isQuitting) return; isQuitting = true; if (harnessService) harnessService.unload(); onQuit(status); } function logErrorAndBail(e) { logError(e); quit("FAIL"); } function ensureIsDir(dir) { if (!(dir.exists() && dir.isDirectory)) throw new Error("directory not found: " + dir.path); } function getDir(path) { var dir = Cc['@mozilla.org/file/local;1'] .createInstance(Ci.nsILocalFile); dir.initWithPath(path); ensureIsDir(dir); return dir; } function buildLoader() { // TODO: This variable doesn't seem to be used, we should // be able to remove it. var compMgr = Components.manager; compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar); for (let name in options.resources) { var path = options.resources[name]; var dir; if (typeof(path) == "string") dir = getDir(path); else { dir = rootFileSpec.clone(); path.forEach(function(part) { dir.append(part); }); ensureIsDir(dir); } var dirUri = ioService.newFileURI(dir); resProt.setSubstitution(name, dirUri); } var jsm = {}; Cu.import(options.loader, jsm); var packaging = new Packaging(); var loader = new jsm.Loader({rootPaths: options.rootPaths.slice(), print: dump, packaging: packaging, metadata: options.metadata, uriPrefix: options.uriPrefix, name: options.name, globals: { packaging: packaging } }); packaging.__setLoader(loader); return loader; } // This will be exposed as the 'packaging' global to all // modules loaded within our loader. function Packaging() { this.__packages = options.manifest; } Packaging.prototype = { __setLoader: function setLoader(loader) { this.__loader = loader; }, get root() { return rootFileSpec.clone(); }, get harnessService() { return harnessService; }, get buildHarnessService() { return buildHarnessService; }, get options() { return options; }, enableE10s: options.enable_e10s, jetpackID: options.jetpackID, uriPrefix: options.uriPrefix, bundleID: options.bundleID, getModuleInfo: function getModuleInfo(path) { return this.__packages[path]; }, createLoader: function createLoader() { return buildLoader(); } }; // Singleton XPCOM component that is responsible for instantiating // a Cuddlefish loader and running the main program, if any. function HarnessService() { this.wrappedJSObject = this; } HarnessService.prototype = { get classDescription() { // This needs to be unique, lest we regress bug 554489. return "Harness Service for " + options.bootstrap.contractID; }, get contractID() { return options.bootstrap.contractID; }, get classID() { return Components.ID(options.bootstrap.classID); }, _xpcom_categories: [{ category: "profile-after-change" }], _xpcom_factory: { get singleton() { return harnessService; }, createInstance: function(outer, iid) { if (outer) throw Cr.NS_ERROR_NO_AGGREGATION; if (!harnessService) harnessService = new HarnessService(); return harnessService.QueryInterface(iid); } }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), get loader() { if (!loader) loader = buildLoader(); return loader; }, get options() { return options; }, load: function Harness_load(reason) { if (isStarted) return; isStarted = true; obSvc.addObserver(this, "quit-application-granted", true); if (options.main) { try { if (reason) options.loadReason = reason; program = this.loader.require(options.main); if ('main' in program) program.main(options, {quit: quit, print: dump}); // Send application readiness notification const APP_READY_TOPIC = options.jetpackID + "_APPLICATION_READY"; obSvc.notifyObservers(null, APP_READY_TOPIC, null); } catch (e) { this.loader.console.exception(e); quit("FAIL"); } } }, unload: function Harness_unload(reason) { if (!isStarted) return; isStarted = false; harnessService = null; obSvc.removeObserver(this, "quit-application-granted"); lifeCycleObserver192.unload(); // Notify the program of unload. if (program) { if (typeof(program.onUnload) === "function") { try { program.onUnload(reason); } catch (err) { if (loader) loader.console.exception(err); } } program = null; } // Notify the loader of unload. if (loader) { loader.unload(reason); loader = null; } for (let name in options.resources) resProt.setSubstitution(name, null); }, observe: function Harness_observe(subject, topic, data) { try { switch (topic) { case "profile-after-change": var appInfo = Cc["@mozilla.org/xre/app-info;1"] .getService(Ci.nsIXULAppInfo); switch (appInfo.ID) { case THUNDERBIRD_ID: case FENNEC_ID: obSvc.addObserver(this, "xul-window-visible", true); break; case FIREFOX_ID: obSvc.addObserver(this, "sessionstore-windows-restored", true); break; default: obSvc.addObserver(this, "final-ui-startup", true); break; } lifeCycleObserver192.init(options.bundleID, logError); break; case "final-ui-startup": // XULRunner case "sessionstore-windows-restored": // Firefox case "xul-window-visible": // Thunderbird, Fennec obSvc.removeObserver(this, topic); this.load(lifeCycleObserver192.loadReason || "startup"); break; case "quit-application-granted": this.unload(lifeCycleObserver192.unloadReason || "shutdown"); quit("OK"); break; } } catch (e) { logErrorAndBail(e); } } }; var factory = HarnessService.prototype._xpcom_factory; if (!factory.wrappedJSObject) factory.wrappedJSObject = factory; return HarnessService; } // This is an error logger of last resort; if we're here, then // we weren't able to initialize Cuddlefish and display a nice // traceback through it. function defaultLogError(e, print) { if (!print) print = dump; var level = "error"; print(e + " (" + e.fileName + ":" + e.lineNumber + ")\n", level); if (e.stack) print("stack:\n" + e.stack + "\n", level); } // Builds an onQuit() function that writes a result file if necessary // and does some other extra things to enhance developer ergonomics. function buildDevQuit(options, dump) { // Absolute path to a file that we put our result code in. Ordinarily // we'd just exit the process with a zero or nonzero return code, but // there doesn't appear to be a way to do this in XULRunner. var resultFile = options.resultFile; // Whether we've written resultFile or not. var fileWritten = false; function attemptQuit() { var appStartup = Cc['@mozilla.org/toolkit/app-startup;1']. getService(Ci.nsIAppStartup); appStartup.quit(Ci.nsIAppStartup.eAttemptQuit); } return function onQuit(result) { dump(result + "\n"); function writeResult() { if (!fileWritten) try { var file = Cc["@mozilla.org/file/local;1"] .createInstance(Ci.nsILocalFile); file.initWithPath(resultFile); var foStream = Cc["@mozilla.org/network/file-output-stream;1"] .createInstance(Ci.nsIFileOutputStream); foStream.init(file, -1, -1, 0); foStream.write(result, result.length); foStream.close(); fileWritten = true; } catch (e) { dump(e + "\n"); } } writeResult(); attemptQuit(); }; } function buildForsakenConsoleDump(dump) { var buffer = ""; var cService = Cc['@mozilla.org/consoleservice;1'].getService() .QueryInterface(Ci.nsIConsoleService); function stringify(arg) { try { return String(arg); } catch(ex) { return ""; } } return function forsakenConsoleDump(msg, level) { msg = stringify(msg); if (msg.indexOf('\n') >= 0) { var str = buffer + msg; if (level === "error") { var err = Cc["@mozilla.org/scripterror;1"] .createInstance(Ci.nsIScriptError); str = str.replace(/^error: /, ""); err.init(str, null, null, 0, 0, 0, "Add-on SDK"); cService.logMessage(err); } else cService.logStringMessage(str); buffer = ""; } else { buffer += msg; } }; } function getDefaults(rootFileSpec) { // Default options to pass back. var options; try { var environ = Cc["@mozilla.org/process/environment;1"] .getService(Ci.nsIEnvironment); var jsonData; var optionsFile = rootFileSpec.clone(); optionsFile.append('harness-options.json'); if (optionsFile.exists()) { var fiStream = Cc['@mozilla.org/network/file-input-stream;1'] .createInstance(Ci.nsIFileInputStream); var siStream = Cc['@mozilla.org/scriptableinputstream;1'] .createInstance(Ci.nsIScriptableInputStream); fiStream.init(optionsFile, 1, 0, false); siStream.init(fiStream); var data = new String(); data += siStream.read(-1); siStream.close(); fiStream.close(); jsonData = data; } else { throw new Error("harness-options.json file must exist."); } options = JSON.parse(jsonData); } catch (e) { defaultLogError(e); throw e; } var onQuit = function() {}; var doDump = buildForsakenConsoleDump(dump); if ('resultFile' in options) onQuit = buildDevQuit(options, print); var logFile; var logStream; if ('logFile' in options) { logFile = Cc["@mozilla.org/file/local;1"] .createInstance(Ci.nsILocalFile); logFile.initWithPath(options.logFile); logStream = Cc["@mozilla.org/network/file-output-stream;1"] .createInstance(Ci.nsIFileOutputStream); logStream.init(logFile, 26 /* PR_WRONLY | PR_APPEND | PR_CREATE_FILE */, -1 , 0); } function print(msg, level) { doDump(msg, level); if (logStream && typeof(msg) == "string") { logStream.write(msg, msg.length); logStream.flush(); } } function logError(e) { defaultLogError(e, print); } return {options: options, onQuit: onQuit, dump: print, logError: logError}; } // Gecko 2, entry point for non-bootstrapped extensions (which register this // component via chrome.manifest.) // FIXME: no install/uninstall notifications on 2.0 for non-bootstrapped addons function NSGetFactory(cid) { try { if (!NSGetFactory.fn) { var rootFileSpec = __LOCATION__.parent.parent; var HarnessService = buildHarnessService(rootFileSpec); NSGetFactory.fn = XPCOMUtils.generateNSGetFactory([HarnessService]); } } catch(e) { Components.utils.reportError(e); dump(e); throw e; } return NSGetFactory.fn(cid); } // Everything below is only used on Gecko 1.9.2 or below. function NSGetModule(compMgr, fileSpec) { var rootFileSpec = fileSpec.parent.parent; var HarnessService = buildHarnessService(rootFileSpec); return XPCOMUtils.generateModule([HarnessService]); } // Program life-cycle events originate in bootstrap.js on 1.9.3. But 1.9.2 // doesn't use bootstrap.js, so we need to do a little extra work there to // determine the reasons for app startup and shutdown. That's what this // singleton is for. On 1.9.3 all methods are no-ops. var lifeCycleObserver192 = { get loadReason() { if (this._inited) { // If you change these names, change them in bootstrap.js too. if (this._addonIsNew) return "install"; return "startup"; } return undefined; }, get unloadReason() { if (this._inited) { // If you change these names, change them in bootstrap.js too. switch (this._emState) { case "item-uninstalled": return "uninstall"; case "item-disabled": return "disable"; } return "shutdown"; } return undefined; }, // This must be called first to initialize the singleton. It must be called // on profile-after-change. init: function lifeCycleObserver192_init(bundleID, logError) { // This component is present in 1.9.2 but not 2.0. if ("@mozilla.org/extensions/manager;1" in Cc && !this._inited) { obSvc.addObserver(this, "em-action-requested", true); this._bundleID = bundleID; this._logError = logError; this._inited = true; try { // This throws if the pref doesn't exist, which is the case when no // new add-ons were installed. var addonIdStr = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch). getCharPref("extensions.newAddons"); } catch (err) {} if (addonIdStr) { var addonIds = addonIdStr.split(","); this._addonIsNew = addonIds.indexOf(this._bundleID) >= 0; } } }, unload: function lifeCycleObserver192_unload() { if (this._inited && !this._unloaded) { obSvc.removeObserver(this, "em-action-requested"); delete this._logError; this._unloaded = true; } }, observe: function lifeCycleObserver192_observe(subj, topic, data) { try { if (topic === "em-action-requested") { if (subj instanceof Ci.nsIUpdateItem && subj.id === this._bundleID) this._emState = data; } } catch (err) { this._logError(err); } }, QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver, Ci.nsISupportsWeakReference, ]) };