diff options
Diffstat (limited to 'dom/settings/SettingsRequestManager.jsm')
-rw-r--r-- | dom/settings/SettingsRequestManager.jsm | 1224 |
1 files changed, 1224 insertions, 0 deletions
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(); +} |