Bug 1027898 - Implement most of nsIContentPrefService2 for e10s. r=adw
authorBlake Kaplan <mrbkap@gmail.com>
Mon, 11 Aug 2014 11:13:36 -0700
changeset 198875 84d14acbb09bbf86903d8a1fe81c2b00c4197975
parent 198874 f2e3009e1bd198c1aefaf9c543acbec0996c439c
child 198876 d20cfaa4220b62da01767efde0e6df99e1145cdb
push id47521
push usermrbkap@mozilla.com
push dateMon, 11 Aug 2014 18:13:51 +0000
treeherdermozilla-inbound@7f5a7dc73420 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1027898
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1027898 - Implement most of nsIContentPrefService2 for e10s. r=adw
browser/components/nsBrowserGlue.js
toolkit/components/contentprefs/ContentPrefService2.jsm
toolkit/components/contentprefs/ContentPrefServiceChild.jsm
toolkit/components/contentprefs/ContentPrefServiceParent.jsm
toolkit/components/contentprefs/ContentPrefUtils.jsm
toolkit/components/contentprefs/moz.build
toolkit/components/contentprefs/nsContentPrefService.js
toolkit/components/contentprefs/tests/mochitest/mochitest.ini
toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -76,18 +76,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
                                   "resource://gre/modules/PlacesBackups.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 
