From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- dom/settings/SettingsDB.jsm | 249 ++++ dom/settings/SettingsManager.js | 506 ++++++++ dom/settings/SettingsManager.manifest | 5 + dom/settings/SettingsRequestManager.jsm | 1224 ++++++++++++++++++++ dom/settings/SettingsService.js | 358 ++++++ dom/settings/SettingsService.manifest | 5 + dom/settings/moz.build | 28 + dom/settings/tests/chrome.ini | 24 + dom/settings/tests/file_bug1110872.html | 47 + dom/settings/tests/file_bug1110872.js | 47 + dom/settings/tests/file_loadserver.js | 17 + dom/settings/tests/test_settings_basics.html | 816 +++++++++++++ dom/settings/tests/test_settings_blobs.html | 148 +++ dom/settings/tests/test_settings_bug1110872.html | 17 + dom/settings/tests/test_settings_data_uris.html | 149 +++ dom/settings/tests/test_settings_events.html | 47 + .../tests/test_settings_navigator_object.html | 37 + .../tests/test_settings_observer_killer.html | 60 + .../tests/test_settings_onsettingchange.html | 306 +++++ dom/settings/tests/test_settings_permissions.html | 184 +++ dom/settings/tests/test_settings_service.js | 138 +++ dom/settings/tests/test_settings_service.xul | 19 + .../tests/test_settings_service_callback.js | 47 + .../tests/test_settings_service_callback.xul | 19 + .../unit/test_settingsrequestmanager_messages.js | 174 +++ dom/settings/tests/unit/xpcshell.ini | 6 + 26 files changed, 4677 insertions(+) create mode 100644 dom/settings/SettingsDB.jsm create mode 100644 dom/settings/SettingsManager.js create mode 100644 dom/settings/SettingsManager.manifest create mode 100644 dom/settings/SettingsRequestManager.jsm create mode 100644 dom/settings/SettingsService.js create mode 100644 dom/settings/SettingsService.manifest create mode 100644 dom/settings/moz.build create mode 100644 dom/settings/tests/chrome.ini create mode 100644 dom/settings/tests/file_bug1110872.html create mode 100644 dom/settings/tests/file_bug1110872.js create mode 100644 dom/settings/tests/file_loadserver.js create mode 100644 dom/settings/tests/test_settings_basics.html create mode 100644 dom/settings/tests/test_settings_blobs.html create mode 100644 dom/settings/tests/test_settings_bug1110872.html create mode 100644 dom/settings/tests/test_settings_data_uris.html create mode 100644 dom/settings/tests/test_settings_events.html create mode 100644 dom/settings/tests/test_settings_navigator_object.html create mode 100644 dom/settings/tests/test_settings_observer_killer.html create mode 100644 dom/settings/tests/test_settings_onsettingchange.html create mode 100644 dom/settings/tests/test_settings_permissions.html create mode 100644 dom/settings/tests/test_settings_service.js create mode 100644 dom/settings/tests/test_settings_service.xul create mode 100644 dom/settings/tests/test_settings_service_callback.js create mode 100644 dom/settings/tests/test_settings_service_callback.xul create mode 100644 dom/settings/tests/unit/test_settingsrequestmanager_messages.js create mode 100644 dom/settings/tests/unit/xpcshell.ini (limited to 'dom/settings') diff --git a/dom/settings/SettingsDB.jsm b/dom/settings/SettingsDB.jsm new file mode 100644 index 000000000..b7d867a48 --- /dev/null +++ b/dom/settings/SettingsDB.jsm @@ -0,0 +1,249 @@ +/* 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"; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +Cu.importGlobalProperties(['Blob', 'File']); +Cu.import("resource://gre/modules/Services.jsm"); + +this.EXPORTED_SYMBOLS = ["SettingsDB", "SETTINGSDB_NAME", "SETTINGSSTORE_NAME"]; + +var DEBUG = false; +var VERBOSE = false; + +try { + DEBUG = + Services.prefs.getBoolPref("dom.mozSettings.SettingsDB.debug.enabled"); + VERBOSE = + Services.prefs.getBoolPref("dom.mozSettings.SettingsDB.verbose.enabled"); +} catch (ex) { } + +function debug(s) { + dump("-*- SettingsDB: " + s + "\n"); +} + +const TYPED_ARRAY_THINGS = new Set([ + "Int8Array", + "Uint8Array", + "Uint8ClampedArray", + "Int16Array", + "Uint16Array", + "Int32Array", + "Uint32Array", + "Float32Array", + "Float64Array", +]); + +this.SETTINGSDB_NAME = "settings"; +this.SETTINGSDB_VERSION = 8; +this.SETTINGSSTORE_NAME = "settings"; + +Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +this.SettingsDB = function SettingsDB() {} + +SettingsDB.prototype = { + + __proto__: IndexedDBHelper.prototype, + + upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { + let objectStore; + if (aOldVersion == 0) { + objectStore = aDb.createObjectStore(SETTINGSSTORE_NAME, { keyPath: "settingName" }); + if (VERBOSE) debug("Created object stores"); + } else if (aOldVersion == 1) { + if (VERBOSE) debug("Get object store for upgrade and remove old index"); + objectStore = aTransaction.objectStore(SETTINGSSTORE_NAME); + objectStore.deleteIndex("settingValue"); + } else { + if (VERBOSE) debug("Get object store for upgrade"); + objectStore = aTransaction.objectStore(SETTINGSSTORE_NAME); + } + + // Loading resource://app/defaults/settings.json doesn't work because + // settings.json is not in the omnijar. + // So we look for the app dir instead and go from here... + let settingsFile = FileUtils.getFile("DefRt", ["settings.json"], false); + if (!settingsFile || (settingsFile && !settingsFile.exists())) { + // On b2g desktop builds the settings.json file is moved in the + // profile directory by the build system. + settingsFile = FileUtils.getFile("ProfD", ["settings.json"], false); + if (!settingsFile || (settingsFile && !settingsFile.exists())) { + return; + } + } + + let chan = NetUtil.newChannel({ + uri: NetUtil.newURI(settingsFile), + loadUsingSystemPrincipal: true}); + let stream = chan.open2(); + // Obtain a converter to read from a UTF-8 encoded input stream. + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + let rawstr = converter.ConvertToUnicode(NetUtil.readInputStreamToString( + stream, + stream.available()) || ""); + let settings; + try { + settings = JSON.parse(rawstr); + } catch(e) { + if (DEBUG) debug("Error parsing " + settingsFile.path + " : " + e); + return; + } + stream.close(); + + objectStore.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + let value = cursor.value; + if (value.settingName in settings) { + if (VERBOSE) debug("Upgrade " +settings[value.settingName]); + value.defaultValue = this.prepareValue(settings[value.settingName]); + delete settings[value.settingName]; + if ("settingValue" in value) { + value.userValue = this.prepareValue(value.settingValue); + delete value.settingValue; + } + cursor.update(value); + } else if ("userValue" in value || "settingValue" in value) { + value.defaultValue = undefined; + if (aOldVersion == 1 && value.settingValue) { + value.userValue = this.prepareValue(value.settingValue); + delete value.settingValue; + } + cursor.update(value); + } else { + cursor.delete(); + } + cursor.continue(); + } else { + for (let name in settings) { + let value = this.prepareValue(settings[name]); + if (VERBOSE) debug("Set new:" + name +", " + value); + objectStore.add({ settingName: name, defaultValue: value, userValue: undefined }); + } + } + }.bind(this); + }, + + // If the value is a data: uri, convert it to a Blob. + convertDataURIToBlob: function(aValue) { + /* base64 to ArrayBuffer decoding, from + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding + */ + function b64ToUint6 (nChr) { + return nChr > 64 && nChr < 91 ? + nChr - 65 + : nChr > 96 && nChr < 123 ? + nChr - 71 + : nChr > 47 && nChr < 58 ? + nChr + 4 + : nChr === 43 ? + 62 + : nChr === 47 ? + 63 + : + 0; + } + + function base64DecToArr(sBase64, nBlocksSize) { + let sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), + nInLen = sB64Enc.length, + nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize + : nInLen * 3 + 1 >> 2, + taBytes = new Uint8Array(nOutLen); + + for (let nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { + nMod4 = nInIdx & 3; + nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; + if (nMod4 === 3 || nInLen - nInIdx === 1) { + for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { + taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; + } + nUint24 = 0; + } + } + return taBytes; + } + + // Check if we have a data: uri, and if it's base64 encoded. + // data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEA... + if (typeof aValue == "string" && aValue.startsWith("data:")) { + try { + let uri = Services.io.newURI(aValue, null, null); + // XXX: that would be nice to reuse the c++ bits of the data: + // protocol handler instead. + let mimeType = "application/octet-stream"; + let mimeDelim = aValue.indexOf(";"); + if (mimeDelim !== -1) { + mimeType = aValue.substring(5, mimeDelim); + } + let start = aValue.indexOf(",") + 1; + let isBase64 = ((aValue.indexOf("base64") + 7) == start); + let payload = aValue.substring(start); + + return new Blob([isBase64 ? base64DecToArr(payload) : payload], + { type: mimeType }); + } catch(e) { + dump(e); + } + } + return aValue + }, + + getObjectKind: function(aObject) { + if (aObject === null || aObject === undefined) { + return "primitive"; + } else if (Array.isArray(aObject)) { + return "array"; + } else if (aObject instanceof File) { + return "file"; + } else if (aObject instanceof Ci.nsIDOMBlob) { + return "blob"; + } else if (aObject.constructor.name == "Date") { + return "date"; + } else if (TYPED_ARRAY_THINGS.has(aObject.constructor.name)) { + return aObject.constructor.name; + } else if (typeof aObject == "object") { + return "object"; + } else { + return "primitive"; + } + }, + + // Makes sure any property that is a data: uri gets converted to a Blob. + prepareValue: function(aObject) { + let kind = this.getObjectKind(aObject); + if (kind == "array") { + let res = []; + aObject.forEach(function(aObj) { + res.push(this.prepareValue(aObj)); + }, this); + return res; + } else if (kind == "file" || kind == "blob" || kind == "date") { + return aObject; + } else if (kind == "primitive") { + return this.convertDataURIToBlob(aObject); + } + + // Fall-through, we now have a dictionary object. + let res = {}; + for (let prop in aObject) { + res[prop] = this.prepareValue(aObject[prop]); + } + return res; + }, + + init: function init() { + this.initDBHelper(SETTINGSDB_NAME, SETTINGSDB_VERSION, + [SETTINGSSTORE_NAME]); + } +} 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 ? "" : 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]); diff --git a/dom/settings/SettingsManager.manifest b/dom/settings/SettingsManager.manifest new file mode 100644 index 000000000..fc9612928 --- /dev/null +++ b/dom/settings/SettingsManager.manifest @@ -0,0 +1,5 @@ +component {c40b1c70-00fb-11e2-a21f-0800200c9a66} SettingsManager.js +contract @mozilla.org/settingsManager;1 {c40b1c70-00fb-11e2-a21f-0800200c9a66} + +component {60c9357c-3ae0-4222-8f55-da01428470d5} SettingsManager.js +contract @mozilla.org/settingsLock;1 {60c9357c-3ae0-4222-8f55-da01428470d5} diff --git a/dom/settings/SettingsRequestManager.jsm b/dom/settings/SettingsRequestManager.jsm new file mode 100644 index 000000000..fced80247 --- /dev/null +++ b/dom/settings/SettingsRequestManager.jsm @@ -0,0 +1,1224 @@ +/* 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 Cr = Components.results; +const Cu = Components.utils; + +Cu.importGlobalProperties(['File']); + +this.EXPORTED_SYMBOLS = ["SettingsRequestManager"]; + +Cu.import("resource://gre/modules/SettingsDB.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PermissionsTable.jsm"); + +var DEBUG = false; +var VERBOSE = false; +var TRACK = false; + +try { + DEBUG = + Services.prefs.getBoolPref("dom.mozSettings.SettingsRequestManager.debug.enabled"); + VERBOSE = + Services.prefs.getBoolPref("dom.mozSettings.SettingsRequestManager.verbose.enabled"); + TRACK = + Services.prefs.getBoolPref("dom.mozSettings.trackTasksUsage"); +} catch (ex) { } + +var allowForceReadOnly = false; +try { + allowForceReadOnly = Services.prefs.getBoolPref("dom.mozSettings.allowForceReadOnly"); +} catch (ex) { } + +function debug(s) { + dump("-*- SettingsRequestManager: " + s + "\n"); +} + +var inParent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) + .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + +const kXpcomShutdownObserverTopic = "xpcom-shutdown"; +const kInnerWindowDestroyed = "inner-window-destroyed"; +const kMozSettingsChangedObserverTopic = "mozsettings-changed"; +const kSettingsReadSuffix = "-read"; +const kSettingsWriteSuffix = "-write"; +const kSettingsClearPermission = "settings-clear"; +const kAllSettingsReadPermission = "settings" + kSettingsReadSuffix; +const kAllSettingsWritePermission = "settings" + kSettingsWriteSuffix; +// Any application with settings permissions, be it for all settings +// or a single one, will need to be able to access the settings API. +// The settings-api permission allows an app to see the mozSettings +// API in order to create locks and queue tasks. Whether these tasks +// will be allowed depends on the exact permissions the app has. +const kSomeSettingsReadPermission = "settings-api" + kSettingsReadSuffix; +const kSomeSettingsWritePermission = "settings-api" + kSettingsWriteSuffix; + +// Time, in seconds, to consider the API is starting to jam +var kSoftLockupDelta = 30; +try { + kSoftLockupDelta = Services.prefs.getIntPref("dom.mozSettings.softLockupDelta"); +} catch (ex) { } + +XPCOMUtils.defineLazyServiceGetter(this, "mrm", + "@mozilla.org/memory-reporter-manager;1", + "nsIMemoryReporterManager"); +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageBroadcaster"); +XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); +XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + +var SettingsPermissions = { + checkPermission: function(aPrincipal, aPerm) { + if (!aPrincipal) { + Cu.reportError("SettingsPermissions.checkPermission was passed a null principal. Denying all permissions."); + return false; + } + if (aPrincipal.origin == "[System Principal]" || + Services.perms.testExactPermissionFromPrincipal(aPrincipal, aPerm) == Ci.nsIPermissionManager.ALLOW_ACTION) { + return true; + } + return false; + }, + hasAllReadPermission: function(aPrincipal) { + return this.checkPermission(aPrincipal, kAllSettingsReadPermission); + }, + hasAllWritePermission: function(aPrincipal) { + return this.checkPermission(aPrincipal, kAllSettingsWritePermission); + }, + hasSomeReadPermission: function(aPrincipal) { + return this.checkPermission(aPrincipal, kSomeSettingsReadPermission); + }, + hasSomeWritePermission: function(aPrincipal) { + return this.checkPermission(aPrincipal, kSomeSettingsWritePermission); + }, + hasClearPermission: function(aPrincipal) { + return this.checkPermission(aPrincipal, kSettingsClearPermission); + }, + hasReadPermission: function(aPrincipal, aSettingsName) { + return this.hasAllReadPermission(aPrincipal) || this.checkPermission(aPrincipal, "settings:" + aSettingsName + kSettingsReadSuffix); + }, + hasWritePermission: function(aPrincipal, aSettingsName) { + return this.hasAllWritePermission(aPrincipal) || this.checkPermission(aPrincipal, "settings:" + aSettingsName + kSettingsWriteSuffix); + } +}; + + +function SettingsLockInfo(aDB, aMsgMgr, aPrincipal, aLockID, aIsServiceLock, aWindowID, aLockStack) { + return { + // ID Shared with the object on the child side + lockID: aLockID, + // Is this a content lock or a settings service lock? + isServiceLock: aIsServiceLock, + // Which inner window ID + windowID: aWindowID, + // Where does this lock comes from + lockStack: aLockStack, + // Tasks to be run once the lock is at the head of the queue + tasks: [], + // This is set to true once a transaction is ready to run, but is not at the + // head of the lock queue. + consumable: false, + // Holds values that are requested to be set until the lock lifetime ends, + // then commits them to the DB. + queuedSets: {}, + // Internal transaction object + _transaction: undefined, + // Message manager that controls the lock + _mm: aMsgMgr, + // If true, it means a permissions check failed, so just fail everything now + _failed: false, + // If we're slated to run finalize, set this to make sure we don't + // somehow run other events afterward. + finalizing: false, + // Lets us know if we can use this lock for a clear command + canClear: true, + // Lets us know if this lock has been used to clear at any point. + hasCleared: false, + // forceReadOnly sets whether we want to do a read only transaction. Define + // true by default, and let queueTask() set this to false if we queue any + // "set" task. Since users of settings locks will queue all tasks before + // any idb transaction is created, we know we will have all needed + // information to set this before creating a transaction. + forceReadOnly: true, + // Principal the lock was created under. We assume that the lock + // will continue to exist under this principal for the duration of + // its lifetime. + principal: aPrincipal, + getObjectStore: function() { + if (VERBOSE) debug("Getting transaction for " + this.lockID); + let store; + // Test for transaction validity via trying to get the + // datastore. If it doesn't work, assume the transaction is + // closed, create a new transaction and try again. + if (this._transaction) { + try { + store = this._transaction.objectStore(SETTINGSSTORE_NAME); + } catch (e) { + if (e.name == "InvalidStateError") { + if (VERBOSE) debug("Current transaction for " + this.lockID + " closed, trying to create new one."); + } else { + if (DEBUG) debug("Unexpected exception, throwing: " + e); + throw e; + } + } + } + // Create one transaction with a global permission. This may be + // slightly slower on apps with full settings permissions, but + // it means we don't have to do our own transaction order + // bookkeeping. + let canReadOnly = allowForceReadOnly && this.forceReadOnly; + if (canReadOnly || !SettingsPermissions.hasSomeWritePermission(this.principal)) { + if (VERBOSE) debug("Making READONLY transaction for " + this.lockID); + this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readonly"); + } else { + if (VERBOSE) debug("Making READWRITE transaction for " + this.lockID); + this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readwrite"); + } + this._transaction.oncomplete = function() { + if (VERBOSE) debug("Transaction for lock " + this.lockID + " closed"); + }.bind(this); + this._transaction.onabort = function () { + if (DEBUG) debug("Transaction for lock " + this.lockID + " aborted"); + this._failed = true; + }.bind(this); + try { + store = this._transaction.objectStore(SETTINGSSTORE_NAME); + } catch (e) { + if (e.name == "InvalidStateError") { + if (DEBUG) debug("Cannot create objectstore on transaction for " + this.lockID); + return null; + } else { + if (DEBUG) debug("Unexpected exception, throwing: " + e); + throw e; + } + } + return store; + } + }; +} + +var SettingsRequestManager = { + // Access to the settings DB + settingsDB: new SettingsDB(), + // Remote messages to listen for from child + messages: ["child-process-shutdown", "Settings:Get", "Settings:Set", + "Settings:Clear", "Settings:Run", "Settings:Finalize", + "Settings:CreateLock", "Settings:RegisterForMessages"], + // Map of LockID to SettingsLockInfo objects + lockInfo: {}, + // Storing soft lockup detection infos + softLockup: { + lockId: null, // last lock dealt with + lockTs: null // last time of dealing with + }, + // Queue of LockIDs. The LockID on the front of the queue is the only lock + // that will have requests processed, all other locks will queue requests + // until they hit the front of the queue. + settingsLockQueue: [], + children: [], + // Since we need to call observers at times when we may not have + // just received a message from a child process, we cache principals + // for message managers and check permissions on them before we send + // settings notifications to child processes. + observerPrincipalCache: new Map(), + totalProcessed: 0, + tasksConsumed: {}, + totalSetProcessed: 0, + tasksSetConsumed: {}, + totalGetProcessed: 0, + tasksGetConsumed: {}, + + init: function() { + if (VERBOSE) debug("init"); + this.settingsDB.init(); + this.messages.forEach((function(msgName) { + ppmm.addMessageListener(msgName, this); + }).bind(this)); + Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false); + Services.obs.addObserver(this, kInnerWindowDestroyed, false); + mrm.registerStrongReporter(this); + }, + + _serializePreservingBinaries: function _serializePreservingBinaries(aObject) { + function needsUUID(aValue) { + if (!aValue || !aValue.constructor) { + return false; + } + return (aValue.constructor.name == "Date") || (aValue instanceof File) || + (aValue instanceof Ci.nsIDOMBlob); + } + // We need to serialize settings objects, otherwise they can change between + // the set() call and the enqueued request being processed. We can't simply + // parse(stringify(obj)) because that breaks things like Blobs, Files and + // Dates, so we use stringify's replacer and parse's reviver parameters to + // preserve binaries. + let binaries = Object.create(null); + let stringified = JSON.stringify(aObject, function(key, value) { + value = this.settingsDB.prepareValue(value); + if (needsUUID(value)) { + let uuid = uuidgen.generateUUID().toString(); + binaries[uuid] = value; + return uuid; + } + return value; + }.bind(this)); + return JSON.parse(stringified, function(key, value) { + if (value in binaries) { + return binaries[value]; + } + return value; + }); + }, + + queueTask: function(aOperation, aData) { + if (VERBOSE) debug("Queueing task: " + aOperation); + + let defer = {}; + + let lock = this.lockInfo[aData.lockID]; + + if (!lock) { + return Promise.reject({error: "Lock already dead, cannot queue task"}); + } + + if (aOperation == "set") { + aData.settings = this._serializePreservingBinaries(aData.settings); + } + + if (aOperation === "set" || aOperation === "clear") { + lock.forceReadOnly = false; + } + + lock.tasks.push({ + operation: aOperation, + data: aData, + defer: defer + }); + + let promise = new Promise(function(resolve, reject) { + defer.resolve = resolve; + defer.reject = reject; + }); + + return promise; + }, + + // Due to the fact that we're skipping the database in some places + // by keeping a local "set" value cache, resolving some calls + // without a call to the database would mean we could potentially + // receive promise responses out of expected order if a get is + // called before a set. Therefore, we wrap our resolve in a null + // get, which means it will resolves afer the rest of the calls + // queued to the DB. + queueTaskReturn: function(aTask, aReturnValue) { + if (VERBOSE) debug("Making task queuing transaction request."); + let data = aTask.data; + let lock = this.lockInfo[data.lockID]; + let store = lock.getObjectStore(lock.principal); + if (!store) { + if (DEBUG) debug("Rejecting task queue on lock " + aTask.data.lockID); + return Promise.reject({task: aTask, error: "Cannot get object store"}); + } + // Due to the fact that we're skipping the database, resolving + // this without a call to the database would mean we could + // potentially receive promise responses out of expected order if + // a get is called before a set. Therefore, we wrap our resolve in + // a null get, which means it will resolves afer the rest of the + // calls queued to the DB. + let getReq = store.get(0); + + let defer = {}; + let promiseWrapper = new Promise(function(resolve, reject) { + defer.resolve = resolve; + defer.reject = reject; + }); + + getReq.onsuccess = function(event) { + return defer.resolve(aReturnValue); + }; + getReq.onerror = function() { + return defer.reject({task: aTask, error: getReq.error.name}); + }; + return promiseWrapper; + }, + + taskGet: function(aTask) { + if (VERBOSE) debug("Running Get task on lock " + aTask.data.lockID); + + // Check that we have permissions for getting the value + let data = aTask.data; + let lock = this.lockInfo[data.lockID]; + + if (!lock) { + return Promise.reject({task: aTask, error: "Lock died, can't finalize"}); + } + if (lock._failed) { + if (DEBUG) debug("Lock failed. All subsequent requests will fail."); + return Promise.reject({task: aTask, error: "Lock failed, all requests now failing."}); + } + + if (lock.hasCleared) { + if (DEBUG) debug("Lock was used for a clear command. All subsequent requests will fail."); + return Promise.reject({task: aTask, error: "Lock was used for a clear command. All subsequent requests will fail."}); + } + + lock.canClear = false; + + if (!SettingsPermissions.hasReadPermission(lock.principal, data.name)) { + if (DEBUG) debug("get not allowed for " + data.name); + lock._failed = true; + return Promise.reject({task: aTask, error: "No permission to get " + data.name}); + } + + // If the value was set during this transaction, use the cached value + if (data.name in lock.queuedSets) { + if (VERBOSE) debug("Returning cached set value " + lock.queuedSets[data.name] + " for " + data.name); + let local_results = {}; + local_results[data.name] = lock.queuedSets[data.name]; + return this.queueTaskReturn(aTask, {task: aTask, results: local_results}); + } + + // Create/Get transaction and make request + if (VERBOSE) debug("Making get transaction request for " + data.name); + let store = lock.getObjectStore(lock.principal); + if (!store) { + if (DEBUG) debug("Rejecting Get task on lock " + aTask.data.lockID); + return Promise.reject({task: aTask, error: "Cannot get object store"}); + } + + if (VERBOSE) debug("Making get request for " + data.name); + let getReq = (data.name === "*") ? store.mozGetAll() : store.mozGetAll(data.name); + + let defer = {}; + let promiseWrapper = new Promise(function(resolve, reject) { + defer.resolve = resolve; + defer.reject = reject; + }); + + getReq.onsuccess = function(event) { + if (VERBOSE) debug("Request for '" + data.name + "' successful. " + + "Record count: " + event.target.result.length); + + if (event.target.result.length == 0) { + if (VERBOSE) debug("MOZSETTINGS-GET-WARNING: " + data.name + " is not in the database.\n"); + } + + let results = {}; + + for (let i in event.target.result) { + let result = event.target.result[i]; + let name = result.settingName; + if (VERBOSE) debug(name + ": " + result.userValue +", " + result.defaultValue); + let value = result.userValue !== undefined ? result.userValue : result.defaultValue; + results[name] = value; + } + return defer.resolve({task: aTask, results: results}); + }; + getReq.onerror = function() { + return defer.reject({task: aTask, error: getReq.error.name}); + }; + return promiseWrapper; + }, + + taskSet: function(aTask) { + let data = aTask.data; + let lock = this.lockInfo[data.lockID]; + let keys = Object.getOwnPropertyNames(data.settings); + + if (!lock) { + return Promise.reject({task: aTask, error: "Lock died, can't finalize"}); + } + if (lock._failed) { + if (DEBUG) debug("Lock failed. All subsequent requests will fail."); + return Promise.reject({task: aTask, error: "Lock failed a permissions check, all requests now failing."}); + } + + if (lock.hasCleared) { + if (DEBUG) debug("Lock was used for a clear command. All subsequent requests will fail."); + return Promise.reject({task: aTask, error: "Lock was used for a clear command. All other requests will fail."}); + } + + lock.canClear = false; + + // If we have no keys, resolve + if (keys.length === 0) { + if (DEBUG) debug("No keys to change entered!"); + return Promise.resolve({task: aTask}); + } + + for (let i = 0; i < keys.length; i++) { + if (!SettingsPermissions.hasWritePermission(lock.principal, keys[i])) { + if (DEBUG) debug("set not allowed on " + keys[i]); + lock._failed = true; + return Promise.reject({task: aTask, error: "No permission to set " + keys[i]}); + } + } + + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + if (VERBOSE) debug("key: " + key + ", val: " + JSON.stringify(data.settings[key]) + ", type: " + typeof(data.settings[key])); + lock.queuedSets[key] = data.settings[key]; + } + + return this.queueTaskReturn(aTask, {task: aTask}); + }, + + startRunning: function(aLockID) { + let lock = this.lockInfo[aLockID]; + + if (!lock) { + if (DEBUG) debug("Lock no longer alive, cannot start running"); + return; + } + + lock.consumable = true; + if (aLockID == this.settingsLockQueue[0] || this.settingsLockQueue.length == 0) { + // If a lock is currently at the head of the queue, run all tasks for + // it. + if (VERBOSE) debug("Start running tasks for " + aLockID); + this.queueConsume(); + } else { + // If a lock isn't at the head of the queue, but requests to be run, + // simply mark it as consumable, which means it will automatically run + // once it comes to the head of the queue. + if (VERBOSE) debug("Queuing tasks for " + aLockID + " while waiting for " + this.settingsLockQueue[0]); + } + }, + + queueConsume: function() { + if (this.settingsLockQueue.length > 0 && this.lockInfo[this.settingsLockQueue[0]].consumable) { + Services.tm.currentThread.dispatch(SettingsRequestManager.consumeTasks.bind(this), Ci.nsIThread.DISPATCH_NORMAL); + } + }, + + finalizeSets: function(aTask) { + let data = aTask.data; + if (VERBOSE) debug("Finalizing tasks for lock " + data.lockID); + let lock = this.lockInfo[data.lockID]; + + if (!lock) { + return Promise.reject({task: aTask, error: "Lock died, can't finalize"}); + } + lock.finalizing = true; + if (lock._failed) { + this.removeLock(data.lockID); + return Promise.reject({task: aTask, error: "Lock failed a permissions check, all requests now failing."}); + } + // If we have cleared, there is no reason to continue finalizing + // this lock. Just resolve promise with task and move on. + if (lock.hasCleared) { + if (VERBOSE) debug("Clear was called on lock, skipping finalize"); + this.removeLock(data.lockID); + return Promise.resolve({task: aTask}); + } + let keys = Object.getOwnPropertyNames(lock.queuedSets); + if (keys.length === 0) { + if (VERBOSE) debug("Nothing to finalize. Exiting."); + this.removeLock(data.lockID); + return Promise.resolve({task: aTask}); + } + + let store = lock.getObjectStore(lock.principal); + if (!store) { + if (DEBUG) debug("Rejecting Set task on lock " + aTask.data.lockID); + this.removeLock(data.lockID); + return Promise.reject({task: aTask, error: "Cannot get object store"}); + } + + // Due to the fact there may have multiple set operations to clear, and + // they're all async, callbacks are gathered into promises, and the promises + // are processed with Promises.all(). + let checkPromises = []; + let finalValues = {}; + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + if (VERBOSE) debug("key: " + key + ", val: " + lock.queuedSets[key] + ", type: " + typeof(lock.queuedSets[key])); + let checkDefer = {}; + let checkPromise = new Promise(function(resolve, reject) { + checkDefer.resolve = resolve; + checkDefer.reject = reject; + }); + + // Get operation is used to fill in the default value, assuming there is + // one. For the moment, if a value doesn't exist in the settings DB, we + // allow the user to add it, and just pass back a null default value. + let checkKeyRequest = store.get(key); + checkKeyRequest.onsuccess = function (event) { + let userValue = lock.queuedSets[key]; + let defaultValue; + if (!event.target.result) { + defaultValue = null; + if (VERBOSE) debug("MOZSETTINGS-GET-WARNING: " + key + " is not in the database.\n"); + } else { + defaultValue = event.target.result.defaultValue; + } + let obj = {settingName: key, defaultValue: defaultValue, userValue: userValue}; + finalValues[key] = {defaultValue: defaultValue, userValue: userValue}; + let setReq = store.put(obj); + setReq.onsuccess = function() { + if (VERBOSE) debug("Set successful!"); + if (VERBOSE) debug("key: " + key + ", val: " + finalValues[key] + ", type: " + typeof(finalValues[key])); + return checkDefer.resolve({task: aTask}); + }; + setReq.onerror = function() { + return checkDefer.reject({task: aTask, error: setReq.error.name}); + }; + }.bind(this); + checkKeyRequest.onerror = function(event) { + return checkDefer.reject({task: aTask, error: checkKeyRequest.error.name}); + }; + checkPromises.push(checkPromise); + } + + let defer = {}; + let promiseWrapper = new Promise(function(resolve, reject) { + defer.resolve = resolve; + defer.reject = reject; + }); + + // Once all transactions are done, or any have failed, remove the lock and + // start processing the tasks from the next lock in the queue. + Promise.all(checkPromises).then(function() { + // If all commits were successful, notify observers + for (let i = 0; i < keys.length; i++) { + this.sendSettingsChange(keys[i], finalValues[keys[i]].userValue, lock.isServiceLock); + } + this.removeLock(data.lockID); + defer.resolve({task: aTask}); + }.bind(this), function(ret) { + this.removeLock(data.lockID); + defer.reject({task: aTask, error: "Set transaction failure"}); + }.bind(this)); + return promiseWrapper; + }, + + // Clear is only expected to be called via tests, and if a lock + // calls clear, it should be the only thing the lock does. This + // allows us to not have to deal with the possibility of query + // integrity checking. Clear should never be called in the wild, + // even by certified apps, which is why it has its own permission + // (settings-clear). + taskClear: function(aTask) { + if (VERBOSE) debug("Clearing"); + let data = aTask.data; + let lock = this.lockInfo[data.lockID]; + + if (lock._failed) { + if (DEBUG) debug("Lock failed, all requests now failing."); + return Promise.reject({task: aTask, error: "Lock failed, all requests now failing."}); + } + + if (!lock.canClear) { + if (DEBUG) debug("Lock tried to clear after queuing other tasks. Failing."); + lock._failed = true; + return Promise.reject({task: aTask, error: "Cannot call clear after queuing other tasks, all requests now failing."}); + } + + if (!SettingsPermissions.hasClearPermission(lock.principal)) { + if (DEBUG) debug("clear not allowed"); + lock._failed = true; + return Promise.reject({task: aTask, error: "No permission to clear DB"}); + } + + lock.hasCleared = true; + + let store = lock.getObjectStore(lock.principal); + if (!store) { + if (DEBUG) debug("Rejecting Clear task on lock " + aTask.data.lockID); + return Promise.reject({task: aTask, error: "Cannot get object store"}); + } + let defer = {}; + let promiseWrapper = new Promise(function(resolve, reject) { + defer.resolve = resolve; + defer.reject = reject; + }); + + let clearReq = store.clear(); + clearReq.onsuccess = function() { + return defer.resolve({task: aTask}); + }; + clearReq.onerror = function() { + return defer.reject({task: aTask}); + }; + return promiseWrapper; + }, + + ensureConnection : function() { + if (VERBOSE) debug("Ensuring Connection"); + let defer = {}; + let promiseWrapper = new Promise(function(resolve, reject) { + defer.resolve = resolve; + defer.reject = reject; + }); + this.settingsDB.ensureDB( + function() { defer.resolve(); }, + function(error) { + if (DEBUG) debug("Cannot open Settings DB. Trying to open an old version?\n"); + defer.reject(error); + } + ); + return promiseWrapper; + }, + + runTasks: function(aLockID) { + if (VERBOSE) debug("Running tasks for " + aLockID); + let lock = this.lockInfo[aLockID]; + if (!lock) { + if (DEBUG) debug("Lock no longer alive, cannot run tasks"); + return; + } + let currentTask = lock.tasks.shift(); + let promises = []; + if (TRACK) { + if (this.tasksConsumed[aLockID] === undefined) { + this.tasksConsumed[aLockID] = 0; + this.tasksGetConsumed[aLockID] = 0; + this.tasksSetConsumed[aLockID] = 0; + } + } + while (currentTask) { + if (VERBOSE) debug("Running Operation " + currentTask.operation); + if (lock.finalizing) { + // We should really never get to this point, but if we do, + // fail every task that happens. + Cu.reportError("Settings lock trying to run more tasks after finalizing. Ignoring tasks, but this is bad. Lock: " + aLockID); + currentTask.defer.reject("Cannot call new task after finalizing"); + } else { + let p; + this.totalProcessed++; + if (TRACK) { + this.tasksConsumed[aLockID]++; + } + switch (currentTask.operation) { + case "get": + this.totalGetProcessed++; + if (TRACK) { + this.tasksGetConsumed[aLockID]++; + } + p = this.taskGet(currentTask); + break; + case "set": + this.totalSetProcessed++; + if (TRACK) { + this.tasksSetConsumed[aLockID]++; + } + p = this.taskSet(currentTask); + break; + case "clear": + p = this.taskClear(currentTask); + break; + case "finalize": + p = this.finalizeSets(currentTask); + break; + default: + if (DEBUG) debug("Invalid operation: " + currentTask.operation); + p.reject("Invalid operation: " + currentTask.operation); + } + p.then(function(ret) { + ret.task.defer.resolve("results" in ret ? ret.results : null); + }.bind(currentTask), function(ret) { + ret.task.defer.reject(ret.error); + }); + promises.push(p); + } + currentTask = lock.tasks.shift(); + } + }, + + consumeTasks: function() { + if (this.settingsLockQueue.length == 0) { + if (VERBOSE) debug("Nothing to run!"); + return; + } + + let lockID = this.settingsLockQueue[0]; + if (VERBOSE) debug("Consuming tasks for " + lockID); + let lock = this.lockInfo[lockID]; + + // If a process dies, we should clean up after it via the + // child-process-shutdown event. But just in case we don't, we want to make + // sure we never block on consuming. + if (!lock) { + if (DEBUG) debug("Lock no longer alive, cannot consume tasks"); + this.queueConsume(); + return; + } + + if (!lock.consumable || lock.tasks.length === 0) { + if (VERBOSE) debug("No more tasks to run or not yet consuamble."); + return; + } + + lock.consumable = false; + this.ensureConnection().then( + function(task) { + this.runTasks(lockID); + this.updateSoftLockup(lockID); + }.bind(this), function(ret) { + dump("-*- SettingsRequestManager: SETTINGS DATABASE ERROR: Cannot make DB connection!\n"); + }); + }, + + observe: function(aSubject, aTopic, aData) { + if (VERBOSE) debug("observe: " + aTopic); + switch (aTopic) { + case kXpcomShutdownObserverTopic: + this.messages.forEach((function(msgName) { + ppmm.removeMessageListener(msgName, this); + }).bind(this)); + Services.obs.removeObserver(this, kXpcomShutdownObserverTopic); + ppmm = null; + mrm.unregisterStrongReporter(this); + break; + + case kInnerWindowDestroyed: + let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; + this.forceFinalizeChildLocksNonOOP(wId); + break; + + default: + if (DEBUG) debug("Wrong observer topic: " + aTopic); + break; + } + }, + + collectReports: function(aCallback, aData, aAnonymize) { + for (let lockId of Object.keys(this.lockInfo)) { + let lock = this.lockInfo[lockId]; + let length = lock.tasks.length; + + if (length === 0) { + continue; + } + + let path = "settings-locks/tasks/lock(id=" + lockId + ")/"; + + aCallback.callback("", path + "alive", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + length, + "Alive tasks for this lock", + aData); + } + + aCallback.callback("", + "settings-locks/tasks-total/processed", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this.totalProcessed, + "The total number of tasks that were executed.", + aData); + + aCallback.callback("", + "settings-locks/tasks-total/set", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this.totalSetProcessed, + "The total number of set tasks that were executed.", + aData); + + aCallback.callback("", + "settings-locks/tasks-total/get", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this.totalGetProcessed, + "The total number of get tasks that were executed.", + aData); + + // if TRACK is not enabled, then, no details are available + if (!TRACK) { + return; + } + + for (let lockId of Object.keys(this.tasksConsumed)) { + let lock = this.lockInfo[lockId]; + let length = 0; + if (lock) { + length = lock.tasks.length; + } + + let path = "settings-locks/tasks/lock(id=" + lockId + ")/"; + + aCallback.callback("", path + "set", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this.tasksSetConsumed[lockId], + "Set tasks for this lock.", + aData); + + aCallback.callback("", path + "get", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this.tasksGetConsumed[lockId], + "Get tasks for this lock.", + aData); + + aCallback.callback("", path + "processed", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this.tasksConsumed[lockId], + "Number of tasks that were executed.", + aData); + } + }, + + sendSettingsChange: function(aKey, aValue, aIsServiceLock) { + this.broadcastMessage("Settings:Change:Return:OK", + { key: aKey, value: aValue }); + var setting = { + key: aKey, + value: aValue, + isInternalChange: aIsServiceLock + }; + setting.wrappedJSObject = setting; + Services.obs.notifyObservers(setting, kMozSettingsChangedObserverTopic, ""); + }, + + broadcastMessage: function broadcastMessage(aMsgName, aContent) { + if (VERBOSE) debug("Broadcast"); + this.children.forEach(function(msgMgr) { + let principal = this.observerPrincipalCache.get(msgMgr); + if (!principal) { + if (DEBUG) debug("Cannot find principal for message manager to check permissions"); + } + else if (SettingsPermissions.hasReadPermission(principal, aContent.key)) { + try { + msgMgr.sendAsyncMessage(aMsgName, aContent); + } catch (e) { + if (DEBUG) debug("Failed sending message: " + aMsgName); + } + } + }.bind(this)); + if (VERBOSE) debug("Finished Broadcasting"); + }, + + addObserver: function(aMsgMgr, aPrincipal) { + if (VERBOSE) debug("Add observer for " + aPrincipal.origin); + if (this.children.indexOf(aMsgMgr) == -1) { + this.children.push(aMsgMgr); + this.observerPrincipalCache.set(aMsgMgr, aPrincipal); + } + }, + + removeObserver: function(aMsgMgr) { + if (VERBOSE) { + let principal = this.observerPrincipalCache.get(aMsgMgr); + if (principal) { + debug("Remove observer for " + principal.origin); + } + } + let index = this.children.indexOf(aMsgMgr); + if (index != -1) { + this.children.splice(index, 1); + this.observerPrincipalCache.delete(aMsgMgr); + } + if (VERBOSE) debug("Principal/MessageManager pairs left in observer cache: " + this.observerPrincipalCache.size); + }, + + removeLock: function(aLockID) { + if (VERBOSE) debug("Removing lock " + aLockID); + if (this.lockInfo[aLockID]) { + let transaction = this.lockInfo[aLockID]._transaction; + if (transaction) { + try { + transaction.abort(); + } catch (e) { + if (e.name == "InvalidStateError") { + if (VERBOSE) debug("Transaction for " + aLockID + " closed already"); + } else { + if (DEBUG) debug("Unexpected exception, throwing: " + e); + throw e; + } + } + } + delete this.lockInfo[aLockID]; + } + let index = this.settingsLockQueue.indexOf(aLockID); + if (index > -1) { + this.settingsLockQueue.splice(index, 1); + } + // If index is 0, the lock we just removed was at the head of + // the queue, so possibly queue the next lock if it's + // consumable. + if (index == 0) { + this.queueConsume(); + } + }, + + hasLockFinalizeTask: function(lock) { + // Go in reverse order because finalize should be the last one + for (let task_index = lock.tasks.length; task_index >= 0; task_index--) { + if (lock.tasks[task_index] + && lock.tasks[task_index].operation === "finalize") { + return true; + } + } + return false; + }, + + enqueueForceFinalize: function(lock) { + if (!this.hasLockFinalizeTask(lock)) { + if (VERBOSE) debug("Alive lock has pending tasks: " + lock.lockID); + this.queueTask("finalize", {lockID: lock.lockID}).then( + function() { + if (VERBOSE) debug("Alive lock " + lock.lockID + " succeeded to force-finalize"); + }, + function(error) { + if (DEBUG) debug("Alive lock " + lock.lockID + " failed to force-finalize due to error: " + error); + } + ); + // Finalize is considered a task running situation, but it also needs to + // queue a task. + this.startRunning(lock.lockID); + } + }, + + forceFinalizeChildLocksNonOOP: function(windowId) { + if (VERBOSE) debug("Forcing finalize on child locks, non OOP"); + + for (let lockId of Object.keys(this.lockInfo)) { + let lock = this.lockInfo[lockId]; + if (lock.windowID === windowId) { + this.enqueueForceFinalize(lock); + } + } + }, + + forceFinalizeChildLocksOOP: function(aMsgMgr) { + if (VERBOSE) debug("Forcing finalize on child locks, OOP"); + + for (let lockId of Object.keys(this.lockInfo)) { + let lock = this.lockInfo[lockId]; + if (lock._mm === aMsgMgr) { + this.enqueueForceFinalize(lock); + } + } + }, + + updateSoftLockup: function(aLockId) { + if (VERBOSE) debug("Treating lock " + aLockId + ", so updating soft lockup infos ..."); + + this.softLockup = { + lockId: aLockId, + lockTs: new Date() + }; + }, + + checkSoftLockup: function() { + if (VERBOSE) debug("Checking for soft lockup ..."); + + if (this.settingsLockQueue.length === 0) { + if (VERBOSE) debug("Empty settings lock queue, no soft lockup ..."); + return; + } + + let head = this.settingsLockQueue[0]; + if (head !== this.softLockup.lockId) { + if (VERBOSE) debug("Non matching head of settings lock queue, no soft lockup ..."); + return; + } + + let delta = (new Date() - this.softLockup.lockTs) / 1000; + if (delta < kSoftLockupDelta) { + if (VERBOSE) debug("Matching head of settings lock queue, but delta (" + delta + ") < 30 secs, no soft lockup ..."); + return; + } + + let msgBlocked = "Settings queue head blocked at " + head + + " for " + delta + " secs, Settings API may be soft lockup. Lock from: " + + this.lockInfo[head].lockStack; + Cu.reportError(msgBlocked); + if (DEBUG) debug(msgBlocked); + }, + + receiveMessage: function(aMessage) { + if (VERBOSE) debug("receiveMessage " + aMessage.name + ": " + JSON.stringify(aMessage.data)); + + let msg = aMessage.data; + let mm = aMessage.target; + + function returnMessage(name, data) { + if (mm) { + try { + mm.sendAsyncMessage(name, data); + } catch (e) { + if (DEBUG) debug("Return message failed, " + name + ": " + e); + } + } else { + try { + gSettingsService.receiveMessage({ name: name, data: data }); + } catch (e) { + if (DEBUG) debug("Direct return message failed, " + name + ": " + e); + } + } + } + + // For all message types that expect a lockID, we check to make + // sure that we're accessing a lock that's part of our process. If + // not, consider it a security violation and kill the app. Killing + // based on creating a colliding lock ID happens as part of + // CreateLock check below. + switch (aMessage.name) { + case "Settings:Get": + case "Settings:Set": + case "Settings:Clear": + case "Settings:Run": + case "Settings:Finalize": + this.checkSoftLockup(); + let kill_process = false; + if (!msg.lockID) { + Cu.reportError("Process sending request for lock that does not exist. Killing."); + kill_process = true; + } + else if (!this.lockInfo[msg.lockID]) { + if (DEBUG) debug("Cannot find lock ID " + msg.lockID); + // This doesn't kill, because we can have things that file + // finalize, then die, and we may get the observer + // notification before we get the IPC messages. + return; + } + else if (mm != this.lockInfo[msg.lockID]._mm) { + Cu.reportError("Process trying to access settings lock from another process. Killing."); + kill_process = true; + } + if (kill_process) { + // Kill the app by checking for a non-existent permission + aMessage.target.assertPermission("message-manager-mismatch-kill"); + return; + } + default: + break; + } + + switch (aMessage.name) { + case "child-process-shutdown": + if (VERBOSE) debug("Child process shutdown received."); + this.forceFinalizeChildLocksOOP(mm); + this.removeObserver(mm); + break; + case "Settings:RegisterForMessages": + if (!SettingsPermissions.hasSomeReadPermission(aMessage.principal)) { + Cu.reportError("Settings message " + aMessage.name + + " from a content process with no 'settings-api-read' privileges."); + aMessage.target.assertPermission("message-manager-no-read-kill"); + return; + } + this.addObserver(mm, aMessage.principal); + break; + case "Settings:UnregisterForMessages": + this.removeObserver(mm); + break; + case "Settings:CreateLock": + if (VERBOSE) debug("Received CreateLock for " + msg.lockID + " from " + aMessage.principal.origin + " window: " + msg.windowID); + // If we try to create a lock ID that collides with one + // already in the system, consider it a security violation and + // kill. + if (msg.lockID in this.settingsLockQueue) { + Cu.reportError("Trying to queue a lock with the same ID as an already queued lock. Killing app."); + aMessage.target.assertPermission("lock-id-duplicate-kill"); + return; + } + + if (this.softLockup.lockId === null) { + this.updateSoftLockup(msg.lockID); + } + + this.settingsLockQueue.push(msg.lockID); + this.lockInfo[msg.lockID] = SettingsLockInfo(this.settingsDB, + mm, + aMessage.principal, + msg.lockID, + msg.isServiceLock, + msg.windowID, + msg.lockStack); + break; + case "Settings:Get": + if (VERBOSE) debug("Received getRequest from " + msg.lockID); + this.queueTask("get", msg).then(function(settings) { + returnMessage("Settings:Get:OK", { + lockID: msg.lockID, + requestID: msg.requestID, + settings: settings + }); + }, function(error) { + if (DEBUG) debug("getRequest FAILED " + msg.name); + returnMessage("Settings:Get:KO", { + lockID: msg.lockID, + requestID: msg.requestID, + errorMsg: error + }); + }); + break; + case "Settings:Set": + if (VERBOSE) debug("Received Set Request from " + msg.lockID); + this.queueTask("set", msg).then(function(settings) { + returnMessage("Settings:Set:OK", { + lockID: msg.lockID, + requestID: msg.requestID + }); + }, function(error) { + returnMessage("Settings:Set:KO", { + lockID: msg.lockID, + requestID: msg.requestID, + errorMsg: error + }); + }); + break; + case "Settings:Clear": + if (VERBOSE) debug("Received Clear Request from " + msg.lockID); + this.queueTask("clear", msg).then(function() { + returnMessage("Settings:Clear:OK", { + lockID: msg.lockID, + requestID: msg.requestID + }); + }, function(error) { + returnMessage("Settings:Clear:KO", { + lockID: msg.lockID, + requestID: msg.requestID, + errorMsg: error + }); + }); + break; + case "Settings:Finalize": + if (VERBOSE) debug("Received Finalize"); + this.queueTask("finalize", msg).then(function() { + returnMessage("Settings:Finalize:OK", { + lockID: msg.lockID + }); + }, function(error) { + returnMessage("Settings:Finalize:KO", { + lockID: msg.lockID, + errorMsg: error + }); + }); + // YES THIS IS SUPPOSED TO FALL THROUGH. Finalize is considered a task + // running situation, but it also needs to queue a task. + case "Settings:Run": + if (VERBOSE) debug("Received Run"); + this.startRunning(msg.lockID); + break; + default: + if (DEBUG) debug("Wrong message: " + aMessage.name); + } + } +}; + +// This code should ALWAYS be living only on the parent side. +if (!inParent) { + debug("SettingsRequestManager should be living on parent side."); + throw Cr.NS_ERROR_ABORT; +} else { + this.SettingsRequestManager = SettingsRequestManager; + SettingsRequestManager.init(); +} diff --git a/dom/settings/SettingsService.js b/dom/settings/SettingsService.js new file mode 100644 index 000000000..09bd3ca72 --- /dev/null +++ b/dom/settings/SettingsService.js @@ -0,0 +1,358 @@ +/* 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 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/SettingsRequestManager.jsm'); + +/* static functions */ +var DEBUG = false; +var VERBOSE = false; + +try { + DEBUG = + Services.prefs.getBoolPref("dom.mozSettings.SettingsService.debug.enabled"); + VERBOSE = + Services.prefs.getBoolPref("dom.mozSettings.SettingsService.verbose.enabled"); +} catch (ex) { } + +function debug(s) { + dump("-*- SettingsService: " + s + "\n"); +} + +XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsIMessageSender"); +XPCOMUtils.defineLazyServiceGetter(this, "mrm", + "@mozilla.org/memory-reporter-manager;1", + "nsIMemoryReporterManager"); + +const nsIClassInfo = Ci.nsIClassInfo; +const kXpcomShutdownObserverTopic = "xpcom-shutdown"; + +const SETTINGSSERVICELOCK_CONTRACTID = "@mozilla.org/settingsServiceLock;1"; +const SETTINGSSERVICELOCK_CID = Components.ID("{d7a395a0-e292-11e1-834e-1761d57f5f99}"); +const nsISettingsServiceLock = Ci.nsISettingsServiceLock; + +function makeSettingsServiceRequest(aCallback, aName, aValue) { + return { + callback: aCallback, + name: aName, + value: aValue + }; +}; + +const kLockListeners = ["Settings:Get:OK", "Settings:Get:KO", + "Settings:Clear:OK", "Settings:Clear:KO", + "Settings:Set:OK", "Settings:Set:KO", + "Settings:Finalize:OK", "Settings:Finalize:KO"]; + +function SettingsServiceLock(aSettingsService, aTransactionCallback) { + if (VERBOSE) debug("settingsServiceLock constr!"); + this._open = true; + this._settingsService = aSettingsService; + this._id = uuidgen.generateUUID().toString(); + this._transactionCallback = aTransactionCallback; + this._requests = {}; + let closeHelper = function() { + if (VERBOSE) debug("closing lock " + this._id); + this._open = false; + this.runOrFinalizeQueries(); + }.bind(this); + + this.addListeners(); + + let createLockPayload = { + lockID: this._id, + isServiceLock: true, + windowID: undefined, + lockStack: (new Error).stack + }; + + this.returnMessage("Settings:CreateLock", createLockPayload); + Services.tm.currentThread.dispatch(closeHelper, Ci.nsIThread.DISPATCH_NORMAL); +} + +SettingsServiceLock.prototype = { + get closed() { + return !this._open; + }, + + addListeners: function() { + for (let msg of kLockListeners) { + cpmm.addMessageListener(msg, this); + } + }, + + removeListeners: function() { + for (let msg of kLockListeners) { + cpmm.removeMessageListener(msg, this); + } + }, + + returnMessage: function(aMessage, aData) { + SettingsRequestManager.receiveMessage({ + name: aMessage, + data: aData, + target: undefined, + principal: Services.scriptSecurityManager.getSystemPrincipal() + }); + }, + + runOrFinalizeQueries: function() { + if (!this._requests || Object.keys(this._requests).length == 0) { + this.returnMessage("Settings:Finalize", {lockID: this._id}); + } else { + this.returnMessage("Settings:Run", {lockID: this._id}); + } + }, + + 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. + if (!msg.requestID) { + switch (aMessage.name) { + case "Settings:Finalize:OK": + if (VERBOSE) debug("Lock finalize ok!"); + this.callTransactionHandle(); + break; + case "Settings:Finalize:KO": + if (DEBUG) debug("Lock finalize failed!"); + this.callAbort(); + break; + default: + if (DEBUG) debug("Message type " + aMessage.name + " is missing a requestID"); + } + + this._settingsService.unregisterLock(this._id); + return; + } + + let req = this._requests[msg.requestID]; + if (!req) { + if (DEBUG) debug("Matching request not found."); + return; + } + delete this._requests[msg.requestID]; + switch (aMessage.name) { + case "Settings:Get:OK": + this._open = true; + let settings_names = Object.keys(msg.settings); + if (settings_names.length > 0) { + let name = settings_names[0]; + if (DEBUG && settings_names.length > 1) { + debug("Warning: overloaded setting:" + name); + } + let result = msg.settings[name]; + this.callHandle(req.callback, name, result); + } else { + this.callHandle(req.callback, req.name, null); + } + this._open = false; + break; + case "Settings:Set:OK": + this._open = true; + // We don't pass values back from sets in SettingsManager... + this.callHandle(req.callback, req.name, req.value); + this._open = false; + break; + case "Settings:Get:KO": + case "Settings:Set:KO": + if (DEBUG) debug("error:" + msg.errorMsg); + this.callError(req.callback, msg.error); + break; + default: + if (DEBUG) debug("Wrong message: " + aMessage.name); + } + this.runOrFinalizeQueries(); + }, + + get: function get(aName, aCallback) { + if (VERBOSE) debug("get (" + this._id + "): " + aName); + if (!this._open) { + if (DEBUG) debug("Settings lock not open!\n"); + throw Components.results.NS_ERROR_ABORT; + } + let reqID = uuidgen.generateUUID().toString(); + this._requests[reqID] = makeSettingsServiceRequest(aCallback, aName); + this.returnMessage("Settings:Get", {requestID: reqID, + lockID: this._id, + name: aName}); + }, + + set: function set(aName, aValue, aCallback) { + if (VERBOSE) debug("set: " + aName + " " + aValue); + if (!this._open) { + throw "Settings lock not open"; + } + let reqID = uuidgen.generateUUID().toString(); + this._requests[reqID] = makeSettingsServiceRequest(aCallback, aName, aValue); + let settings = {}; + settings[aName] = aValue; + this.returnMessage("Settings:Set", {requestID: reqID, + lockID: this._id, + settings: settings}); + }, + + callHandle: function callHandle(aCallback, aName, aValue) { + try { + aCallback && aCallback.handle ? aCallback.handle(aName, aValue) : null; + } catch (e) { + if (DEBUG) debug("settings 'handle' for " + aName + " callback threw an exception, dropping: " + e + "\n"); + } + }, + + callAbort: function callAbort(aCallback, aMessage) { + try { + aCallback && aCallback.handleAbort ? aCallback.handleAbort(aMessage) : null; + } catch (e) { + if (DEBUG) debug("settings 'abort' callback threw an exception, dropping: " + e + "\n"); + } + }, + + callError: function callError(aCallback, aMessage) { + try { + aCallback && aCallback.handleError ? aCallback.handleError(aMessage) : null; + } catch (e) { + if (DEBUG) debug("settings 'error' callback threw an exception, dropping: " + e + "\n"); + } + }, + + callTransactionHandle: function callTransactionHandle() { + try { + this._transactionCallback && this._transactionCallback.handle ? this._transactionCallback.handle() : null; + } catch (e) { + if (DEBUG) debug("settings 'Transaction handle' callback threw an exception, dropping: " + e + "\n"); + } + }, + + classID : SETTINGSSERVICELOCK_CID, + QueryInterface : XPCOMUtils.generateQI([nsISettingsServiceLock]) +}; + +const SETTINGSSERVICE_CID = Components.ID("{f656f0c0-f776-11e1-a21f-0800200c9a66}"); + +function SettingsService() +{ + if (VERBOSE) debug("settingsService Constructor"); + this._locks = []; + this._serviceLocks = {}; + this._createdLocks = 0; + this._unregisteredLocks = 0; + this.init(); +} + +SettingsService.prototype = { + + init: function() { + Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false); + mrm.registerStrongReporter(this); + }, + + uninit: function() { + Services.obs.removeObserver(this, kXpcomShutdownObserverTopic); + mrm.unregisterStrongReporter(this); + }, + + observe: function(aSubject, aTopic, aData) { + if (VERBOSE) debug("observe: " + aTopic); + if (aTopic === kXpcomShutdownObserverTopic) { + this.uninit(); + } + }, + + receiveMessage: function(aMessage) { + if (VERBOSE) debug("Entering receiveMessage"); + + let lockID = aMessage.data.lockID; + if (!lockID) { + if (DEBUG) debug("No lock ID"); + return; + } + + if (!(lockID in this._serviceLocks)) { + if (DEBUG) debug("Received message for lock " + lockID + " but no lock"); + return; + } + + if (VERBOSE) debug("Delivering message"); + this._serviceLocks[lockID].receiveMessage(aMessage); + }, + + createLock: function createLock(aCallback) { + if (VERBOSE) debug("Calling createLock"); + var lock = new SettingsServiceLock(this, aCallback); + if (VERBOSE) debug("Created lock " + lock._id); + this.registerLock(lock); + return lock; + }, + + registerLock: function(aLock) { + if (VERBOSE) debug("Registering lock " + aLock._id); + this._locks.push(aLock._id); + this._serviceLocks[aLock._id] = aLock; + this._createdLocks++; + }, + + 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._serviceLocks[aLockID].removeListeners(); + this._serviceLocks[aLockID] = null; + delete this._serviceLocks[aLockID]; + this._unregisteredLocks++; + } + }, + + collectReports: function(aCallback, aData, aAnonymize) { + aCallback.callback("", + "settings-service-locks/alive", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this._locks.length, + "The number of service locks that are currently alives.", + aData); + + aCallback.callback("", + "settings-service-locks/created", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this._createdLocks, + "The number of service locks that were created.", + aData); + + aCallback.callback("", + "settings-service-locks/deleted", + Ci.nsIMemoryReporter.KIND_OTHER, + Ci.nsIMemoryReporter.UNITS_COUNT, + this._unregisteredLocks, + "The number of service locks that were deleted.", + aData); + }, + + classID : SETTINGSSERVICE_CID, + QueryInterface : XPCOMUtils.generateQI([Ci.nsISettingsService, + Ci.nsIObserver, + Ci.nsIMemoryReporter]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsService, SettingsServiceLock]); diff --git a/dom/settings/SettingsService.manifest b/dom/settings/SettingsService.manifest new file mode 100644 index 000000000..ae464f921 --- /dev/null +++ b/dom/settings/SettingsService.manifest @@ -0,0 +1,5 @@ +component {d7a395a0-e292-11e1-834e-1761d57f5f99} SettingsService.js +contract @mozilla.org/settingsServiceLock;1 {d7a395a0-e292-11e1-834e-1761d57f5f99} + +component {f656f0c0-f776-11e1-a21f-0800200c9a66} SettingsService.js +contract @mozilla.org/settingsService;1 {f656f0c0-f776-11e1-a21f-0800200c9a66} diff --git a/dom/settings/moz.build b/dom/settings/moz.build new file mode 100644 index 000000000..426edcbce --- /dev/null +++ b/dom/settings/moz.build @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXTRA_COMPONENTS += [ + 'SettingsManager.manifest' +] + +EXTRA_PP_COMPONENTS += [ + 'SettingsManager.js' +] + +if CONFIG['MOZ_B2G']: + EXTRA_COMPONENTS += [ + 'SettingsService.js', + 'SettingsService.manifest', + ] + +EXTRA_JS_MODULES += [ + 'SettingsDB.jsm', + 'SettingsRequestManager.jsm' +] + +MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini'] + +XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini'] diff --git a/dom/settings/tests/chrome.ini b/dom/settings/tests/chrome.ini new file mode 100644 index 000000000..92b1554a0 --- /dev/null +++ b/dom/settings/tests/chrome.ini @@ -0,0 +1,24 @@ +[DEFAULT] +skip-if = toolkit == 'android' # Bug 1287455: takes too long to complete on Android +support-files = + file_loadserver.js + file_bug1110872.js + file_bug1110872.html + test_settings_service.js + test_settings_service_callback.js + +[test_settings_service.xul] +run-if = buildapp == 'b2g' || buildapp == 'mulet' +[test_settings_service_callback.xul] +run-if = buildapp == 'b2g' || buildapp == 'mulet' +[test_settings_basics.html] +[test_settings_permissions.html] +[test_settings_blobs.html] +[test_settings_data_uris.html] +[test_settings_events.html] +[test_settings_navigator_object.html] +[test_settings_onsettingchange.html] +[test_settings_bug1110872.html] +skip-if = !e10s +[test_settings_observer_killer.html] +skip-if = !debug diff --git a/dom/settings/tests/file_bug1110872.html b/dom/settings/tests/file_bug1110872.html new file mode 100644 index 000000000..3dcc45b82 --- /dev/null +++ b/dom/settings/tests/file_bug1110872.html @@ -0,0 +1,47 @@ + + + + Test for Bug {1110872} Settings API Reloads + + + + Mozilla Bug {1110872} Inner Window for Reload Test +

