gecko-dev/dom/settings/SettingsManager.js
2017-01-12 14:16:52 -08:00

507 lines
18 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
var DEBUG = false;
var VERBOSE = false;
try {
DEBUG =
Services.prefs.getBoolPref("dom.mozSettings.SettingsManager.debug.enabled");
VERBOSE =
Services.prefs.getBoolPref("dom.mozSettings.SettingsManager.verbose.enabled");
} catch (ex) { }
function debug(s) {
dump("-*- SettingsManager: " + s + "\n");
}
XPCOMUtils.defineLazyServiceGetter(Services, "DOMRequest",
"@mozilla.org/dom/dom-request-service;1",
"nsIDOMRequestService");
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
"@mozilla.org/childprocessmessagemanager;1",
"nsIMessageSender");
XPCOMUtils.defineLazyServiceGetter(this, "mrm",
"@mozilla.org/memory-reporter-manager;1",
"nsIMemoryReporterManager");
XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
const kObserverSoftLimit = 10;
/**
* In order to make SettingsManager work with Privileged Apps, we need the lock
* to be OOP. However, the lock state needs to be managed on the child process,
* while the IDB functions now happen on the parent process so we don't have to
* expose IDB permissions at the child process level. We use the
* DOMRequestHelper mechanism to deal with DOMRequests/promises across the
* processes.
*
* However, due to the nature of the IDBTransaction lifetime, we need to relay
* to the parent when to finalize the transaction once the child is done with the
* lock. We keep a list of all open requests for a lock, and once the lock
* reaches the end of its receiveMessage function with no more queued requests,
* we consider it dead. At that point, we send a message to the parent to notify
* it to finalize the transaction.
*/
function SettingsLock(aSettingsManager) {
if (VERBOSE) debug("settings lock init");
this._open = true;
this._settingsManager = aSettingsManager;
this._id = uuidgen.generateUUID().toString();
// DOMRequestIpcHelper.initHelper sets this._window
this.initDOMRequestHelper(this._settingsManager._window, ["Settings:Get:OK", "Settings:Get:KO",
"Settings:Clear:OK", "Settings:Clear:KO",
"Settings:Set:OK", "Settings:Set:KO",
"Settings:Finalize:OK", "Settings:Finalize:KO"]);
let createLockPayload = {
lockID: this._id,
isServiceLock: false,
windowID: this._settingsManager.innerWindowID,
lockStack: (new Error).stack
};
this.sendMessage("Settings:CreateLock", createLockPayload);
Services.tm.currentThread.dispatch(this._closeHelper.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
// We only want to file closeHelper once per set of receiveMessage calls.
this._closeCalled = true;
}
SettingsLock.prototype = {
__proto__: DOMRequestIpcHelper.prototype,
set onsettingstransactionsuccess(aHandler) {
this.__DOM_IMPL__.setEventHandler("onsettingstransactionsuccess", aHandler);
},
get onsettingstransactionsuccess() {
return this.__DOM_IMPL__.getEventHandler("onsettingstransactionsuccess");
},
set onsettingstransactionfailure(aHandler) {
this.__DOM_IMPL__.setEventHandler("onsettingstransactionfailure", aHandler);
},
get onsettingstransactionfailure() {
return this.__DOM_IMPL__.getEventHandler("onsettingstransactionfailure");
},
get closed() {
return !this._open;
},
_closeHelper: function() {
if (VERBOSE) debug("closing lock " + this._id);
this._open = false;
this._closeCalled = false;
if (!this._requests || Object.keys(this._requests).length == 0) {
if (VERBOSE) debug("Requests exhausted, finalizing " + this._id);
this._settingsManager.unregisterLock(this._id);
this.sendMessage("Settings:Finalize", {lockID: this._id});
} else {
if (VERBOSE) debug("Requests left: " + Object.keys(this._requests).length);
this.sendMessage("Settings:Run", {lockID: this._id});
}
},
_wrap: function _wrap(obj) {
return Cu.cloneInto(obj, this._settingsManager._window);
},
sendMessage: function(aMessageName, aData) {
// sendMessage can be called after our window has died, or get
// queued to run later in a thread via _closeHelper, but the
// SettingsManager may have died in between the time it was
// scheduled and the time it runs. Make sure our window is valid
// before sending, otherwise just ignore.
if (!this._settingsManager._window) {
Cu.reportError(
"SettingsManager window died, cannot run settings transaction." +
" SettingsMessage: " + aMessageName +
" SettingsData: " + JSON.stringify(aData));
return;
}
cpmm.sendAsyncMessage(aMessageName,
aData,
undefined,
this._settingsManager._window.document.nodePrincipal);
},
receiveMessage: function(aMessage) {
let msg = aMessage.data;
// SettingsRequestManager broadcasts changes to all locks in the child. If
// our lock isn't being addressed, just return.
if (msg.lockID != this._id) {
return;
}
if (VERBOSE) debug("receiveMessage (" + this._id + "): " + aMessage.name);
// Finalizing a transaction does not return a request ID since we are
// supposed to fire callbacks.
//
// We also destroy the DOMRequestHelper after we've received the
// finalize message. At this point, we will be guarenteed no more
// request returns are coming from the SettingsRequestManager.
if (!msg.requestID) {
let event;
switch (aMessage.name) {
case "Settings:Finalize:OK":
if (VERBOSE) debug("Lock finalize ok: " + this._id);
event = new this._window.MozSettingsTransactionEvent("settingstransactionsuccess", {});
this.__DOM_IMPL__.dispatchEvent(event);
this.destroyDOMRequestHelper();
break;
case "Settings:Finalize:KO":
if (DEBUG) debug("Lock finalize failed: " + this._id);
event = new this._window.MozSettingsTransactionEvent("settingstransactionfailure", {
error: msg.errorMsg
});
this.__DOM_IMPL__.dispatchEvent(event);
this.destroyDOMRequestHelper();
break;
default:
if (DEBUG) debug("Message type " + aMessage.name + " is missing a requestID");
}
return;
}
let req = this.getRequest(msg.requestID);
if (!req) {
if (DEBUG) debug("Matching request not found.");
return;
}
this.removeRequest(msg.requestID);
// DOMRequest callbacks called from here can die due to having
// things like marionetteScriptFinished in them. Make sure we file
// our call to run/finalize BEFORE opening the lock and fulfilling
// DOMRequests.
if (!this._closeCalled) {
// We only want to file closeHelper once per set of receiveMessage calls.
Services.tm.currentThread.dispatch(this._closeHelper.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
this._closeCalled = true;
}
if (VERBOSE) debug("receiveMessage: " + aMessage.name);
switch (aMessage.name) {
case "Settings:Get:OK":
for (let i in msg.settings) {
msg.settings[i] = this._wrap(msg.settings[i]);
}
this._open = true;
Services.DOMRequest.fireSuccess(req.request, this._wrap(msg.settings));
this._open = false;
break;
case "Settings:Set:OK":
case "Settings:Clear:OK":
this._open = true;
Services.DOMRequest.fireSuccess(req.request, 0);
this._open = false;
break;
case "Settings:Get:KO":
case "Settings:Set:KO":
case "Settings:Clear:KO":
if (DEBUG) debug("error:" + msg.errorMsg);
Services.DOMRequest.fireError(req.request, msg.errorMsg);
break;
default:
if (DEBUG) debug("Wrong message: " + aMessage.name);
}
},
get: function get(aName) {
if (VERBOSE) debug("get (" + this._id + "): " + aName);
if (!this._open) {
dump("Settings lock not open!\n");
throw Components.results.NS_ERROR_ABORT;
}
let req = this.createRequest();
let reqID = this.getRequestId({request: req});
this.sendMessage("Settings:Get", {requestID: reqID,
lockID: this._id,
name: aName});
return req;
},
set: function set(aSettings) {
if (VERBOSE) debug("send: " + JSON.stringify(aSettings));
if (!this._open) {
throw "Settings lock not open";
}
let req = this.createRequest();
let reqID = this.getRequestId({request: req});
this.sendMessage("Settings:Set", {requestID: reqID,
lockID: this._id,
settings: aSettings});
return req;
},
clear: function clear() {
if (VERBOSE) debug("clear");
if (!this._open) {
throw "Settings lock not open";
}
let req = this.createRequest();
let reqID = this.getRequestId({request: req});
this.sendMessage("Settings:Clear", {requestID: reqID,
lockID: this._id});
return req;
},
classID: Components.ID("{60c9357c-3ae0-4222-8f55-da01428470d5}"),
contractID: "@mozilla.org/settingsLock;1",
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
Ci.nsIObserver,
Ci.nsISupportsWeakReference])
};
function SettingsManager() {
this._callbacks = null;
this._isRegistered = false;
this._locks = [];
this._createdLocks = 0;
this._unregisteredLocks = 0;
}
SettingsManager.prototype = {
_wrap: function _wrap(obj) {
return Cu.cloneInto(obj, this._window);
},
set onsettingchange(aHandler) {
this.__DOM_IMPL__.setEventHandler("onsettingchange", aHandler);
this.checkMessageRegistration();
},
get onsettingchange() {
return this.__DOM_IMPL__.getEventHandler("onsettingchange");
},
createLock: function() {
let lock = new SettingsLock(this);
if (VERBOSE) debug("creating lock " + lock._id);
this._locks.push(lock._id);
this._createdLocks++;
return lock;
},
unregisterLock: function(aLockID) {
let lock_index = this._locks.indexOf(aLockID);
if (lock_index != -1) {
if (VERBOSE) debug("Unregistering lock " + aLockID);
this._locks.splice(lock_index, 1);
this._unregisteredLocks++;
}
},
receiveMessage: function(aMessage) {
if (VERBOSE) debug("Settings::receiveMessage: " + aMessage.name);
let msg = aMessage.json;
switch (aMessage.name) {
case "Settings:Change:Return:OK":
if (VERBOSE) debug('data:' + msg.key + ':' + msg.value + '\n');
let event = new this._window.MozSettingsEvent("settingchange", this._wrap({
settingName: msg.key,
settingValue: msg.value
}));
this.__DOM_IMPL__.dispatchEvent(event);
if (this._callbacks && this._callbacks[msg.key]) {
if (VERBOSE) debug("observe callback called! " + msg.key + " " + this._callbacks[msg.key].length);
this._callbacks[msg.key].forEach(function(cb) {
cb(this._wrap({settingName: msg.key, settingValue: msg.value}));
}.bind(this));
} else {
if (VERBOSE) debug("no observers stored!");
}
break;
default:
if (DEBUG) debug("Wrong message: " + aMessage.name);
}
},
// If we have either observer callbacks or an event handler,
// register for messages from the main thread. Otherwise, if no one
// is listening, unregister to reduce parent load.
checkMessageRegistration: function checkRegistration() {
let handler = this.__DOM_IMPL__.getEventHandler("onsettingchange");
if (!this._isRegistered) {
if (VERBOSE) debug("Registering for messages");
cpmm.sendAsyncMessage("Settings:RegisterForMessages",
undefined,
undefined,
this._window.document.nodePrincipal);
this._isRegistered = true;
} else {
if ((!this._callbacks || Object.keys(this._callbacks).length == 0) &&
!handler) {
if (VERBOSE) debug("Unregistering for messages");
cpmm.sendAsyncMessage("Settings:UnregisterForMessages",
undefined,
undefined,
this._window.document.nodePrincipal);
this._isRegistered = false;
this._callbacks = null;
}
}
},
addObserver: function addObserver(aName, aCallback) {
if (VERBOSE) debug("addObserver " + aName);
if (!this._callbacks) {
this._callbacks = {};
}
if (!this._callbacks[aName]) {
this._callbacks[aName] = [aCallback];
} else {
this._callbacks[aName].push(aCallback);
}
let length = this._callbacks[aName].length;
if (length >= kObserverSoftLimit) {
debug("WARNING: MORE THAN " + kObserverSoftLimit + " OBSERVERS FOR " +
aName + ": " + length + " FROM" + (new Error).stack);
#ifdef DEBUG
debug("JS STOPS EXECUTING AT THIS POINT IN DEBUG BUILDS!");
throw Components.results.NS_ERROR_ABORT;
#endif
}
this.checkMessageRegistration();
},
removeObserver: function removeObserver(aName, aCallback) {
if (VERBOSE) debug("deleteObserver " + aName);
if (this._callbacks && this._callbacks[aName]) {
let index = this._callbacks[aName].indexOf(aCallback);
if (index != -1) {
this._callbacks[aName].splice(index, 1);
if (this._callbacks[aName].length == 0) {
delete this._callbacks[aName];
}
} else {
if (VERBOSE) debug("Callback not found for: " + aName);
}
} else {
if (VERBOSE) debug("No observers stored for " + aName);
}
this.checkMessageRegistration();
},
init: function(aWindow) {
if (VERBOSE) debug("SettingsManager init");
mrm.registerStrongReporter(this);
cpmm.addMessageListener("Settings:Change:Return:OK", this);
Services.obs.addObserver(this, "inner-window-destroyed", false);
let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
this.innerWindowID = util.currentInnerWindowID;
this._window = aWindow;
},
observe: function(aSubject, aTopic, aData) {
if (VERBOSE) debug("Topic: " + aTopic);
if (aTopic === "inner-window-destroyed") {
let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
if (wId === this.innerWindowID) {
if (DEBUG) debug("Received: inner-window-destroyed for valid innerWindowID=" + wId + ", cleanup.");
this.cleanup();
}
}
},
collectReports: function(aCallback, aData, aAnonymize) {
for (let topic in this._callbacks) {
let length = this._callbacks[topic].length;
if (length == 0) {
continue;
}
let path;
if (length < kObserverSoftLimit) {
path = "settings-observers";
} else {
path = "settings-observers-suspect/referent(topic=" +
(aAnonymize ? "<anonymized>" : topic) + ")";
}
aCallback.callback("", path,
Ci.nsIMemoryReporter.KIND_OTHER,
Ci.nsIMemoryReporter.UNITS_COUNT,
length,
"The number of settings observers for this topic.",
aData);
}
aCallback.callback("",
"settings-locks/alive",
Ci.nsIMemoryReporter.KIND_OTHER,
Ci.nsIMemoryReporter.UNITS_COUNT,
this._locks.length,
"The number of locks that are currently alives.",
aData);
aCallback.callback("",
"settings-locks/created",
Ci.nsIMemoryReporter.KIND_OTHER,
Ci.nsIMemoryReporter.UNITS_COUNT,
this._createdLocks,
"The number of locks that were created.",
aData);
aCallback.callback("",
"settings-locks/deleted",
Ci.nsIMemoryReporter.KIND_OTHER,
Ci.nsIMemoryReporter.UNITS_COUNT,
this._unregisteredLocks,
"The number of locks that were deleted.",
aData);
},
cleanup: function() {
Services.obs.removeObserver(this, "inner-window-destroyed");
// At this point, the window is dying, so there's nothing left
// that we could do with our lock. Go ahead and run finalize on
// it to make sure changes are commited.
for (let i = 0; i < this._locks.length; ++i) {
if (DEBUG) debug("Lock alive at destroy, finalizing: " + this._locks[i]);
// Due to bug 1105511 we should be able to send this without
// cached principals. However, this is scary because any iframe
// in the process could run this?
cpmm.sendAsyncMessage("Settings:Finalize",
{lockID: this._locks[i]});
}
cpmm.removeMessageListener("Settings:Change:Return:OK", this);
mrm.unregisterStrongReporter(this);
this.innerWindowID = null;
this._window = null;
},
classID: Components.ID("{c40b1c70-00fb-11e2-a21f-0800200c9a66}"),
contractID: "@mozilla.org/settingsManager;1",
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
Ci.nsIDOMGlobalPropertyInitializer,
Ci.nsIObserver,
Ci.nsIMemoryReporter]),
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsManager, SettingsLock]);