/* 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]);