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