- XPCOMUtils.defineLazyModuleGetter(this, "RemotePrompt",
-                                   "resource:///modules/RemotePrompt.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RemotePrompt",
+                                  "resource:///modules/RemotePrompt.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ContentPrefServiceParent",
+                                  "resource://gre/modules/ContentPrefServiceParent.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
                                   "resource:///modules/sessionstore/SessionStore.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
                                   "resource:///modules/BrowserUITelemetry.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
@@ -502,20 +505,19 @@ BrowserGlue.prototype = {
     ShumwayUtils.init();
 #endif
     webrtcUI.init();
     AboutHome.init();
     SessionStore.init();
     BrowserUITelemetry.init();
     ContentSearch.init();
 
-    if (Services.appinfo.browserTabsRemote) {
-      ContentClick.init();
-      RemotePrompt.init();
-    }
+    ContentClick.init();
+    RemotePrompt.init();
+    ContentPrefServiceParent.init();
 
     LoginManagerParent.init();
 
     Services.obs.notifyObservers(null, "browser-ui-startup-complete", "");
   },
 
   _checkForOldBuildUpdates: function () {
     // check for update if our build is old
--- a/toolkit/components/contentprefs/ContentPrefService2.jsm
+++ b/toolkit/components/contentprefs/ContentPrefService2.jsm
@@ -24,17 +24,17 @@
 
 let EXPORTED_SYMBOLS = [
   "ContentPrefService2",
 ];
 
 const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/ContentPrefUtils.jsm");
 Cu.import("resource://gre/modules/ContentPrefStore.jsm");
 
 function ContentPrefService2(cps) {
   this._cps = cps;
   this._cache = cps._cache;
   this._pbStore = cps._privModeStorage;
 }
 
@@ -818,49 +818,16 @@ ContentPrefService2.prototype = {
     if (supportedIIDs.some(function (i) iid.equals(i)))
       return this;
     if (iid.equals(Ci.nsIContentPrefService))
       return this._cps;
     throw Cr.NS_ERROR_NO_INTERFACE;
   },
 };
 
-function ContentPref(domain, name, value) {
-  this.domain = domain;
-  this.name = name;
-  this.value = value;
-}
-
-ContentPref.prototype = {
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPref]),
-};
-
-function cbHandleResult(callback, pref) {
-  safeCallback(callback, "handleResult", [pref]);
-}
-
-function cbHandleCompletion(callback, reason) {
-  safeCallback(callback, "handleCompletion", [reason]);
-}
-
-function cbHandleError(callback, nsresult) {
-  safeCallback(callback, "handleError", [nsresult]);
-}
-
-function safeCallback(callbackObj, methodName, args) {
-  if (!callbackObj || typeof(callbackObj[methodName]) != "function")
-    return;
-  try {
-    callbackObj[methodName].apply(callbackObj, args);
-  }
-  catch (err) {
-    Cu.reportError(err);
-  }
-}
-
 function checkGroupArg(group) {
   if (!group || typeof(group) != "string")
     throw invalidArg("domain must be nonempty string.");
 }
 
 function checkNameArg(name) {
   if (!name || typeof(name) != "string")
     throw invalidArg("name must be nonempty string.");
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefServiceChild.jsm
@@ -0,0 +1,236 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* 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";
+
+this.EXPORTED_SYMBOLS = [ "ContentPrefServiceChild" ];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/ContentPrefUtils.jsm");
+Cu.import("resource://gre/modules/ContentPrefStore.jsm");
+
+// We only need one bit of information out of the context.
+function contextArg(context) {
+  return (context && context.usePrivateBrowsing) ?
+            { usePrivateBrowsing: true } :
+            null;
+}
+
+function NYI() {
+  throw new Error("Do not add any new users of these functions");
+}
+
+function CallbackCaller(callback) {
+  this._callback = callback;
+}
+
+CallbackCaller.prototype = {
+  handleResult: function(contentPref) {
+    cbHandleResult(this._callback,
+                   new ContentPref(contentPref.domain,
+                                   contentPref.name,
+                                   contentPref.value));
+  },
+
+  handleError: function(result) {
+    cbHandleError(this._callback, result);
+  },
+
+  handleCompletion: function(reason) {
+    cbHandleCompletion(this._callback, reason);
+  },
+};
+
+let ContentPrefServiceChild = {
+  QueryInterface: XPCOMUtils.generateQI([ Ci.nsIContentPrefService2 ]),
+
+  // Map from pref name -> set of observers
+  _observers: new Map(),
+
+  _mm: Cc["@mozilla.org/childprocessmessagemanager;1"]
+         .getService(Ci.nsIMessageSender),
+
+  _getRandomId: function() {
+    return Cc["@mozilla.org/uuid-generator;1"]
+             .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+  },
+
+  // Map from random ID string -> CallbackCaller, per request
+  _requests: new Map(),
+
+  init: function() {
+    this._mm.addMessageListener("ContentPrefs:HandleResult", this);
+    this._mm.addMessageListener("ContentPrefs:HandleError", this);
+    this._mm.addMessageListener("ContentPrefs:HandleCompletion", this);
+  },
+
+  receiveMessage: function(msg) {
+    let data = msg.data;
+    let callback;
+    switch (msg.name) {
+      case "ContentPrefs:HandleResult":
+        callback = this._requests.get(data.requestId);
+        callback.handleResult(data.contentPref);
+        break;
+
+      case "ContentPrefs:HandleError":
+        callback = this._requests.get(data.requestId);
+        callback.handleError(data.error);
+        break;
+
+      case "ContentPrefs:HandleCompletion":
+        callback = this._requests.get(data.requestId);
+        this._requests.delete(data.requestId);
+        callback.handleCompletion(data.reason);
+        break;
+
+      case "ContentPrefs:NotifyObservers": {
+        let observerList = this._observers.get(data.name);
+        if (!observerList)
+          break;
+
+        for (let observer of observerList) {
+          safeCallback(observer, data.callback, data.args);
+        }
+
+        break;
+      }
+    }
+  },
+
+  _callFunction: function(call, args, callback) {
+    let requestId = this._getRandomId();
+    let data = { call: call, args: args, requestId: requestId };
+
+    this._mm.sendAsyncMessage("ContentPrefs:FunctionCall", data);
+
+    this._requests.set(requestId, new CallbackCaller(callback));
+  },
+
+  getByName: function(name, context, callback) {
+    return this._callFunction("getByName",
+                              [ name, contextArg(context) ],
+                              callback);
+  },
+
+  getByDomainAndName: function(domain, name, context, callback) {
+    return this._callFunction("getByDomainAndName",
+                              [ domain, name, contextArg(context) ],
+                              callback);
+  },
+
+  getBySubdomainAndName: function(domain, name, context, callback) {
+    return this._callFunction("getBySubdomainAndName",
+                              [ domain, name, contextArg(context) ],
+                              callback);
+  },
+
+  getGlobal: function(name, context, callback) {
+    return this._callFunction("getGlobal",
+                              [ name, contextArg(context) ],
+                              callback);
+  },
+
+  getCachedByDomainAndName: NYI,
+  getCachedBySubdomainAndName: NYI,
+  getCachedGlobal: NYI,
+
+  set: function(domain, name, value, context, callback) {
+    this._callFunction("set",
+                       [ domain, name, value, contextArg(context) ],
+                       callback);
+  },
+
+  setGlobal: function(name, value, context, callback) {
+    this._callFunction("setGlobal",
+                       [ name, value, contextArg(context) ],
+                       callback);
+  },
+
+  removeByDomainAndName: function(domain, name, context, callback) {
+    this._callFunction("removeByDomainAndName",
+                       [ domain, name, contextArg(context) ],
+                       callback);
+  },
+
+  removeBySubdomainAndName: function(domain, name, context, callback) {
+    this._callFunction("removeBySubdomainAndName",
+                       [ domain, name, contextArg(context) ],
+                       callback);
+  },
+
+  removeGlobal: function(name, context, callback) {
+    this._callFunction("removeGlobal", [ name, contextArg(context) ], callback);
+  },
+
+  removeByDomain: function(domain, context, callback) {
+    this._callFunction("removeByDomain", [ domain, contextArg(context) ],
+                       callback);
+  },
+
+  removeBySubdomain: function(domain, context, callback) {
+    this._callFunction("removeBySubdomain", [ domain, contextArg(context) ],
+                       callback);
+  },
+
+  removeByName: function(name, context, callback) {
+    this._callFunction("removeByName", [ name, value, contextArg(context) ],
+                       callback);
+  },
+
+  removeAllDomains: function(context, callback) {
+    this._callFunction("removeAllDomains", [ contextArg(context) ], callback);
+  },
+
+  removeAllGlobals: function(context, callback) {
+    this._callFunction("removeAllGlobals", [ contextArg(context) ],
+                       callback);
+  },
+
+  addObserverForName: function(name, observer) {
+    let set = this._observers.get(name);
+    if (!set) {
+      set = new Set();
+      if (this._observers.size === 0) {
+        // This is the first observer of any kind. Start listening for changes.
+        this._mm.addMessageListener("ContentPrefs:NotifyObservers", this);
+      }
+
+      // This is the first observer for this name. Start listening for changes
+      // to it.
+      this._mm.sendAsyncMessage("ContentPrefs:AddObserverForName", { name: name });
+      this._observers.set(name, set);
+    }
+
+    set.add(observer);
+  },
+
+  removeObserverForName: function(name, observer) {
+    let set = this._observers.get(name);
+    if (!set)
+      return;
+
+    set.delete(observer);
+    if (set.size === 0) {
+      // This was the last observer for this name. Stop listening for changes.
+      this._mm.sendAsyncMessage("ContentPrefs:RemoveObserverForName", { name: name });
+
+      this._observers.delete(name);
+      if (this._observers.size === 0) {
+        // This was the last observer for this process. Stop listing for all
+        // changes.
+        this._mm.removeMessageListener("ContentPrefs:NotifyObservers", this);
+      }
+    }
+  },
+
+  extractDomain: NYI
+};
+
+ContentPrefServiceChild.init();
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefServiceParent.jsm
@@ -0,0 +1,127 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* 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";
+
+this.EXPORTED_SYMBOLS = [ "ContentPrefServiceParent" ];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+let ContentPrefServiceParent = {
+  _cps2: null,
+
+  init: function() {
+    let globalMM = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+                     .getService(Ci.nsIMessageListenerManager);
+
+    this._cps2 = Cc["@mozilla.org/content-pref/service;1"]
+                  .getService(Ci.nsIContentPrefService2);
+
+    globalMM.addMessageListener("ContentPrefs:FunctionCall", this);
+
+    let observerChangeHandler = this.handleObserverChange.bind(this);
+    globalMM.addMessageListener("ContentPrefs:AddObserverForName", observerChangeHandler);
+    globalMM.addMessageListener("ContentPrefs:RemoveObserverForName", observerChangeHandler);
+    globalMM.addMessageListener("child-process-shutdown", observerChangeHandler);
+  },
+
+  // Map from message manager -> content pref observer.
+  _observers: new Map(),
+
+  handleObserverChange: function(msg) {
+    let observer = this._observers.get(msg.target);
+    if (msg.name === "child-process-shutdown") {
+      // If we didn't have any observers for this child process, don't do
+      // anything.
+      if (!observer)
+        return;
+
+      for (let i of observer._names) {
+        this._cps2.removeObserverForName(i, observer);
+      }
+
+      this._observers.delete(msg.target);
+      return;
+    }
+
+    let prefName = msg.data.name;
+    if (msg.name === "ContentPrefs:AddObserverForName") {
+      // The child process is responsible for not adding multiple parent
+      // observers for the same name.
+      if (!observer) {
+        observer = {
+          onContentPrefSet: function(group, name, value) {
+            msg.target.sendAsyncMessage("ContentPrefs:NotifyObservers",
+                                        { name: name, callback: "onContentPrefSet",
+                                          args: [ group, name, value ] });
+          },
+
+          onContentPrefRemoved: function(group, name) {
+            msg.target.sendAsyncMessage("ContentPrefs:NotifyObservers",
+                                        { name: name, callback: "onContentPrefRemoved",
+                                          args: [ group, name ] });
+          },
+
+          // The names we're using this observer object for, used to keep track
+          // of the number of names we care about as well as for removing this
+          // observer if its associated process goes away.
+          _names: new Set()
+        };
+
+        this._observers.set(msg.target, observer);
+      }
+
+      observer._names.add(prefName);
+
+      this._cps2.addObserverForName(prefName, observer);
+    } else {
+      // RemoveObserverForName
+
+      // We must have an observer.
+      this._cps2.removeObserverForName(prefName, observer);
+
+      observer._names.delete(prefName);
+      if (observer._names.size === 0) {
+        // This was the last use for this observer.
+        this._observers.delete(msg.target);
+      }
+    }
+  },
+
+  receiveMessage: function(msg) {
+    let data = msg.data;
+
+    let args = data.args;
+    let requestId = data.requestId;
+
+    let listener = {
+      handleResult: function(pref) {
+        msg.target.sendAsyncMessage("ContentPrefs:HandleResult",
+                                    { requestId: requestId,
+                                      contentPref: pref });
+      },
+
+      handleError: function(error) {
+        msg.target.sendAsyncMessage("ContentPrefs:HandleError",
+                                    { requestId: requestId,
+                                      error: error });
+      },
+
+      handleCompletion: function(reason) {
+        msg.target.sendAsyncMessage("ContentPrefs:HandleCompletion",
+                                    { requestId: requestId,
+                                      reason: reason });
+      }
+    };
+
+    // Push our special listener.
+    args.push(listener);
+
+    // And call the function.
+    this._cps2[data.call](...args);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefUtils.jsm
@@ -0,0 +1,52 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* 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";
+
+this.EXPORTED_SYMBOLS = [
+  "ContentPref",
+  "cbHandleResult",
+  "cbHandleError",
+  "cbHandleCompletion",
+  "safeCallback",
+];
+
+const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function ContentPref(domain, name, value) {
+  this.domain = domain;
+  this.name = name;
+  this.value = value;
+}
+
+ContentPref.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPref]),
+};
+
+function cbHandleResult(callback, pref) {
+  safeCallback(callback, "handleResult", [pref]);
+}
+
+function cbHandleCompletion(callback, reason) {
+  safeCallback(callback, "handleCompletion", [reason]);
+}
+
+function cbHandleError(callback, nsresult) {
+  safeCallback(callback, "handleError", [nsresult]);
+}
+
+function safeCallback(callbackObj, methodName, args) {
+  if (!callbackObj || typeof(callbackObj[methodName]) != "function")
+    return;
+  try {
+    callbackObj[methodName].apply(callbackObj, args);
+  }
+  catch (err) {
+    Cu.reportError(err);
+  }
+}
--- a/toolkit/components/contentprefs/moz.build
+++ b/toolkit/components/contentprefs/moz.build
@@ -4,19 +4,26 @@
 # 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/.
 
 XPCSHELL_TESTS_MANIFESTS += [
     'tests/unit/xpcshell.ini',
     'tests/unit_cps2/xpcshell.ini',
 ]
 
