]> git.donarmstrong.com Git - dactyl.git/blob - common/bootstrap.js
Import r6976 from upstream hg supporting Firefox up to 25.*
[dactyl.git] / common / bootstrap.js
1 // Copyright (c) 2010-2011 by Kris Maglione <maglione.k@gmail.com>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 //
6 // See https://wiki.mozilla.org/Extension_Manager:Bootstrapped_Extensions
7 // for details.
8 "use strict";
9
10 const global = this;
11
12 var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
13
14 function module(uri) Cu.import(uri, {});
15
16 const DEBUG = true;
17
18 __defineGetter__("BOOTSTRAP", () => "resource://" + moduleName + "/bootstrap.jsm");
19
20 var { AddonManager } = module("resource://gre/modules/AddonManager.jsm");
21 var { XPCOMUtils }   = module("resource://gre/modules/XPCOMUtils.jsm");
22 var { Services }     = module("resource://gre/modules/Services.jsm");
23
24 const resourceProto = Services.io.getProtocolHandler("resource")
25                               .QueryInterface(Ci.nsIResProtocolHandler);
26 const categoryManager = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
27 const manager = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
28
29 const BOOTSTRAP_CONTRACT = "@dactyl.googlecode.com/base/bootstrap";
30
31 var name = "dactyl";
32
33 function reportError(e) {
34     let stack = e.stack || Error().stack;
35     dump("\n" + name + ": bootstrap: " + e + "\n" + stack + "\n");
36     Cu.reportError(e);
37     Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService)
38                                        .logStringMessage(stack);
39 }
40 function debug(...args) {
41     if (DEBUG)
42         dump(name + ": " + args.join(", ") + "\n");
43 }
44
45 function httpGet(uri) {
46     let xmlhttp = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
47     xmlhttp.overrideMimeType("text/plain");
48     xmlhttp.open("GET", uri.spec || uri, false);
49     xmlhttp.send(null);
50     return xmlhttp;
51 }
52
53 let moduleName;
54 let initialized = false;
55 let addon = null;
56 let addonData = null;
57 let basePath = null;
58 let bootstrap;
59 let bootstrap_jsm;
60 let categories = [];
61 let components = {};
62 let resources = [];
63 let getURI = null;
64
65 let JSMLoader = {
66     SANDBOX: Cu.nukeSandbox && false,
67
68     get addon() addon,
69
70     currentModule: null,
71
72     factories: [],
73
74     get name() name,
75
76     get module() moduleName,
77
78     globals: {},
79     modules: {},
80
81     times: {
82         all: 0,
83         add: function add(major, minor, delta) {
84             this.all += delta;
85
86             this[major] = (this[major] || 0) + delta;
87             if (minor) {
88                 minor = ":" + minor;
89                 this[minor] = (this[minor] || 0) + delta;
90                 this[major + minor] = (this[major + minor] || 0) + delta;
91             }
92         },
93         clear: function clear() {
94             for (let key in this)
95                 if (typeof this[key] !== "number")
96                     delete this[key];
97         }
98     },
99
100     getTarget: function getTarget(url) {
101         let uri = Services.io.newURI(url, null, null);
102         if (uri.schemeIs("resource"))
103             return resourceProto.resolveURI(uri);
104
105         let chan = Services.io.newChannelFromURI(uri);
106         try { chan.cancel(Cr.NS_BINDING_ABORTED); } catch (e) {}
107         return chan.name;
108     },
109
110     _atexit: [],
111
112     atexit: function atexit(arg, self) {
113         if (typeof arg !== "string")
114             this._atexit.push(arguments);
115         else
116             for each (let [fn, self] in this._atexit)
117                 try {
118                     fn.call(self, arg);
119                 }
120                 catch (e) {
121                     reportError(e);
122                 }
123     },
124
125     _load: function _load(name, target) {
126         let urls = [name];
127         if (name.indexOf(":") === -1)
128             urls = this.config["module-paths"].map(path => path + name + ".jsm");
129
130         for each (let url in urls)
131             try {
132                 var uri = this.getTarget(url);
133                 if (uri in this.globals)
134                     return this.modules[name] = this.globals[uri];
135
136                 this.globals[uri] = this.modules[name];
137                 bootstrap_jsm.loadSubScript(url, this.modules[name]);
138                 return;
139             }
140             catch (e) {
141                 debug("Loading " + name + ": " + e);
142                 delete this.globals[uri];
143
144                 if (typeof e != "string")
145                     throw e;
146             }
147
148         throw Error("No such module: " + name);
149     },
150
151     load: function load(name, target) {
152         if (!this.modules.hasOwnProperty(name)) {
153             this.modules[name] = this.modules.base ? bootstrap.create(this.modules.base)
154                                                    : bootstrap.import({ JSMLoader: this, module: global.module });
155
156             let currentModule = this.currentModule;
157             this.currentModule = this.modules[name];
158
159             try {
160                 this._load(name, this.modules[name]);
161             }
162             catch (e) {
163                 delete this.modules[name];
164                 reportError(e);
165                 throw e;
166             }
167             finally {
168                 this.currentModule = currentModule;
169             }
170         }
171
172         let module = this.modules[name];
173         if (target)
174             for each (let symbol in module.EXPORTED_SYMBOLS)
175                 target[symbol] = module[symbol];
176
177         return module;
178     },
179
180     // Cuts down on stupid, fscking url mangling.
181     get loadSubScript() bootstrap_jsm.loadSubScript,
182
183     cleanup: function unregister() {
184         for each (let factory in this.factories.splice(0))
185             manager.unregisterFactory(factory.classID, factory);
186     },
187
188     Factory: function Factory(class_) ({
189         __proto__: class_.prototype,
190
191         createInstance: function (outer, iid) {
192             try {
193                 if (outer != null)
194                     throw Cr.NS_ERROR_NO_AGGREGATION;
195                 if (!class_.instance)
196                     class_.instance = new class_();
197                 return class_.instance.QueryInterface(iid);
198             }
199             catch (e) {
200                 Cu.reportError(e);
201                 throw e;
202             }
203         }
204     }),
205
206     registerFactory: function registerFactory(factory) {
207         manager.registerFactory(factory.classID,
208                                 String(factory.classID),
209                                 factory.contractID,
210                                 factory);
211         this.factories.push(factory);
212     }
213 };
214
215 function init() {
216     debug("bootstrap: init");
217
218     let manifestURI = getURI("chrome.manifest");
219     let manifest = httpGet(manifestURI)
220             .responseText
221             .replace(/#(resource)#/g, "$1")
222             .replace(/^\s*|\s*$|#.*/g, "")
223             .replace(/^\s*\n/gm, "");
224
225     for each (let line in manifest.split("\n")) {
226         let fields = line.split(/\s+/);
227         switch (fields[0]) {
228         case "category":
229             categoryManager.addCategoryEntry(fields[1], fields[2], fields[3], false, true);
230             categories.push([fields[1], fields[2]]);
231             break;
232         case "component":
233             components[fields[1]] = new FactoryProxy(getURI(fields[2]).spec, fields[1]);
234             break;
235         case "contract":
236             components[fields[2]].contractID = fields[1];
237             break;
238
239         case "resource":
240             moduleName = moduleName || fields[1];
241             resources.push(fields[1]);
242             resourceProto.setSubstitution(fields[1], getURI(fields[2]));
243         }
244     }
245
246     JSMLoader.config = JSON.parse(httpGet("resource://dactyl-local/config.json").responseText);
247
248     bootstrap_jsm = module(BOOTSTRAP);
249     if (!JSMLoader.SANDBOX)
250         bootstrap = bootstrap_jsm;
251     else {
252         bootstrap = Cu.Sandbox(Cc["@mozilla.org/systemprincipal;1"].createInstance(),
253                                { sandboxName: BOOTSTRAP });
254         Services.scriptloader.loadSubScript(BOOTSTRAP, bootstrap);
255     }
256     bootstrap.require = JSMLoader.load("base").require;
257
258     // Flush the cache if necessary, just to be paranoid
259     let pref = "extensions.dactyl.cacheFlushCheck";
260     let val  = addon.version;
261     if (!Services.prefs.prefHasUserValue(pref) || Services.prefs.getCharPref(pref) != val) {
262         var cacheFlush = true;
263         Services.obs.notifyObservers(null, "startupcache-invalidate", "");
264         Services.prefs.setCharPref(pref, val);
265     }
266
267     try {
268         //JSMLoader.load("disable-acr").init(addon.id);
269     }
270     catch (e) {
271         reportError(e);
272     }
273
274     Services.obs.notifyObservers(null, "dactyl-rehash", null);
275
276     JSMLoader.bootstrap = global;
277
278     JSMLoader.load("config", global);
279     JSMLoader.load("main", global);
280
281     JSMLoader.cacheFlush = cacheFlush;
282     JSMLoader.load("base", global);
283
284     if (!(BOOTSTRAP_CONTRACT in Cc)) {
285         // Use Sandbox to prevent closures over this scope
286         let sandbox = Cu.Sandbox(Cc["@mozilla.org/systemprincipal;1"].createInstance());
287         let factory = Cu.evalInSandbox("({ createInstance: function () this })", sandbox);
288
289         factory.classID         = Components.ID("{f541c8b0-fe26-4621-a30b-e77d21721fb5}");
290         factory.contractID      = BOOTSTRAP_CONTRACT;
291         factory.QueryInterface  = XPCOMUtils.generateQI([Ci.nsIFactory]);
292         factory.wrappedJSObject = factory;
293
294         manager.registerFactory(factory.classID, String(factory.classID),
295                                 BOOTSTRAP_CONTRACT, factory);
296     }
297
298     Cc[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader = !Cu.unload && JSMLoader;
299
300     for each (let component in components)
301         component.register();
302
303     updateVersion();
304
305     if (addon !== addonData)
306         require("main", global);
307 }
308
309 /**
310  * Performs necessary migrations after a version change.
311  */
312 function updateVersion() {
313     function isDev(ver) /^hg|pre$/.test(ver);
314     try {
315         if (typeof require === "undefined" || addon === addonData)
316             return;
317
318         JSMLoader.load("prefs", global);
319         config.lastVersion = localPrefs.get("lastVersion", null);
320
321         localPrefs.set("lastVersion", addon.version);
322
323         // We're switching from a nightly version to a stable or
324         // semi-stable version or vice versa.
325         //
326         // Disable automatic updates when switching to nightlies,
327         // restore the default action when switching to stable.
328         if (!config.lastVersion || isDev(config.lastVersion) != isDev(addon.version))
329             addon.applyBackgroundUpdates = AddonManager[isDev(addon.version) ? "AUTOUPDATE_DISABLE" : "AUTOUPDATE_DEFAULT"];
330     }
331     catch (e) {
332         reportError(e);
333     }
334 }
335
336 function startup(data, reason) {
337     debug("bootstrap: startup " + reasonToString(reason));
338     basePath = data.installPath;
339
340     if (!initialized) {
341         initialized = true;
342
343         debug("bootstrap: init " + data.id);
344
345         addonData = data;
346         addon = data;
347         name = data.id.replace(/@.*/, "");
348         AddonManager.getAddonByID(addon.id, function (a) {
349             addon = a;
350
351             updateVersion();
352             if (typeof require !== "undefined")
353                 require("main", global);
354         });
355
356         if (basePath.isDirectory())
357             getURI = function getURI(path) {
358                 let uri = Services.io.newFileURI(basePath);
359                 uri.path += path;
360                 return Services.io.newFileURI(uri.QueryInterface(Ci.nsIFileURL).file);
361             };
362         else
363             getURI = function getURI(path)
364                 Services.io.newURI("jar:" + Services.io.newFileURI(basePath).spec.replace(/!/g, "%21") + "!" +
365                                    "/" + path, null, null);
366
367         try {
368             init();
369         }
370         catch (e) {
371             reportError(e);
372         }
373     }
374 }
375
376 /**
377  * An XPCOM class factory proxy. Loads the JavaScript module at *url*
378  * when an instance is to be created and calls its NSGetFactory method
379  * to obtain the actual factory.
380  *
381  * @param {string} url The URL of the module housing the real factory.
382  * @param {string} classID The CID of the class this factory represents.
383  */
384 function FactoryProxy(url, classID) {
385     this.url = url;
386     this.classID = Components.ID(classID);
387 }
388 FactoryProxy.prototype = {
389     QueryInterface: XPCOMUtils.generateQI(Ci.nsIFactory),
390     register: function () {
391         debug("bootstrap: register: " + this.classID + " " + this.contractID);
392
393         JSMLoader.registerFactory(this);
394     },
395     get module() {
396         debug("bootstrap: create module: " + this.contractID);
397
398         Object.defineProperty(this, "module", { value: {}, enumerable: true });
399         JSMLoader.load(this.url, this.module);
400         return this.module;
401     },
402     createInstance: function (iids) {
403         return let (factory = this.module.NSGetFactory(this.classID))
404             factory.createInstance.apply(factory, arguments);
405     }
406 }
407
408 function shutdown(data, reason) {
409     let strReason = reasonToString(reason);
410     debug("bootstrap: shutdown " + strReason);
411
412     if (reason != APP_SHUTDOWN) {
413         try {
414             //JSMLoader.load("disable-acr").cleanup(addon.id);
415         }
416         catch (e) {
417             reportError(e);
418         }
419
420         if (~[ADDON_UPGRADE, ADDON_DOWNGRADE, ADDON_UNINSTALL].indexOf(reason))
421             Services.obs.notifyObservers(null, "dactyl-purge", null);
422
423         Services.obs.notifyObservers(null, "dactyl-cleanup", strReason);
424         Services.obs.notifyObservers(null, "dactyl-cleanup-modules", reasonToString(reason));
425
426         JSMLoader.atexit(strReason);
427         JSMLoader.cleanup(strReason);
428
429         if (JSMLoader.SANDBOX)
430             Cu.nukeSandbox(bootstrap);
431         bootstrap_jsm.require = null;
432         Cu.unload(BOOTSTRAP);
433         bootstrap = null;
434         bootstrap_jsm = null;
435
436         for each (let [category, entry] in categories)
437             categoryManager.deleteCategoryEntry(category, entry, false);
438         for each (let resource in resources)
439             resourceProto.setSubstitution(resource, null);
440     }
441 }
442
443 function uninstall(data, reason) {
444     debug("bootstrap: uninstall " + reasonToString(reason));
445     if (reason == ADDON_UNINSTALL) {
446         Services.prefs.deleteBranch("extensions.dactyl.");
447
448         if (BOOTSTRAP_CONTRACT in Cc) {
449             let service = Cc[BOOTSTRAP_CONTRACT].getService().wrappedJSObject;
450             manager.unregisterFactory(service.classID, service);
451         }
452     }
453 }
454
455 function reasonToString(reason) {
456     for each (let name in ["disable", "downgrade", "enable",
457                            "install", "shutdown", "startup",
458                            "uninstall", "upgrade"])
459         if (reason == global["ADDON_" + name.toUpperCase()] ||
460             reason == global["APP_" + name.toUpperCase()])
461             return name;
462 }
463
464 function install(data, reason) { debug("bootstrap: install " + reasonToString(reason)); }
465
466 // vim: set fdm=marker sw=4 ts=4 et: