diff options
Diffstat (limited to 'dom/settings/SettingsManager.js')
-rw-r--r-- | dom/settings/SettingsManager.js | 506 |
1 files changed, 506 insertions, 0 deletions
diff --git a/dom/settings/SettingsManager.js b/dom/settings/SettingsManager.js new file mode 100644 index 000000000..a73e5f312 --- /dev/null +++ b/dom/settings/SettingsManager.js @@ -0,0 +1,506 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); + +var DEBUG = false; +var VERBOSE = false; + +try { + DEBUG = + Services.prefs.getBoolPref("dom.mozSettings.SettingsManager.debug.enabled"); + VERBOSE = + Services.prefs.getBoolPref("dom.mozSettings.SettingsManager.verbose.enabled"); +} catch (ex) { } + +function debug(s) { + dump("-*- SettingsManager: " + s + "\n"); +} + +XPCOMUtils.defineLazyServiceGetter(Services, "DOMRequest", + "@mozilla.org/dom/dom-request-service;1", + "nsIDOMRequestService"); +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsIMessageSender"); +XPCOMUtils.defineLazyServiceGetter(this, "mrm", + "@mozilla.org/memory-reporter-manager;1", + "nsIMemoryReporterManager"); +XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +const kObserverSoftLimit = 10; + +/** + * In order to make SettingsManager work with Privileged Apps, we need the lock + * to be OOP. However, the lock state needs to be managed on the child process, + * while the IDB functions now happen on the parent process so we don't have to + * expose IDB permissions at the child process level. We use the + * DOMRequestHelper mechanism to deal with DOMRequests/promises across the + * processes. + * + * However, due to the nature of the IDBTransaction lifetime, we need to relay + * to the parent when to finalize the transaction once the child is done with the + * lock. We keep a list of all open requests for a lock, and once the lock + * reaches the end of its receiveMessage function with no more queued requests, + * we consider it dead. At that point, we send a message to the parent to notify + * it to finalize the transaction. + */ + +function SettingsLock(aSettingsManager) { + if (VERBOSE) debug("settings lock init"); + this._open = true; + this._settingsManager = aSettingsManager; + this._id = uuidgen.generateUUID().toString(); + + // DOMRequestIpcHelper.initHelper sets this._window + this.initDOMRequestHelper(this._settingsManager._window, ["Settings:Get:OK", "Settings:Get:KO", + "Settings:Clear:OK", "Settings:Clear:KO", + "Settings:Set:OK", "Settings:Set:KO", + "Settings:Finalize:OK", "Settings:Finalize:KO"]); + let createLockPayload = { + lockID: this._id, + isServiceLock: false, + windowID: this._settingsManager.innerWindowID, + lockStack: (new Error).stack + }; + this.sendMessage("Settings:CreateLock", createLockPayload); + Services.tm.currentThread.dispatch(this._closeHelper.bind(this), Ci.nsIThread.DISPATCH_NORMAL); + + // We only want to file closeHelper once per set of receiveMessage calls. + this._closeCalled = true; +} + +SettingsLock.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + set onsettingstransactionsuccess(aHandler) { + this.__DOM_IMPL__.setEventHandler("onsettingstransactionsuccess", aHandler); + }, + + get onsettingstransactionsuccess() { + return this.__DOM_IMPL__.getEventHandler("onsettingstransactionsuccess"); + }, + + set onsettingstransactionfailure(aHandler) { + this.__DOM_IMPL__.setEventHandler("onsettingstransactionfailure", aHandler); + }, + + get onsettingstransactionfailure() { + return this.__DOM_IMPL__.getEventHandler("onsettingstransactionfailure"); + }, + + get closed() { + return !this._open; + }, + + _closeHelper: function() { + if (VERBOSE) debug("closing lock " + this._id); + this._open = false; + this._closeCalled = false; + if (!this._requests || Object.keys(this._requests).length == 0) { + if (VERBOSE) debug("Requests exhausted, finalizing " + this._id); + this._settingsManager.unregisterLock(this._id); + this.sendMessage("Settings:Finalize", {lockID: this._id}); + } else { + if (VERBOSE) debug("Requests left: " + Object.keys(this._requests).length); + this.sendMessage("Settings:Run", {lockID: this._id}); + } + }, + + + _wrap: function _wrap(obj) { + return Cu.cloneInto(obj, this._settingsManager._window); + }, + + sendMessage: function(aMessageName, aData) { + // sendMessage can be called after our window has died, or get + // queued to run later in a thread via _closeHelper, but the + // SettingsManager may have died in between the time it was + // scheduled and the time it runs. Make sure our window is valid + // before sending, otherwise just ignore. + if (!this._settingsManager._window) { + Cu.reportError( + "SettingsManager window died, cannot run settings transaction." + + " SettingsMessage: " + aMessageName + + " SettingsData: " + JSON.stringify(aData)); + return; + } + cpmm.sendAsyncMessage(aMessageName, + aData, + undefined, + this._settingsManager._window.document.nodePrincipal); + }, + + receiveMessage: function(aMessage) { + let msg = aMessage.data; + + // SettingsRequestManager broadcasts changes to all locks in the child. If + // our lock isn't being addressed, just return. + if (msg.lockID != this._id) { + return; + } + if (VERBOSE) debug("receiveMessage (" + this._id + "): " + aMessage.name); + + // Finalizing a transaction does not return a request ID since we are + // supposed to fire callbacks. + // + // We also destroy the DOMRequestHelper after we've received the + // finalize message. At this point, we will be guarenteed no more + // request returns are coming from the SettingsRequestManager. + + if (!msg.requestID) { + let event; + switch (aMessage.name) { + case "Settings:Finalize:OK": + if (VERBOSE) debug("Lock finalize ok: " + this._id); + event = new this._window.MozSettingsTransactionEvent("settingstransactionsuccess", {}); + this.__DOM_IMPL__.dispatchEvent(event); + this.destroyDOMRequestHelper(); + break; + case "Settings:Finalize:KO": + if (DEBUG) debug("Lock finalize failed: " + this._id); + event = new this._window.MozSettingsTransactionEvent("settingstransactionfailure", { + error: msg.errorMsg + }); + this.__DOM_IMPL__.dispatchEvent(event); + this.destroyDOMRequestHelper(); + break; + default: + if (DEBUG) debug("Message type " + aMessage.name + " is missing a requestID"); + } + return; + } + + + let req = this.getRequest(msg.requestID); + if (!req) { + if (DEBUG) debug("Matching request not found."); + return; + } + this.removeRequest(msg.requestID); + // DOMRequest callbacks called from here can die due to having + // things like marionetteScriptFinished in them. Make sure we file + // our call to run/finalize BEFORE opening the lock and fulfilling + // DOMRequests. + if (!this._closeCalled) { + // We only want to file closeHelper once per set of receiveMessage calls. + Services.tm.currentThread.dispatch(this._closeHelper.bind(this), Ci.nsIThread.DISPATCH_NORMAL); + this._closeCalled = true; + } + if (VERBOSE) debug("receiveMessage: " + aMessage.name); + switch (aMessage.name) { + case "Settings:Get:OK": + for (let i in msg.settings) { + msg.settings[i] = this._wrap(msg.settings[i]); + } + this._open = true; + Services.DOMRequest.fireSuccess(req.request, this._wrap(msg.settings)); + this._open = false; + break; + case "Settings:Set:OK": + case "Settings:Clear:OK": + this._open = true; + Services.DOMRequest.fireSuccess(req.request, 0); + this._open = false; + break; + case "Settings:Get:KO": + case "Settings:Set:KO": + case "Settings:Clear:KO": + if (DEBUG) debug("error:" + msg.errorMsg); + Services.DOMRequest.fireError(req.request, msg.errorMsg); + break; + default: + if (DEBUG) debug("Wrong message: " + aMessage.name); + } + }, + + get: function get(aName) { + if (VERBOSE) debug("get (" + this._id + "): " + aName); + if (!this._open) { + dump("Settings lock not open!\n"); + throw Components.results.NS_ERROR_ABORT; + } + let req = this.createRequest(); + let reqID = this.getRequestId({request: req}); + this.sendMessage("Settings:Get", {requestID: reqID, + lockID: this._id, + name: aName}); + return req; + }, + + set: function set(aSettings) { + if (VERBOSE) debug("send: " + JSON.stringify(aSettings)); + if (!this._open) { + throw "Settings lock not open"; + } + let req = this.createRequest(); + let reqID = this.getRequestId({request: req}); + this.sendMessage("Settings:Set", {requestID: reqID, + lockID: this._id, + settings: aSettings}); + return req; + }, + + clear: function clear() { + if (VERBOSE) debug("clear"); + if (!this._open) { + throw "Settings lock not open"; + } + let req = this.createRequest(); + let reqID = this.getRequestId({request: req}); + this.sendMessage("Settings:Clear", {requestID: reqID, + lockID: this._id}); + return req; + }, + + classID: Components.ID("{60c9357c-3ae0-4222-8f55-da01428470d5}"), + contractID: "@mozilla.org/settingsLock;1", + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, + Ci.nsIObserver, + Ci.nsISupportsWeakReference]) +}; + +function SettingsManager() { + this._callbacks = null; + this._isRegistered = false; + this._locks = []; + this._createdLocks = 0; + this._unregisteredLocks = 0; +} + +SettingsManager.prototype = { + _wrap: function _wrap(obj) { + return Cu.cloneInto(obj, this._window); + }, + + set onsettingchange(aHandler) { + this.__DOM_IMPL__.setEventHandler("onsettingchange", aHandler); + this.checkMessageRegistration(); + }, + + get onsettingchange() { + return this.__DOM_IMPL__.getEventHandler("onsettingchange"); + }, + + createLock: function() { + let lock = new SettingsLock(this); + if (VERBOSE) debug("creating lock " + lock._id); + this._locks.push(lock._id); + this._createdLocks++; + return lock; + }, + + unregisterLock: function(aLockID) { + let lock_index = this._locks.indexOf(aLockID); + if (lock_index != -1) { + if (VERBOSE) debug("Unregistering lock " + aLockID); + this._locks.splice(lock_index, 1); + this._unregisteredLocks++; + } + }, + + receiveMessage: function(aMessage) { + if (VERBOSE) debug("Settings::receiveMessage: " + aMessage.name); + let msg = aMessage.json; + + switch (aMessage.name) { + case "Settings:Change:Return:OK": + if (VERBOSE) debug('data:' + msg.key + ':' + msg.value + '\n'); + + let event = new this._window.MozSettingsEvent("settingchange", this._wrap({ + settingName: msg.key, + settingValue: msg.value + })); + this.__DOM_IMPL__.dispatchEvent(event); + + if (this._callbacks && this._callbacks[msg.key]) { + if (VERBOSE) debug("observe callback called! " + msg.key + " " + this._callbacks[msg.key].length); + this._callbacks[msg.key].forEach(function(cb) { + cb(this._wrap({settingName: msg.key, settingValue: msg.value})); + }.bind(this)); + } else { + if (VERBOSE) debug("no observers stored!"); + } + break; + default: + if (DEBUG) debug("Wrong message: " + aMessage.name); + } + }, + + // If we have either observer callbacks or an event handler, + // register for messages from the main thread. Otherwise, if no one + // is listening, unregister to reduce parent load. + checkMessageRegistration: function checkRegistration() { + let handler = this.__DOM_IMPL__.getEventHandler("onsettingchange"); + if (!this._isRegistered) { + if (VERBOSE) debug("Registering for messages"); + cpmm.sendAsyncMessage("Settings:RegisterForMessages", + undefined, + undefined, + this._window.document.nodePrincipal); + this._isRegistered = true; + } else { + if ((!this._callbacks || Object.keys(this._callbacks).length == 0) && + !handler) { + if (VERBOSE) debug("Unregistering for messages"); + cpmm.sendAsyncMessage("Settings:UnregisterForMessages", + undefined, + undefined, + this._window.document.nodePrincipal); + this._isRegistered = false; + this._callbacks = null; + } + } + }, + + addObserver: function addObserver(aName, aCallback) { + if (VERBOSE) debug("addObserver " + aName); + + if (!this._callbacks) { + this._callbacks = {}; + } + + if (!this._callbacks[aName]) { + this._callbacks[aName] = [aCallback]; + } else { + this._callbacks[aName].push(aCallback); + } + + let length = this._callbacks[aName].length; + if (length >= kObserverSoftLimit) { + debug("WARNING: MORE THAN " + kObserverSoftLimit + " OBSERVERS FOR " + + aName + ": " + length + " FROM" + (new Error).stack); +#ifdef DEBUG + debug("JS STOPS EXECUTING AT THIS POINT IN DEBUG BUILDS!"); + throw Components.results.NS_ERROR_ABORT; +#endif + } + + this.checkMessageRegistration(); + }, + + removeObserver: function removeObserver(aName, aCallback) { + if (VERBOSE) debug("deleteObserver " + aName); + if (this._callbacks && this._callbacks[aName]) { + let index = this._callbacks[aName].indexOf(aCallback); + if (index != -1) { + this._callbacks[aName].splice(index, 1); + if (this._callbacks[aName].length == 0) { + delete this._callbacks[aName]; + } + } else { + if (VERBOSE) debug("Callback not found for: " + aName); + } + } else { + if (VERBOSE) debug("No observers stored for " + aName); + } + this.checkMessageRegistration(); + }, + + init: function(aWindow) { + if (VERBOSE) debug("SettingsManager init"); + mrm.registerStrongReporter(this); + cpmm.addMessageListener("Settings:Change:Return:OK", this); + Services.obs.addObserver(this, "inner-window-destroyed", false); + let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + this.innerWindowID = util.currentInnerWindowID; + this._window = aWindow; + }, + + observe: function(aSubject, aTopic, aData) { + if (VERBOSE) debug("Topic: " + aTopic); + if (aTopic === "inner-window-destroyed") { + let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (wId === this.innerWindowID) { + if (DEBUG) debug("Received: inner-window-destroyed for valid innerWindowID=" + wId + ", cleanup."); + this.cleanup(); + } + } + }, + + collectReports: function(aCallback, aData, aAnonymize) { + for (let topic in this._callbacks) { + let length = this._callbacks[topic].length; + if (length == 0) { + continue; + } + + let path; + if (length < kObserverSoftLimit) { + path = "settings-observers"; + } else { + path = "settings-observers-suspect/referent(topic=" + + (aAnonymize ? "<anonymized>" : topic) + ")"; + } + + aCallback.callback("", path, + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + length, + "The number of settings observers for this topic.", + aData); + } + + aCallback.callback("", + "settings-locks/alive", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this._locks.length, + "The number of locks that are currently alives.", + aData); + + aCallback.callback("", + "settings-locks/created", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this._createdLocks, + "The number of locks that were created.", + aData); + + aCallback.callback("", + "settings-locks/deleted", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this._unregisteredLocks, + "The number of locks that were deleted.", + aData); + }, + + cleanup: function() { + Services.obs.removeObserver(this, "inner-window-destroyed"); + // At this point, the window is dying, so there's nothing left + // that we could do with our lock. Go ahead and run finalize on + // it to make sure changes are commited. + for (let i = 0; i < this._locks.length; ++i) { + if (DEBUG) debug("Lock alive at destroy, finalizing: " + this._locks[i]); + // Due to bug 1105511 we should be able to send this without + // cached principals. However, this is scary because any iframe + // in the process could run this? + cpmm.sendAsyncMessage("Settings:Finalize", + {lockID: this._locks[i]}); + } + cpmm.removeMessageListener("Settings:Change:Return:OK", this); + mrm.unregisterStrongReporter(this); + this.innerWindowID = null; + this._window = null; + }, + + classID: Components.ID("{c40b1c70-00fb-11e2-a21f-0800200c9a66}"), + contractID: "@mozilla.org/settingsManager;1", + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, + Ci.nsIDOMGlobalPropertyInitializer, + Ci.nsIObserver, + Ci.nsIMemoryReporter]), +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsManager, SettingsLock]); |