+MOCHITEST_MANIFESTS += [
+    'tests/mochitest/mochitest.ini'
+]
+
 EXTRA_COMPONENTS += [
     'nsContentPrefService.js',
     'nsContentPrefService.manifest',
 ]
 
 EXTRA_JS_MODULES += [
     'ContentPrefInstance.jsm',
     'ContentPrefService2.jsm',
+    'ContentPrefServiceChild.jsm',
+    'ContentPrefServiceParent.jsm',
     'ContentPrefStore.jsm',
+    'ContentPrefUtils.jsm',
 ]
 
--- a/toolkit/components/contentprefs/nsContentPrefService.js
+++ b/toolkit/components/contentprefs/nsContentPrefService.js
@@ -5,18 +5,24 @@
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 const CACHE_MAX_GROUP_ENTRIES = 100;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 
 function ContentPrefService() {
+  if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
+    return Cu.import("resource://gre/modules/ContentPrefServiceChild.jsm")
+             .ContentPrefServiceChild;
+  }
+
   // If this throws an exception, it causes the getService call to fail,
   // but the next time a consumer tries to retrieve the service, we'll try
   // to initialize the database again, which might work if the failure
   // was due to a temporary condition (like being out of disk space).
   this._dbInit();
 
   this._observerSvc.addObserver(this, "last-pb-context-exited", false);
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/mochitest/mochitest.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+[test_remoteContentPrefs.html]
+skip-if = buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android' || e10s #bug 783513 # b2g(nested ipc not working) b2g-debug(nested ipc not working) b2g-desktop(nested ipc not working)
new file mode 100644
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html
@@ -0,0 +1,296 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for nsIContentPrefService2 in child processes</title>
+  <script type="application/javascript"
+          src="/tests/SimpleTest/SimpleTest.js">
+  </script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+  <script type="application/javascript;version=1.8">
+    "use strict";
+
+    SimpleTest.waitForExplicitFinish();
+
+    const childFrameURL =
+      "data:text/html,<!DOCTYPE HTML><html><body></body></html>";
+
+    function childFrameScript() {
+      "use strict";
+
+      function Tester(resultArray) {
+        this.results = [];
+      }
+
+      Tester.prototype.is =
+        function(a, b, note) {
+          this.results.push([a === b, note + " (" + a + ", " + b + ")"]);
+        };
+      Tester.prototype.ok =
+        function(b, note) {
+          this.results.push([b != false, note]);
+        };
+
+      var cps = Components.classes["@mozilla.org/content-pref/service;1"]
+                          .getService(Components.interfaces.nsIContentPrefService2);
+
+      let test = null;
+      function* test1(message) {
+        let tester = new Tester();
+
+        tester.ok(cps !== null, "got the content pref service");
+
+        cps.setGlobal("testing", 42, null, {
+          handleCompletion: function(reason) {
+            tester.is(reason, 0, "set a pref?");
+            test.next();
+          }
+        });
+
+        yield;
+
+        let numResults = 0;
+        cps.getGlobal("testing", null, {
+          handleResult: function(pref) {
+            numResults++;
+            tester.is(pref.name, "testing", "pref has the right name");
+            tester.is(pref.value, 42, "pref has the right value");
+          },
+
+          handleCompletion: function(reason) {
+            tester.is(reason, 0, "get a pref?");
+            tester.is(numResults, 1, "got the right number of prefs");
+            tester.is(test.next().done, true, "done with test1");
+            message.target.sendAsyncMessage("testRemoteContentPrefs:test1Finished",
+                                            { results: tester.results });
+          }
+        });
+
+        yield;
+      }
+
+      function* test2(message) {
+        let tester = new Tester();
+
+        let observer;
+        let removed = false;
+        cps.addObserverForName("testName", observer = {
+          onContentPrefSet: function(group, name, value) {
+            if (removed) {
+              message.target.sendAsyncMessage("testRemoteContentPrefs:fail",
+                                              { reason: "unexpected notification" });
+            }
+            tester.is(group, null, "group should be null");
+            tester.is(name, "testName", "should only see testName");
+            tester.is(value, 42, "value should be correct");
+
+            message.target.sendAsyncMessage("testRemoteContentPrefs:test2poke2", {})
+          },
+
+          onContentPrefRemoved: function(group, name) {
+            tester.is(group, null, "group should be null");
+            tester.is(name, "testName");
+            tester.is(test.next().done, true, "should be done with test2");
+
+            cps.removeObserverForName("testName", observer);
+            removed = true;
+
+            message.target.sendAsyncMessage("testRemoteContentPrefs:test2Finished",
+                                            { results: tester.results });
+          }
+        });
+
+        message.target.sendAsyncMessage("testRemoteContentPrefs:test2poke", {});
+        yield;
+      }
+
+      function* test3(message) {
+        let tester = new Tester();
+
+        cps.setGlobal("testName", 42, null, {
+          handleCompletion: function(reason) {
+            tester.is(reason, 0, "set a pref");
+            cps.set("http://mochi.test", "testpref", "str", null, {
+              handleCompletion: function(reason) {
+                tester.is(reason, 0, "set a pref");
+                test.next();
+              }
+            });
+          }
+        });
+
+        yield;
+
+        cps.removeByDomain("http://mochi.test", null, {
+          handleCompletion: function(reason) {
+            tester.is(reason, 0, "remove succeeded");
+            cps.getByDomainAndName("http://mochi.test", "testpref", null, {
+              handleResult: function() {
+                message.target.sendAsyncMessage("testRemoteContentPrefs:fail",
+                                                { reason: "got removed pref in test3" });
+              },
+              handleCompletion: function() {
+                test.next();
+              }
+            });
+          }
+        });
+
+        yield;
+
+        message.target.sendAsyncMessage("testRemoteContentPrefs:test3Finished",
+                                        { results: tester.results });
+      }
+
+      function* test4(message) {
+        let tester = new Tester();
+
+        let prefObserver = {
+          onContentPrefSet: function(group, name, value) {
+            test.next({ group: group, name: name, value: value });
+          },
+          onContentPrefRemoved: function(group, name) {
+            test.next({ group: group, name: name });
+          }
+        };
+
+        addMessageListener("testRemoteContentPrefs:prefResults", (msg) => {
+          test.next(msg.data.results);
+        });
+
+        cps.addObserverForName("test", prefObserver);
+
+        cps.set("http://mochi.test", "test", 42, { usePrivateBrowsing: true });
+        let event = yield;
+        tester.is(event.name, "test");
+
+        message.target.sendAsyncMessage("testRemoteContentPrefs:getPref",
+                                        { group: "http://mochi.test", name: "test" });
+
+        let results = yield;
+        tester.is(results.length, 0, "should not have seen the pb pref");
+
+        message.target.sendAsyncMessage("testRemoteContentPrefs:test4Finished",
+                                        { results: tester.results });
+      }
+
+      addMessageListener("testRemoteContentPrefs:test1", function(message) {
+        test = test1(message);
+        test.next();
+      });
+      addMessageListener("testRemoteContentPrefs:test2", function(message) {
+        test = test2(message);
+        test.next();
+      });
+      addMessageListener("testRemoteContentPrefs:test3", function(message) {
+        test = test3(message);
+        test.next();
+      });
+      addMessageListener("testRemoteContentPrefs:test4", function(message) {
+        test = test4(message);
+        test.next();
+      });
+    }
+
+    function processResults(results) {
+      for (let i of results) {
+        ok(...i);
+      }
+    }
+
+    let test;
+    function* testStructure(mm) {
+      let lastResult;
+
+      function testDone(msg) {
+        test.next(msg.data);
+      }
+
+      mm.addMessageListener("testRemoteContentPrefs:test1Finished", testDone);
+      mm.addMessageListener("testRemoteContentPrefs:test2Finished", testDone);
+      mm.addMessageListener("testRemoteContentPrefs:test3Finished", testDone);
+      mm.addMessageListener("testRemoteContentPrefs:test4Finished", testDone);
+
+      mm.addMessageListener("testRemoteContentPrefs:fail", function(msg) {
+        ok(false, msg.data.reason);
+      });
+
+      mm.sendAsyncMessage("testRemoteContentPrefs:test1", {});
+      lastResult = yield;
+      processResults(lastResult.results);
+
+      var cps = SpecialPowers.Cc["@mozilla.org/content-pref/service;1"]
+                             .getService(SpecialPowers.Ci.nsIContentPrefService2);
+      mm.sendAsyncMessage("testRemoteContentPrefs:test2", {});
+      mm.addMessageListener("testRemoteContentPrefs:test2poke", function() {
+        cps.setGlobal("testName", 42, null);
+      });
+      mm.addMessageListener("testRemoteContentPrefs:test2poke2", function() {
+        cps.removeGlobal("testName", null);
+      });
+
+      lastResult = yield;
+      processResults(lastResult.results);
+
+      mm.sendAsyncMessage("testRemoteContentPrefs:test3", {});
+      lastResult = yield;
+      processResults(lastResult.results);
+
+      mm.addMessageListener("testRemoteContentPrefs:getPref", function(msg) {
+        let results = [];
+        cps.getByDomainAndName(msg.data.group, msg.data.name, null, {
+          handleResult: function(pref) {
+            results.push(pref);
+          },
+          handleCompletion: function(reason) {
+            mm.sendAsyncMessage("testRemoteContentPrefs:prefResults",
+                                { results: results });
+          }
+        });
+      });
+
+      mm.sendAsyncMessage("testRemoteContentPrefs:test4", {});
+      lastResult = yield;
+      processResults(lastResult.results);
+
+      SimpleTest.finish();
+    }
+
+    function runTests() {
+      info("Browser prefs set.");
+
+      let iframe = document.createElement("iframe");
+      SpecialPowers.wrap(iframe).mozbrowser = true;
+      iframe.id = "iframe";
+      iframe.src = childFrameURL;
+
+      iframe.addEventListener("mozbrowserloadend", function() {
+        info("Got iframe load event.");
+        let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+        mm.loadFrameScript("data:,(" + childFrameScript.toString() + ")();",
+                           false);
+
+        test = testStructure(mm);
+        test.next();
+      });
+
+      document.body.appendChild(iframe);
+    }
+
+    addEventListener("load", function() {
+      info("Got load event.");
+
+      SpecialPowers.addPermission("browser", true, document);
+      SpecialPowers.pushPrefEnv({
+        "set": [
+          ["dom.ipc.browser_frames.oop_by_default", true],
+          ["dom.mozBrowserFramesEnabled", true],
+          ["browser.pagethumbnails.capturing_disabled", true]
+        ]
+      }, runTests);
+    });
+  </script>
+</body>
+</html>