+ + + diff --git a/dom/settings/tests/file_bug1110872.js b/dom/settings/tests/file_bug1110872.js new file mode 100644 index 000000000..d31b6c2f3 --- /dev/null +++ b/dom/settings/tests/file_bug1110872.js @@ -0,0 +1,47 @@ +"use strict"; + +SimpleTest.waitForExplicitFinish(); + +var iframe; +var loadedEvents = 0; + +function loadServer() { + var url = SimpleTest.getTestFileURL("file_loadserver.js"); + var script = SpecialPowers.loadChromeScript(url); +} + +function runTest() { + iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.addEventListener('load', mozbrowserLoaded); + iframe.src = 'file_bug1110872.html'; +} + +function iframeBodyRecv(msg) { + switch (loadedEvents) { + case 1: + // If we get a message back before we've seen 2 loads, that means + // something went wrong with the test. Fail immediately. + ok(true, 'got response from first test!'); + break; + case 2: + // If we get a message back after 2 loads (initial load, reload), + // it means the callback for the last lock fired, which means the + // SettingsRequestManager queue has to have been cleared + // correctly. + ok(true, 'further queries returned ok after SettingsManager death'); + SimpleTest.finish(); + break; + } +} + +function mozbrowserLoaded() { + loadedEvents++; + iframe.contentWindow.postMessage({name: "start", step: loadedEvents}, '*'); + window.addEventListener('message', iframeBodyRecv); +} + +window.addEventListener("load", function() { + loadServer(); + runTest(); +}); diff --git a/dom/settings/tests/file_loadserver.js b/dom/settings/tests/file_loadserver.js new file mode 100644 index 000000000..a919d9690 --- /dev/null +++ b/dom/settings/tests/file_loadserver.js @@ -0,0 +1,17 @@ +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cu = Components.utils; + +// Stolen from SpecialPowers, since at this point we don't know we're in a test. +var isMainProcess = function() { + try { + return Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULRuntime). + processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + } catch (e) { } + return true; +}; + +if (isMainProcess()) { + Components.utils.import("resource://gre/modules/SettingsRequestManager.jsm"); +} diff --git a/dom/settings/tests/test_settings_basics.html b/dom/settings/tests/test_settings_basics.html new file mode 100644 index 000000000..a14650390 --- /dev/null +++ b/dom/settings/tests/test_settings_basics.html @@ -0,0 +1,816 @@ + + + + + Test for Bug {678695} Settings API + + + + + + +Mozilla Bug {678695} +

+ +
+
+
+ + diff --git a/dom/settings/tests/test_settings_blobs.html b/dom/settings/tests/test_settings_blobs.html new file mode 100644 index 000000000..6d24111cf --- /dev/null +++ b/dom/settings/tests/test_settings_blobs.html @@ -0,0 +1,148 @@ + + + + + Test for Bug 821630 Settings API + + + + + + +Mozilla Bug 821630 +

+ +
+
+
+ + diff --git a/dom/settings/tests/test_settings_bug1110872.html b/dom/settings/tests/test_settings_bug1110872.html new file mode 100644 index 000000000..296c71a8c --- /dev/null +++ b/dom/settings/tests/test_settings_bug1110872.html @@ -0,0 +1,17 @@ + + + + Test for Bug {1110872} Settings API + + + + + + + Mozilla Bug {1110872} +

+ + + diff --git a/dom/settings/tests/test_settings_data_uris.html b/dom/settings/tests/test_settings_data_uris.html new file mode 100644 index 000000000..7d9165fb3 --- /dev/null +++ b/dom/settings/tests/test_settings_data_uris.html @@ -0,0 +1,149 @@ + + + + + Test for Bug 806374 Settings API + + + + + + +Mozilla Bug 821630 +

+ +
+
+
+ + diff --git a/dom/settings/tests/test_settings_events.html b/dom/settings/tests/test_settings_events.html new file mode 100644 index 000000000..a8bf851be --- /dev/null +++ b/dom/settings/tests/test_settings_events.html @@ -0,0 +1,47 @@ + + + + + Test for Bug 678695 + + + + +Mozilla Bug 678695 +

+ +
+
+
+ + diff --git a/dom/settings/tests/test_settings_navigator_object.html b/dom/settings/tests/test_settings_navigator_object.html new file mode 100644 index 000000000..2f666aee0 --- /dev/null +++ b/dom/settings/tests/test_settings_navigator_object.html @@ -0,0 +1,37 @@ + + + + + Test for Bug 898512 Settings API + + + + + + +Mozilla Bug 898512 +

+ +
+
+
+ + diff --git a/dom/settings/tests/test_settings_observer_killer.html b/dom/settings/tests/test_settings_observer_killer.html new file mode 100644 index 000000000..8e7ed973c --- /dev/null +++ b/dom/settings/tests/test_settings_observer_killer.html @@ -0,0 +1,60 @@ + + + + + Test for Bug 1193469 Settings API + + + + + + +Mozilla Bug 1193469 +

+ +
+
+
+ + diff --git a/dom/settings/tests/test_settings_onsettingchange.html b/dom/settings/tests/test_settings_onsettingchange.html new file mode 100644 index 000000000..974da0c63 --- /dev/null +++ b/dom/settings/tests/test_settings_onsettingchange.html @@ -0,0 +1,306 @@ + + + + + Test for Bug 678695 Settings API + + + + + + +Mozilla Bug 678695 +

+ +
+
+
+ + diff --git a/dom/settings/tests/test_settings_permissions.html b/dom/settings/tests/test_settings_permissions.html new file mode 100644 index 000000000..4cc02385a --- /dev/null +++ b/dom/settings/tests/test_settings_permissions.html @@ -0,0 +1,184 @@ + + + + + Test for Bug {678695} Settings API + + + + + + +Mozilla Bug {900551} +

+ +
+
+
+ + diff --git a/dom/settings/tests/test_settings_service.js b/dom/settings/tests/test_settings_service.js new file mode 100644 index 000000000..132877a5d --- /dev/null +++ b/dom/settings/tests/test_settings_service.js @@ -0,0 +1,138 @@ +"use strict"; + +var Cu = Components.utils; +var Cc = Components.classes; +var Ci = Components.interfaces; + +if (SpecialPowers.isMainProcess()) { + SpecialPowers.Cu.import("resource://gre/modules/SettingsRequestManager.jsm"); +} + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +SimpleTest.waitForExplicitFinish(); + +XPCOMUtils.defineLazyServiceGetter(this, "SettingsService", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + +var tests = [ + /* Callback tests */ + function() { + let callbackCount = 10; + + let callback = { + handle: function(name, result) { + switch (callbackCount) { + case 10: + case 9: + is(result, true, "result is true"); + break; + case 8: + case 7: + is(result, false, "result is false"); + break; + case 6: + case 5: + is(result, 9, "result is 9"); + break; + case 4: + case 3: + is(result, 9.4, "result is 9.4"); + break; + case 2: + is(result, false, "result is false"); + break; + case 1: + is(result, null, "result is null"); + break; + default: + ok(false, "Unexpected call: " + callbackCount); + } + + --callbackCount; + if (callbackCount === 0) { + next(); + } + }, + + handleError: function(name) { + ok(false, "error: " + name); + } + }; + + let lock = SettingsService.createLock(); + let lock1 = SettingsService.createLock(); + + lock.set("asdf", true, callback, null); + lock1.get("asdf", callback); + lock.get("asdf", callback); + lock.set("asdf", false, callback, null); + lock.get("asdf", callback); + lock.set("int", 9, callback, null); + lock.get("int", callback); + lock.set("doub", 9.4, callback, null); + lock.get("doub", callback); + lock1.get("asdfxxx", callback); + }, + + /* Observer tests */ + function() { + const MOZSETTINGS_CHANGED = "mozsettings-changed"; + const TEST_OBSERVER_KEY = "test.observer.key"; + const TEST_OBSERVER_VALUE = true; + const TEST_OBSERVER_MESSAGE = "test.observer.message"; + + var obs = { + observe: function (subject, topic, data) { + + if (topic !== MOZSETTINGS_CHANGED) { + ok(false, "Event is not mozsettings-changed."); + return; + } + // Data is now stored in subject + if ("wrappedJSObject" in subject) { + ok(true, "JS object wrapped into subject"); + subject = subject.wrappedJSObject; + } + if (subject["key"] != TEST_OBSERVER_KEY) { + return; + } + + function checkProp(name, type, value) { + ok(name in subject, "subject." + name + " is present"); + is(typeof subject[name], type, "subject." + name + " is " + type); + is(subject[name], value, "subject." + name + " is " + value); + } + + checkProp("key", "string", TEST_OBSERVER_KEY); + checkProp("value", "boolean", TEST_OBSERVER_VALUE); + checkProp("isInternalChange", "boolean", true); + + Services.obs.removeObserver(this, MOZSETTINGS_CHANGED); + next(); + } + }; + + Services.obs.addObserver(obs, MOZSETTINGS_CHANGED, false); + + let lock = SettingsService.createLock(); + lock.set(TEST_OBSERVER_KEY, TEST_OBSERVER_VALUE, null); + } +]; + +function next() { + let step = tests.shift(); + if (step) { + try { + step(); + } catch(e) { + ok(false, "Test threw: " + e); + } + } else { + SimpleTest.finish(); + } +} + +next(); diff --git a/dom/settings/tests/test_settings_service.xul b/dom/settings/tests/test_settings_service.xul new file mode 100644 index 000000000..58a9efad9 --- /dev/null +++ b/dom/settings/tests/test_settings_service.xul @@ -0,0 +1,19 @@ + + + + + + + + + + Mozilla Bug 678695 + + + + + + + Mozilla Bug 1012214 + + +