1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 const Cc = Components.classes;
8 const Ci = Components.interfaces;
9 const Cu = Components.utils;
11 this.EXPORTED_SYMBOLS = ["SettingsRequestManager"];
13 Cu.import("resource://gre/modules/SettingsDB.jsm");
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
15 Cu.import("resource://gre/modules/Services.jsm");
16 Cu.import("resource://gre/modules/PermissionsTable.jsm");
23 Services.prefs.getBoolPref("dom.mozSettings.SettingsRequestManager.debug.enabled");
25 Services.prefs.getBoolPref("dom.mozSettings.SettingsRequestManager.verbose.enabled");
29 dump("-*- SettingsRequestManager: " + s + "\n");
32 const kXpcomShutdownObserverTopic = "xpcom-shutdown";
33 const kInnerWindowDestroyed = "inner-window-destroyed";
34 const kMozSettingsChangedObserverTopic = "mozsettings-changed";
35 const kSettingsReadSuffix = "-read";
36 const kSettingsWriteSuffix = "-write";
37 const kSettingsClearPermission = "settings-clear";
38 const kAllSettingsReadPermission = "settings" + kSettingsReadSuffix;
39 const kAllSettingsWritePermission = "settings" + kSettingsWriteSuffix;
40 // Any application with settings permissions, be it for all settings
41 // or a single one, will need to be able to access the settings API.
42 // The settings-api permission allows an app to see the mozSettings
43 // API in order to create locks and queue tasks. Whether these tasks
44 // will be allowed depends on the exact permissions the app has.
45 const kSomeSettingsReadPermission = "settings-api" + kSettingsReadSuffix;
46 const kSomeSettingsWritePermission = "settings-api" + kSettingsWriteSuffix;
48 XPCOMUtils.defineLazyServiceGetter(this, "mrm",
49 "@mozilla.org/memory-reporter-manager;1",
50 "nsIMemoryReporterManager");
51 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
52 "@mozilla.org/parentprocessmessagemanager;1",
53 "nsIMessageBroadcaster");
54 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
55 "@mozilla.org/uuid-generator;1",
58 let SettingsPermissions = {
59 checkPermission: function(aPrincipal, aPerm) {
61 Cu.reportError("SettingsPermissions.checkPermission was passed a null principal. Denying all permissions.");
64 if (aPrincipal.origin == "[System Principal]" ||
65 Services.perms.testExactPermissionFromPrincipal(aPrincipal, aPerm) == Ci.nsIPermissionManager.ALLOW_ACTION) {
70 hasAllReadPermission: function(aPrincipal) {
71 return this.checkPermission(aPrincipal, kAllSettingsReadPermission);
73 hasAllWritePermission: function(aPrincipal) {
74 return this.checkPermission(aPrincipal, kAllSettingsWritePermission);
76 hasSomeReadPermission: function(aPrincipal) {
77 return this.checkPermission(aPrincipal, kSomeSettingsReadPermission);
79 hasSomeWritePermission: function(aPrincipal) {
80 return this.checkPermission(aPrincipal, kSomeSettingsWritePermission);
82 hasClearPermission: function(aPrincipal) {
83 return this.checkPermission(aPrincipal, kSettingsClearPermission);
85 hasReadPermission: function(aPrincipal, aSettingsName) {
86 return this.hasAllReadPermission(aPrincipal) || this.checkPermission(aPrincipal, "settings:" + aSettingsName + kSettingsReadSuffix);
88 hasWritePermission: function(aPrincipal, aSettingsName) {
89 return this.hasAllWritePermission(aPrincipal) || this.checkPermission(aPrincipal, "settings:" + aSettingsName + kSettingsWriteSuffix);
94 function SettingsLockInfo(aDB, aMsgMgr, aPrincipal, aLockID, aIsServiceLock, aWindowID) {
96 // ID Shared with the object on the child side
98 // Is this a content lock or a settings service lock?
99 isServiceLock: aIsServiceLock,
100 // Which inner window ID
102 // Tasks to be run once the lock is at the head of the queue
104 // This is set to true once a transaction is ready to run, but is not at the
105 // head of the lock queue.
107 // Holds values that are requested to be set until the lock lifetime ends,
108 // then commits them to the DB.
110 // Internal transaction object
111 _transaction: undefined,
112 // Message manager that controls the lock
114 // If true, it means a permissions check failed, so just fail everything now
116 // If we're slated to run finalize, set this to make sure we don't
117 // somehow run other events afterward.
119 // Lets us know if we can use this lock for a clear command
121 // Lets us know if this lock has been used to clear at any point.
123 // Principal the lock was created under. We assume that the lock
124 // will continue to exist under this principal for the duration of
126 principal: aPrincipal,
127 getObjectStore: function() {
128 if (VERBOSE) debug("Getting transaction for " + this.lockID);
130 // Test for transaction validity via trying to get the
131 // datastore. If it doesn't work, assume the transaction is
132 // closed, create a new transaction and try again.
133 if (this._transaction) {
135 store = this._transaction.objectStore(SETTINGSSTORE_NAME);
137 if (e.name == "InvalidStateError") {
138 if (VERBOSE) debug("Current transaction for " + this.lockID + " closed, trying to create new one.");
140 if (DEBUG) debug("Unexpected exception, throwing: " + e);
145 // Create one transaction with a global permission. This may be
146 // slightly slower on apps with full settings permissions, but
147 // it means we don't have to do our own transaction order
149 if (!SettingsPermissions.hasSomeWritePermission(this.principal)) {
150 if (VERBOSE) debug("Making READONLY transaction for " + this.lockID);
151 this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readonly");
153 if (VERBOSE) debug("Making READWRITE transaction for " + this.lockID);
154 this._transaction = aDB._db.transaction(SETTINGSSTORE_NAME, "readwrite");
156 this._transaction.oncomplete = function() {
157 if (VERBOSE) debug("Transaction for lock " + this.lockID + " closed");
159 this._transaction.onabort = function () {
160 if (DEBUG) debug("Transaction for lock " + this.lockID + " aborted");
164 store = this._transaction.objectStore(SETTINGSSTORE_NAME);
166 if (e.name == "InvalidStateError") {
167 if (DEBUG) debug("Cannot create objectstore on transaction for " + this.lockID);
170 if (DEBUG) debug("Unexpected exception, throwing: " + e);
179 let SettingsRequestManager = {
180 // Access to the settings DB
181 settingsDB: new SettingsDB(),
182 // Remote messages to listen for from child
183 messages: ["child-process-shutdown", "Settings:Get", "Settings:Set",
184 "Settings:Clear", "Settings:Run", "Settings:Finalize",
185 "Settings:CreateLock", "Settings:RegisterForMessages"],
186 // Map of LockID to SettingsLockInfo objects
188 // Queue of LockIDs. The LockID on the front of the queue is the only lock
189 // that will have requests processed, all other locks will queue requests
190 // until they hit the front of the queue.
191 settingsLockQueue: [],
193 // Since we need to call observers at times when we may not have
194 // just received a message from a child process, we cache principals
195 // for message managers and check permissions on them before we send
196 // settings notifications to child processes.
197 observerPrincipalCache: new Map(),
201 if (VERBOSE) debug("init");
202 this.settingsDB.init();
203 this.messages.forEach((function(msgName) {
204 ppmm.addMessageListener(msgName, this);
206 Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false);
207 Services.obs.addObserver(this, kInnerWindowDestroyed, false);
208 mrm.registerStrongReporter(this);
211 _serializePreservingBinaries: function _serializePreservingBinaries(aObject) {
212 function needsUUID(aValue) {
213 if (!aValue || !aValue.constructor) {
216 return (aValue.constructor.name == "Date") || (aValue instanceof Ci.nsIDOMFile) ||
217 (aValue instanceof Ci.nsIDOMBlob);
219 // We need to serialize settings objects, otherwise they can change between
220 // the set() call and the enqueued request being processed. We can't simply
221 // parse(stringify(obj)) because that breaks things like Blobs, Files and
222 // Dates, so we use stringify's replacer and parse's reviver parameters to
223 // preserve binaries.
224 let binaries = Object.create(null);
225 let stringified = JSON.stringify(aObject, function(key, value) {
226 value = this.settingsDB.prepareValue(value);
227 if (needsUUID(value)) {
228 let uuid = uuidgen.generateUUID().toString();
229 binaries[uuid] = value;
234 return JSON.parse(stringified, function(key, value) {
235 if (value in binaries) {
236 return binaries[value];
242 queueTask: function(aOperation, aData) {
243 if (VERBOSE) debug("Queueing task: " + aOperation);
247 let lock = this.lockInfo[aData.lockID];
250 return Promise.reject({error: "Lock already dead, cannot queue task"});
253 if (aOperation == "set") {
254 aData.settings = this._serializePreservingBinaries(aData.settings);
257 this.lockInfo[aData.lockID].tasks.push({
258 operation: aOperation,
263 let promise = new Promise(function(resolve, reject) {
264 defer.resolve = resolve;
265 defer.reject = reject;
271 // Due to the fact that we're skipping the database in some places
272 // by keeping a local "set" value cache, resolving some calls
273 // without a call to the database would mean we could potentially
274 // receive promise responses out of expected order if a get is
275 // called before a set. Therefore, we wrap our resolve in a null
276 // get, which means it will resolves afer the rest of the calls
278 queueTaskReturn: function(aTask, aReturnValue) {
279 if (VERBOSE) debug("Making task queuing transaction request.");
280 let data = aTask.data;
281 let lock = this.lockInfo[data.lockID];
282 let store = lock.getObjectStore(lock.principal);
284 if (DEBUG) debug("Rejecting task queue on lock " + aTask.data.lockID);
285 return Promise.reject({task: aTask, error: "Cannot get object store"});
287 // Due to the fact that we're skipping the database, resolving
288 // this without a call to the database would mean we could
289 // potentially receive promise responses out of expected order if
290 // a get is called before a set. Therefore, we wrap our resolve in
291 // a null get, which means it will resolves afer the rest of the
292 // calls queued to the DB.
293 let getReq = store.get(0);
296 let promiseWrapper = new Promise(function(resolve, reject) {
297 defer.resolve = resolve;
298 defer.reject = reject;
301 getReq.onsuccess = function(event) {
302 return defer.resolve(aReturnValue);
304 getReq.onerror = function() {
305 return defer.reject({task: aTask, error: getReq.error.name});
307 return promiseWrapper;
310 taskGet: function(aTask) {
311 if (VERBOSE) debug("Running Get task on lock " + aTask.data.lockID);
313 // Check that we have permissions for getting the value
314 let data = aTask.data;
315 let lock = this.lockInfo[data.lockID];
318 return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
321 if (DEBUG) debug("Lock failed. All subsequent requests will fail.");
322 return Promise.reject({task: aTask, error: "Lock failed, all requests now failing."});
325 if (lock.hasCleared) {
326 if (DEBUG) debug("Lock was used for a clear command. All subsequent requests will fail.");
327 return Promise.reject({task: aTask, error: "Lock was used for a clear command. All subsequent requests will fail."});
330 lock.canClear = false;
332 if (!SettingsPermissions.hasReadPermission(lock.principal, data.name)) {
333 if (DEBUG) debug("get not allowed for " + data.name);
335 return Promise.reject({task: aTask, error: "No permission to get " + data.name});
338 // If the value was set during this transaction, use the cached value
339 if (data.name in lock.queuedSets) {
340 if (VERBOSE) debug("Returning cached set value " + lock.queuedSets[data.name] + " for " + data.name);
341 let local_results = {};
342 local_results[data.name] = lock.queuedSets[data.name];
343 return this.queueTaskReturn(aTask, {task: aTask, results: local_results});
346 // Create/Get transaction and make request
347 if (VERBOSE) debug("Making get transaction request for " + data.name);
348 let store = lock.getObjectStore(lock.principal);
350 if (DEBUG) debug("Rejecting Get task on lock " + aTask.data.lockID);
351 return Promise.reject({task: aTask, error: "Cannot get object store"});
354 if (VERBOSE) debug("Making get request for " + data.name);
355 let getReq = (data.name === "*") ? store.mozGetAll() : store.mozGetAll(data.name);
358 let promiseWrapper = new Promise(function(resolve, reject) {
359 defer.resolve = resolve;
360 defer.reject = reject;
363 getReq.onsuccess = function(event) {
364 if (VERBOSE) debug("Request for '" + data.name + "' successful. " +
365 "Record count: " + event.target.result.length);
367 if (event.target.result.length == 0) {
368 if (VERBOSE) debug("MOZSETTINGS-GET-WARNING: " + data.name + " is not in the database.\n");
373 for (let i in event.target.result) {
374 let result = event.target.result[i];
375 let name = result.settingName;
376 if (VERBOSE) debug(name + ": " + result.userValue +", " + result.defaultValue);
377 let value = result.userValue !== undefined ? result.userValue : result.defaultValue;
378 results[name] = value;
380 return defer.resolve({task: aTask, results: results});
382 getReq.onerror = function() {
383 return defer.reject({task: aTask, error: getReq.error.name});
385 return promiseWrapper;
388 taskSet: function(aTask) {
389 let data = aTask.data;
390 let lock = this.lockInfo[data.lockID];
391 let keys = Object.getOwnPropertyNames(data.settings);
394 return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
397 if (DEBUG) debug("Lock failed. All subsequent requests will fail.");
398 return Promise.reject({task: aTask, error: "Lock failed a permissions check, all requests now failing."});
401 if (lock.hasCleared) {
402 if (DEBUG) debug("Lock was used for a clear command. All subsequent requests will fail.");
403 return Promise.reject({task: aTask, error: "Lock was used for a clear command. All other requests will fail."});
406 lock.canClear = false;
408 // If we have no keys, resolve
409 if (keys.length === 0) {
410 if (DEBUG) debug("No keys to change entered!");
411 return Promise.resolve({task: aTask});
414 for (let i = 0; i < keys.length; i++) {
415 if (!SettingsPermissions.hasWritePermission(lock.principal, keys[i])) {
416 if (DEBUG) debug("set not allowed on " + keys[i]);
418 return Promise.reject({task: aTask, error: "No permission to set " + keys[i]});
422 for (let i = 0; i < keys.length; i++) {
424 if (VERBOSE) debug("key: " + key + ", val: " + JSON.stringify(data.settings[key]) + ", type: " + typeof(data.settings[key]));
425 lock.queuedSets[key] = data.settings[key];
428 return this.queueTaskReturn(aTask, {task: aTask});
431 startRunning: function(aLockID) {
432 let lock = this.lockInfo[aLockID];
435 if (DEBUG) debug("Lock no longer alive, cannot start running");
439 lock.consumable = true;
440 if (aLockID == this.settingsLockQueue[0] || this.settingsLockQueue.length == 0) {
441 // If a lock is currently at the head of the queue, run all tasks for
443 if (VERBOSE) debug("Start running tasks for " + aLockID);
446 // If a lock isn't at the head of the queue, but requests to be run,
447 // simply mark it as consumable, which means it will automatically run
448 // once it comes to the head of the queue.
449 if (VERBOSE) debug("Queuing tasks for " + aLockID + " while waiting for " + this.settingsLockQueue[0]);
453 queueConsume: function() {
454 if (this.settingsLockQueue.length > 0 && this.lockInfo[this.settingsLockQueue[0]].consumable) {
455 Services.tm.currentThread.dispatch(SettingsRequestManager.consumeTasks.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
459 finalizeSets: function(aTask) {
460 let data = aTask.data;
461 if (VERBOSE) debug("Finalizing tasks for lock " + data.lockID);
462 let lock = this.lockInfo[data.lockID];
465 return Promise.reject({task: aTask, error: "Lock died, can't finalize"});
467 lock.finalizing = true;
469 this.removeLock(data.lockID);
470 return Promise.reject({task: aTask, error: "Lock failed a permissions check, all requests now failing."});
472 // If we have cleared, there is no reason to continue finalizing
473 // this lock. Just resolve promise with task and move on.
474 if (lock.hasCleared) {
475 if (VERBOSE) debug("Clear was called on lock, skipping finalize");
476 this.removeLock(data.lockID);
477 return Promise.resolve({task: aTask});
479 let keys = Object.getOwnPropertyNames(lock.queuedSets);
480 if (keys.length === 0) {
481 if (VERBOSE) debug("Nothing to finalize. Exiting.");
482 this.removeLock(data.lockID);
483 return Promise.resolve({task: aTask});
486 let store = lock.getObjectStore(lock.principal);
488 if (DEBUG) debug("Rejecting Set task on lock " + aTask.data.lockID);
489 this.removeLock(data.lockID);
490 return Promise.reject({task: aTask, error: "Cannot get object store"});
493 // Due to the fact there may have multiple set operations to clear, and
494 // they're all async, callbacks are gathered into promises, and the promises
495 // are processed with Promises.all().
496 let checkPromises = [];
497 let finalValues = {};
498 for (let i = 0; i < keys.length; i++) {
500 if (VERBOSE) debug("key: " + key + ", val: " + lock.queuedSets[key] + ", type: " + typeof(lock.queuedSets[key]));
502 let checkPromise = new Promise(function(resolve, reject) {
503 checkDefer.resolve = resolve;
504 checkDefer.reject = reject;
507 // Get operation is used to fill in the default value, assuming there is
508 // one. For the moment, if a value doesn't exist in the settings DB, we
509 // allow the user to add it, and just pass back a null default value.
510 let checkKeyRequest = store.get(key);
511 checkKeyRequest.onsuccess = function (event) {
512 let userValue = lock.queuedSets[key];
514 if (!event.target.result) {
516 if (VERBOSE) debug("MOZSETTINGS-GET-WARNING: " + key + " is not in the database.\n");
518 defaultValue = event.target.result.defaultValue;
520 let obj = {settingName: key, defaultValue: defaultValue, userValue: userValue};
521 finalValues[key] = {defaultValue: defaultValue, userValue: userValue};
522 let setReq = store.put(obj);
523 setReq.onsuccess = function() {
524 if (VERBOSE) debug("Set successful!");
525 if (VERBOSE) debug("key: " + key + ", val: " + finalValues[key] + ", type: " + typeof(finalValues[key]));
526 return checkDefer.resolve({task: aTask});
528 setReq.onerror = function() {
529 return checkDefer.reject({task: aTask, error: setReq.error.name});
532 checkKeyRequest.onerror = function(event) {
533 return checkDefer.reject({task: aTask, error: checkKeyRequest.error.name});
535 checkPromises.push(checkPromise);
539 let promiseWrapper = new Promise(function(resolve, reject) {
540 defer.resolve = resolve;
541 defer.reject = reject;
544 // Once all transactions are done, or any have failed, remove the lock and
545 // start processing the tasks from the next lock in the queue.
546 Promise.all(checkPromises).then(function() {
547 // If all commits were successful, notify observers
548 for (let i = 0; i < keys.length; i++) {
549 this.sendSettingsChange(keys[i], finalValues[keys[i]].userValue, lock.isServiceLock);
551 this.removeLock(data.lockID);
552 defer.resolve({task: aTask});
553 }.bind(this), function(ret) {
554 this.removeLock(data.lockID);
555 defer.reject({task: aTask, error: "Set transaction failure"});
557 return promiseWrapper;
560 // Clear is only expected to be called via tests, and if a lock
561 // calls clear, it should be the only thing the lock does. This
562 // allows us to not have to deal with the possibility of query
563 // integrity checking. Clear should never be called in the wild,
564 // even by certified apps, which is why it has its own permission
566 taskClear: function(aTask) {
567 if (VERBOSE) debug("Clearing");
568 let data = aTask.data;
569 let lock = this.lockInfo[data.lockID];
572 if (DEBUG) debug("Lock failed, all requests now failing.");
573 return Promise.reject({task: aTask, error: "Lock failed, all requests now failing."});
576 if (!lock.canClear) {
577 if (DEBUG) debug("Lock tried to clear after queuing other tasks. Failing.");
579 return Promise.reject({task: aTask, error: "Cannot call clear after queuing other tasks, all requests now failing."});
582 if (!SettingsPermissions.hasClearPermission(lock.principal)) {
583 if (DEBUG) debug("clear not allowed");
585 return Promise.reject({task: aTask, error: "No permission to clear DB"});
588 lock.hasCleared = true;
590 let store = lock.getObjectStore(lock.principal);
592 if (DEBUG) debug("Rejecting Clear task on lock " + aTask.data.lockID);
593 return Promise.reject({task: aTask, error: "Cannot get object store"});
596 let promiseWrapper = new Promise(function(resolve, reject) {
597 defer.resolve = resolve;
598 defer.reject = reject;
601 let clearReq = store.clear();
602 clearReq.onsuccess = function() {
603 return defer.resolve({task: aTask});
605 clearReq.onerror = function() {
606 return defer.reject({task: aTask});
608 return promiseWrapper;
611 ensureConnection : function() {
612 if (VERBOSE) debug("Ensuring Connection");
614 let promiseWrapper = new Promise(function(resolve, reject) {
615 defer.resolve = resolve;
616 defer.reject = reject;
618 this.settingsDB.ensureDB(
619 function() { defer.resolve(); },
621 if (DEBUG) debug("Cannot open Settings DB. Trying to open an old version?\n");
625 return promiseWrapper;
628 runTasks: function(aLockID) {
629 if (VERBOSE) debug("Running tasks for " + aLockID);
630 let lock = this.lockInfo[aLockID];
632 if (DEBUG) debug("Lock no longer alive, cannot run tasks");
635 let currentTask = lock.tasks.shift();
637 while (currentTask) {
638 if (VERBOSE) debug("Running Operation " + currentTask.operation);
639 if (lock.finalizing) {
640 // We should really never get to this point, but if we do,
641 // fail every task that happens.
642 Cu.reportError("Settings lock " + aLockID + " trying to run task '" + currentTask.operation + "' after finalizing. Ignoring tasks, but this is bad. Lock: " + aLockID);
643 currentTask.defer.reject("Cannot call new task after finalizing");
646 this.tasksConsumed++;
647 switch (currentTask.operation) {
649 p = this.taskGet(currentTask);
652 p = this.taskSet(currentTask);
655 p = this.taskClear(currentTask);
658 p = this.finalizeSets(currentTask);
661 if (DEBUG) debug("Invalid operation: " + currentTask.operation);
662 p.reject("Invalid operation: " + currentTask.operation);
664 p.then(function(ret) {
665 ret.task.defer.resolve("results" in ret ? ret.results : null);
666 }.bind(currentTask), function(ret) {
667 ret.task.defer.reject(ret.error);
671 currentTask = lock.tasks.shift();
675 consumeTasks: function() {
676 if (this.settingsLockQueue.length == 0) {
677 if (VERBOSE) debug("Nothing to run!");
681 let lockID = this.settingsLockQueue[0];
682 if (VERBOSE) debug("Consuming tasks for " + lockID);
683 let lock = this.lockInfo[lockID];
685 // If a process dies, we should clean up after it via the
686 // child-process-shutdown event. But just in case we don't, we want to make
687 // sure we never block on consuming.
689 if (DEBUG) debug("Lock no longer alive, cannot consume tasks");
694 if (!lock.consumable || lock.tasks.length === 0) {
695 if (VERBOSE) debug("No more tasks to run or not yet consuamble.");
699 lock.consumable = false;
700 this.ensureConnection().then(
702 this.runTasks(lockID);
703 }.bind(this), function(ret) {
704 dump("-*- SettingsRequestManager: SETTINGS DATABASE ERROR: Cannot make DB connection!\n");
708 observe: function(aSubject, aTopic, aData) {
709 if (VERBOSE) debug("observe: " + aTopic);
711 case kXpcomShutdownObserverTopic:
712 this.messages.forEach((function(msgName) {
713 ppmm.removeMessageListener(msgName, this);
715 Services.obs.removeObserver(this, kXpcomShutdownObserverTopic);
717 mrm.unregisterStrongReporter(this);
720 case kInnerWindowDestroyed:
721 let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
722 this.forceFinalizeChildLocksNonOOP(wId);
726 if (DEBUG) debug("Wrong observer topic: " + aTopic);
731 collectReports: function(aCallback, aData, aAnonymize) {
732 for (let lockId of Object.keys(this.lockInfo)) {
733 let lock = this.lockInfo[lockId];
734 let length = lock.tasks.length;
740 let path = "settings-locks/tasks/queue-length(id=" + lockId + ")";
742 aCallback.callback("", path,
743 Ci.nsIMemoryReporter.KIND_OTHER,
744 Ci.nsIMemoryReporter.UNITS_COUNT,
746 "Tasks queue length for this lock",
750 aCallback.callback("",
751 "settings-locks/tasks/processed",
752 Ci.nsIMemoryReporter.KIND_OTHER,
753 Ci.nsIMemoryReporter.UNITS_COUNT,
755 "The number of tasks that were executed.",
759 sendSettingsChange: function(aKey, aValue, aIsServiceLock) {
760 this.broadcastMessage("Settings:Change:Return:OK",
761 { key: aKey, value: aValue });
762 Services.obs.notifyObservers(this, kMozSettingsChangedObserverTopic,
766 isInternalChange: aIsServiceLock
770 broadcastMessage: function broadcastMessage(aMsgName, aContent) {
771 if (VERBOSE) debug("Broadcast");
772 this.children.forEach(function(msgMgr) {
773 let principal = this.observerPrincipalCache.get(msgMgr);
775 if (DEBUG) debug("Cannot find principal for message manager to check permissions");
777 else if (SettingsPermissions.hasReadPermission(principal, aContent.key)) {
779 msgMgr.sendAsyncMessage(aMsgName, aContent);
781 if (DEBUG) debug("Failed sending message: " + aMsgName);
785 if (VERBOSE) debug("Finished Broadcasting");
788 addObserver: function(aMsgMgr, aPrincipal) {
789 if (VERBOSE) debug("Add observer for " + aPrincipal.origin);
790 if (this.children.indexOf(aMsgMgr) == -1) {
791 this.children.push(aMsgMgr);
792 this.observerPrincipalCache.set(aMsgMgr, aPrincipal);
796 removeObserver: function(aMsgMgr) {
798 let principal = this.observerPrincipalCache.get(aMsgMgr);
800 debug("Remove observer for " + principal.origin);
803 let index = this.children.indexOf(aMsgMgr);
805 this.children.splice(index, 1);
806 this.observerPrincipalCache.delete(aMsgMgr);
808 if (VERBOSE) debug("Principal/MessageManager pairs left in observer cache: " + this.observerPrincipalCache.size);
811 removeLock: function(aLockID) {
812 if (VERBOSE) debug("Removing lock " + aLockID);
813 if (this.lockInfo[aLockID]) {
814 let transaction = this.lockInfo[aLockID]._transaction;
819 if (e.name == "InvalidStateError") {
820 if (VERBOSE) debug("Transaction for " + aLockID + " closed already");
822 if (DEBUG) debug("Unexpected exception, throwing: " + e);
827 delete this.lockInfo[aLockID];
829 let index = this.settingsLockQueue.indexOf(aLockID);
831 this.settingsLockQueue.splice(index, 1);
833 // If index is 0, the lock we just removed was at the head of
834 // the queue, so possibly queue the next lock if it's
841 hasLockFinalizeTask: function(lock) {
842 // Go in reverse order because finalize should be the last one
843 for (let task_index = lock.tasks.length; task_index >= 0; task_index--) {
844 if (lock.tasks[task_index]
845 && lock.tasks[task_index].operation === "finalize") {
852 enqueueForceFinalize: function(lock) {
853 if (!this.hasLockFinalizeTask(lock)) {
854 if (VERBOSE) debug("Alive lock has pending tasks: " + lock.lockID);
855 this.queueTask("finalize", {lockID: lock.lockID}).then(
857 if (VERBOSE) debug("Alive lock " + lock.lockID + " succeeded to force-finalize");
860 if (DEBUG) debug("Alive lock " + lock.lockID + " failed to force-finalize due to error: " + error);
863 // Finalize is considered a task running situation, but it also needs to
865 this.startRunning(lock.lockID);
869 forceFinalizeChildLocksNonOOP: function(windowId) {
870 if (VERBOSE) debug("Forcing finalize on child locks, non OOP");
872 for (let lockId of Object.keys(this.lockInfo)) {
873 let lock = this.lockInfo[lockId];
874 if (lock.windowID === windowId) {
875 this.enqueueForceFinalize(lock);
880 forceFinalizeChildLocksOOP: function(aMsgMgr) {
881 if (VERBOSE) debug("Forcing finalize on child locks, OOP");
883 for (let lockId of Object.keys(this.lockInfo)) {
884 let lock = this.lockInfo[lockId];
885 if (lock._mm === aMsgMgr) {
886 this.enqueueForceFinalize(lock);
891 receiveMessage: function(aMessage) {
892 if (VERBOSE) debug("receiveMessage " + aMessage.name + ": " + JSON.stringify(aMessage.data));
894 let msg = aMessage.data;
895 let mm = aMessage.target;
897 function returnMessage(name, data) {
899 mm.sendAsyncMessage(name, data);
901 if (DEBUG) debug("Return message failed, " + name);
905 // For all message types that expect a lockID, we check to make
906 // sure that we're accessing a lock that's part of our process. If
907 // not, consider it a security violation and kill the app. Killing
908 // based on creating a colliding lock ID happens as part of
909 // CreateLock check below.
910 switch (aMessage.name) {
913 case "Settings:Clear":
915 case "Settings:Finalize":
916 let kill_process = false;
918 Cu.reportError("Process sending request for lock that does not exist. Killing.");
921 else if (!this.lockInfo[msg.lockID]) {
922 if (DEBUG) debug("Cannot find lock ID " + msg.lockID);
923 // This doesn't kill, because we can have things that file
924 // finalize, then die, and we may get the observer
925 // notification before we get the IPC messages.
928 else if (mm != this.lockInfo[msg.lockID]._mm) {
929 Cu.reportError("Process trying to access settings lock from another process. Killing.");
933 // Kill the app by checking for a non-existent permission
934 aMessage.target.assertPermission("message-manager-mismatch-kill");
941 switch (aMessage.name) {
942 case "child-process-shutdown":
943 if (VERBOSE) debug("Child process shutdown received.");
944 this.forceFinalizeChildLocksOOP(mm);
945 this.removeObserver(mm);
947 case "Settings:RegisterForMessages":
948 if (!SettingsPermissions.hasSomeReadPermission(aMessage.principal)) {
949 Cu.reportError("Settings message " + aMessage.name +
950 " from a content process with no 'settings-api-read' privileges.");
951 aMessage.target.assertPermission("message-manager-no-read-kill");
954 this.addObserver(mm, aMessage.principal);
956 case "Settings:UnregisterForMessages":
957 this.removeObserver(mm);
959 case "Settings:CreateLock":
960 if (VERBOSE) debug("Received CreateLock for " + msg.lockID + " from " + aMessage.principal.origin + " window: " + msg.windowID);
961 // If we try to create a lock ID that collides with one
962 // already in the system, consider it a security violation and
964 if (msg.lockID in this.settingsLockQueue) {
965 Cu.reportError("Trying to queue a lock with the same ID as an already queued lock. Killing app.");
966 aMessage.target.assertPermission("lock-id-duplicate-kill");
969 this.settingsLockQueue.push(msg.lockID);
970 this.lockInfo[msg.lockID] = SettingsLockInfo(this.settingsDB,
978 if (VERBOSE) debug("Received getRequest from " + msg.lockID);
979 this.queueTask("get", msg).then(function(settings) {
980 returnMessage("Settings:Get:OK", {
982 requestID: msg.requestID,
986 if (DEBUG) debug("getRequest FAILED " + msg.name);
987 returnMessage("Settings:Get:KO", {
989 requestID: msg.requestID,
995 if (VERBOSE) debug("Received Set Request from " + msg.lockID);
996 this.queueTask("set", msg).then(function(settings) {
997 returnMessage("Settings:Set:OK", {
999 requestID: msg.requestID
1001 }, function(error) {
1002 returnMessage("Settings:Set:KO", {
1004 requestID: msg.requestID,
1009 case "Settings:Clear":
1010 if (VERBOSE) debug("Received Clear Request from " + msg.lockID);
1011 this.queueTask("clear", msg).then(function() {
1012 returnMessage("Settings:Clear:OK", {
1014 requestID: msg.requestID
1016 }, function(error) {
1017 returnMessage("Settings:Clear:KO", {
1019 requestID: msg.requestID,
1024 case "Settings:Finalize":
1025 if (VERBOSE) debug("Received Finalize");
1026 this.queueTask("finalize", msg).then(function() {
1027 returnMessage("Settings:Finalize:OK", {
1030 }, function(error) {
1031 returnMessage("Settings:Finalize:KO", {
1036 // YES THIS IS SUPPOSED TO FALL THROUGH. Finalize is considered a task
1037 // running situation, but it also needs to queue a task.
1038 case "Settings:Run":
1039 if (VERBOSE) debug("Received Run");
1040 this.startRunning(msg.lockID);
1043 if (DEBUG) debug("Wrong message: " + aMessage.name);
1048 SettingsRequestManager.init();