summaryrefslogtreecommitdiffstats
path: root/dom/settings/SettingsManager.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/settings/SettingsManager.js')
-rw-r--r--dom/settings/SettingsManager.js506
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]);