Bug 1541557: Part 7 - Convert SpecialPowers to use JSWindowActors rather than framescripts. r=nika
☠☠ backed out by f9bf5e4b0b4f ☠ ☠
authorKris Maglione <maglione.k@gmail.com>
Wed, 12 Jun 2019 10:48:29 -0700
changeset 543730 46ff845a7b0cdabf640bb2e3c783735ab68b7cd1
parent 543729 c2697f04d38cf0b01b1f3e227910ab5890926a33
child 543731 1471732eca80f6fa44ae50b39c0317965cd29671
push id2131
push userffxbld-merge
push dateMon, 26 Aug 2019 18:30:20 +0000
treeherdermozilla-release@b19ffb3ca153 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnika
bugs1541557
milestone69.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 1541557: Part 7 - Convert SpecialPowers to use JSWindowActors rather than framescripts. r=nika Differential Revision: https://phabricator.services.mozilla.com/D35057
browser/base/content/test/performance/browser_startup_content.js
devtools/shared/webconsole/test/test_console_serviceworker_cached.html
docshell/test/chrome/bug89419_window.xul
dom/html/test/test_bug1146116.html
editor/spellchecker/tests/test_bug1209414.html
testing/mochitest/tests/SimpleTest/ChromePowers.js
testing/specialpowers/api.js
testing/specialpowers/content/SpecialPowers.jsm
testing/specialpowers/content/SpecialPowersAPI.jsm
testing/specialpowers/content/SpecialPowersAPIParent.jsm
testing/specialpowers/content/SpecialPowersChild.jsm
testing/specialpowers/content/SpecialPowersObserver.jsm
testing/specialpowers/content/SpecialPowersObserverAPI.jsm
testing/specialpowers/content/SpecialPowersParent.jsm
testing/specialpowers/content/specialpowersFrameScript.js
testing/specialpowers/moz.build
toolkit/components/normandy/test/browser/browser_actions_AddonStudyAction.js
toolkit/components/normandy/test/browser/browser_actions_BranchedAddonStudyAction.js
--- a/browser/base/content/test/performance/browser_startup_content.js
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -15,17 +15,17 @@
 "use strict";
 
 /* Set this to true only for debugging purpose; it makes the output noisy. */
 const kDumpAllStacks = false;
 
 const whitelist = {
   modules: new Set([
     "chrome://mochikit/content/ShutdownLeaksCollector.jsm",
-    "resource://specialpowers/SpecialPowers.jsm",
+    "resource://specialpowers/SpecialPowersChild.jsm",
     "resource://specialpowers/SpecialPowersAPI.jsm",
     "resource://specialpowers/WrapPrivileged.jsm",
 
     "resource://gre/modules/ContentProcessSingleton.jsm",
 
     // General utilities
     "resource://gre/modules/AppConstants.jsm",
     "resource://gre/modules/AsyncShutdown.jsm",
@@ -60,17 +60,16 @@ const whitelist = {
 
     // Extensions
     "resource://gre/modules/ExtensionProcessScript.jsm",
     "resource://gre/modules/ExtensionUtils.jsm",
     "resource://gre/modules/MessageChannel.jsm",
   ]),
   frameScripts: new Set([
     // Test related
-    "resource://specialpowers/specialpowersFrameScript.js",
     "chrome://mochikit/content/shutdown-leaks-collector.js",
     "chrome://mochikit/content/tests/SimpleTest/AsyncUtilsContent.js",
     "chrome://mochikit/content/tests/BrowserTestUtils/content-utils.js",
 
     // Browser front-end
     "chrome://global/content/browser-content.js",
 
     // Forms
--- a/devtools/shared/webconsole/test/test_console_serviceworker_cached.html
+++ b/devtools/shared/webconsole/test/test_console_serviceworker_cached.html
@@ -87,17 +87,17 @@ let startTest = async function () {
   await swr.unregister();
 
   SimpleTest.finish();
 };
 addEventListener("load", startTest);
 
 // This test needs to add tabs that are controlled by a service worker
 // so use some special powers to dig around and find gBrowser
-let {gBrowser} = SpecialPowers._getTopChromeWindow(SpecialPowers.window.get());
+let {gBrowser} = SpecialPowers._getTopChromeWindow(SpecialPowers.window);
 
 SimpleTest.registerCleanupFunction(() => {
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
 });
 
 function addTab(url) {
--- a/docshell/test/chrome/bug89419_window.xul
+++ b/docshell/test/chrome/bug89419_window.xul
@@ -4,23 +4,17 @@
 <window id="89419Test"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         width="600"
         height="600"
         onload="setTimeout(nextTest, 0);"
         title="bug 89419 test">
 
   <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
-  <script>
-    ChromeUtils.import("resource://specialpowers/SpecialPowers.jsm", this);
-
-    window.addMessageListener = function() {};
-    window.removeMessageListener = function() {};
-    window.SpecialPowers = new SpecialPowers(window, window);
-  </script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/ChromePowers.js"/>
   <script type="application/javascript" src="docshell_helpers.js" />
   <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script>
 
   <script type="application/javascript"><![CDATA[
     // Define the generator-iterator for the tests.
     var tests = testIterator();
 
     ////
--- a/dom/html/test/test_bug1146116.html
+++ b/dom/html/test/test_bug1146116.html
@@ -34,18 +34,18 @@ function getGlobal(thing) {
   return SpecialPowers.unwrap(SpecialPowers.Cu.getGlobalForObject(thing));
 }
 
 function onFileOpened(message) {
   const file = message.domFile;
   const elem = document.getElementById("file");
   is(getGlobal(elem), window,
      "getGlobal() works as expected");
-  isnot(getGlobal(file), window,
-        "File from MessageManager is wrapped");
+  is(getGlobal(file), window,
+     "File from MessageManager is not wrapped");
   SpecialPowers.wrap(elem).mozSetFileArray([file]);
   is(getGlobal(elem.files[0]), window,
      "File read back from input element is not wrapped");
   helper.addMessageListener("file.removed", onFileRemoved);
   helper.sendAsyncMessage("file.remove", null);
 }
 
 function onFileRemoved() {
--- a/editor/spellchecker/tests/test_bug1209414.html
+++ b/editor/spellchecker/tests/test_bug1209414.html
@@ -37,22 +37,22 @@ var script;
  */
 
 var onSpellCheck =
   SpecialPowers.Cu.import(
     "resource://testing-common/AsyncSpellCheckTestHelper.jsm").onSpellCheck;
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(async function() {
-  /* global browserElement */
+  /* global actorParent */
   /* eslint-env mozilla/frame-script */
   script = SpecialPowers.loadChromeScript(function() {
-    var chromeWin = browserElement.ownerGlobal.docShell
-                    .rootTreeItem.domWindow
-                    .QueryInterface(Ci.nsIDOMChromeWindow);
+    var chromeWin = actorParent.rootFrameLoader
+                    .ownerElement.ownerGlobal.docShell
+                    .rootTreeItem.domWindow;
     var contextMenu = chromeWin.document.getElementById("contentAreaContextMenu");
     contextMenu.addEventListener("popupshown",
                                  () => sendAsyncMessage("popupshown"));
 
     // eslint-disable-next-line mozilla/use-services
     var dir = Cc["@mozilla.org/file/directory_service;1"]
                 .getService(Ci.nsIProperties)
                 .get("CurWorkD", Ci.nsIFile);
--- a/testing/mochitest/tests/SimpleTest/ChromePowers.js
+++ b/testing/mochitest/tests/SimpleTest/ChromePowers.js
@@ -1,48 +1,62 @@
 /* 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/. */
 
-ChromeUtils.import("resource://specialpowers/SpecialPowersAPI.jsm", this);
+const {SpecialPowersAPI, bindDOMWindowUtils} = ChromeUtils.import("resource://specialpowers/SpecialPowersAPI.jsm");
+const {SpecialPowersAPIParent} = ChromeUtils.import("resource://specialpowers/SpecialPowersAPIParent.jsm");
 
 class ChromePowers extends SpecialPowersAPI {
   constructor(window) {
     super();
 
     this.window = Cu.getWeakReference(window);
 
     this.chromeWindow = window;
 
     this.DOMWindowUtils = bindDOMWindowUtils(window);
 
-    this.spObserver = new SpecialPowersObserverAPI();
-    this.spObserver._sendReply = this._sendReply.bind(this);
+    this.parentActor = new SpecialPowersAPIParent();
+    this.parentActor.sendAsyncMessage = this.sendReply.bind(this);
+
     this.listeners = new Map();
   }
 
   toString() { return "[ChromePowers]"; }
   sanityCheck() { return "foo"; }
 
-  _sendReply(aOrigMsg, aType, aMsg) {
-    var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
+  get contentWindow() {
+    return window;
+  }
+
+  get document() {
+    return window.document;
+  }
+
+  get docShell() {
+    return window.docShell;
+  }
+
+  sendReply(aType, aMsg) {
+    var msg = {name: aType, json: aMsg, data: aMsg};
     if (!this.listeners.has(aType)) {
       throw new Error(`No listener for ${aType}`);
     }
     this.listeners.get(aType)(msg);
   }
 
-  _sendSyncMessage(aType, aMsg) {
-    var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
-    return [this._receiveMessage(msg)];
+  sendAsyncMessage(aType, aMsg) {
+    var msg = {name: aType, json: aMsg, data: aMsg};
+    this.receiveMessage(msg);
   }
 
-  _sendAsyncMessage(aType, aMsg) {
-    var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
-    this._receiveMessage(msg);
+  async sendQuery(aType, aMsg) {
+    var msg = {name: aType, json: aMsg, data: aMsg};
+    return this.receiveMessage(msg);
   }
 
   _addMessageListener(aType, aCallback) {
     if (this.listeners.has(aType)) {
       throw new Error(`unable to handle multiple listeners for ${aType}`);
     }
     this.listeners.set(aType, aCallback);
   }
@@ -53,36 +67,32 @@ class ChromePowers extends SpecialPowers
   registerProcessCrashObservers() {
     this._sendSyncMessage("SPProcessCrashService", { op: "register-observer" });
   }
 
   unregisterProcessCrashObservers() {
     this._sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" });
   }
 
-  _receiveMessage(aMessage) {
+  receiveMessage(aMessage) {
     switch (aMessage.name) {
       case "SpecialPowers.Quit":
         let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
         appStartup.quit(Ci.nsIAppStartup.eForceQuit);
         break;
       case "SPProcessCrashService":
         if (aMessage.json.op == "register-observer" || aMessage.json.op == "unregister-observer") {
           // Hack out register/unregister specifically for browser-chrome leaks
           break;
-        } else if (aMessage.type == "crash-observed") {
-          for (let e of msg.dumpIDs) {
-            this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
-          }
         }
       default:
         // All calls go here, because we need to handle SPProcessCrashService calls as well
-        return this.spObserver._receiveMessageAPI(aMessage);
+        return this.parentActor.receiveMessage(aMessage);
     }
-    return undefined;		// Avoid warning.
+    return undefined;
   }
 
   quit() {
     // We come in here as SpecialPowers.quit, but SpecialPowers is really ChromePowers.
     // For some reason this.<func> resolves to TestRunner, so using SpecialPowers
     // allows us to use the ChromePowers object which we defined below.
     SpecialPowers._sendSyncMessage("SpecialPowers.Quit", {});
   }
@@ -98,13 +108,13 @@ class ChromePowers extends SpecialPowers
   executeAfterFlushingMessageQueue(aCallback) {
     aCallback();
   }
 }
 
 if (window.parent.SpecialPowers && !window.SpecialPowers) {
   window.SpecialPowers = window.parent.SpecialPowers;
 } else {
-  ChromeUtils.import("resource://specialpowers/SpecialPowersObserverAPI.jsm", this);
+  ChromeUtils.import("resource://specialpowers/SpecialPowersAPIParent.jsm", this);
 
   window.SpecialPowers = new ChromePowers(window);
 }
 
--- a/testing/specialpowers/api.js
+++ b/testing/specialpowers/api.js
@@ -1,30 +1,43 @@
 /* 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/. */
 
 /* globals ExtensionAPI */
 
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {FileUtils} = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "resProto",
                                    "@mozilla.org/network/protocol;1?name=resource",
                                    "nsISubstitutingProtocolHandler");
 
 this.specialpowers = class extends ExtensionAPI {
   onStartup() {
     let uri = Services.io.newURI("content/", null, this.extension.rootURI);
     resProto.setSubstitutionWithFlags("specialpowers", uri,
                                       resProto.ALLOW_CONTENT_ACCESS);
 
-    const {SpecialPowersObserver} = ChromeUtils.import("resource://specialpowers/SpecialPowersObserver.jsm");
-    this.observer = new SpecialPowersObserver();
-    this.observer.init();
+    // Register special testing modules.
+    Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
+              .autoRegister(FileUtils.getFile("ProfD", ["tests.manifest"]));
+
+    ChromeUtils.registerWindowActor("SpecialPowers", {
+      allFrames: true,
+      child: {
+        moduleURI: "resource://specialpowers/SpecialPowersChild.jsm",
+        events: {
+          DOMWindowCreated: {},
+        },
+      },
+      parent: {
+        moduleURI: "resource://specialpowers/SpecialPowersParent.jsm",
+      },
+    });
   }
 
   onShutdown() {
-    this.observer.uninit();
-    this.observer = null;
+    ChromeUtils.unregisterWindowActor("SpecialPowers");
     resProto.setSubstitution("specialpowers", null);
   }
 };
--- a/testing/specialpowers/content/SpecialPowersAPI.jsm
+++ b/testing/specialpowers/content/SpecialPowersAPI.jsm
@@ -104,18 +104,20 @@ SPConsoleListener.prototype = {
       Services.console.unregisterListener(this);
     }
   },
 
   QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener,
                                           Ci.nsIObserver]),
 };
 
-class SpecialPowersAPI {
+class SpecialPowersAPI extends JSWindowActorChild {
   constructor() {
+    super();
+
     this._consoleListeners = [];
     this._encounteredCrashDumpFiles = [];
     this._unexpectedCrashDumpFiles = { };
     this._crashDumpDir = null;
     this._mfl = null;
     this._applyingPrefs = false;
     this._applyingPermissions = false;
     this._observingPermissions = false;
@@ -216,17 +218,17 @@ class SpecialPowersAPI {
   /*
    * Load a privileged script that runs same-process. This is different from
    * |loadChromeScript|, which will run in the parent process in e10s mode.
    */
   loadPrivilegedScript(aFunction) {
     var str = "(" + aFunction.toString() + ")();";
     let gGlobalObject = Cu.getGlobalForObject(this);
     let sb = Cu.Sandbox(gGlobalObject);
-    var window = this.window.get();
+    var window = this.contentWindow;
     var mc = new window.MessageChannel();
     sb.port = mc.port1;
     try {
       let blob = new Blob([str], {type: "application/javascript"});
       let blobUrl = URL.createObjectURL(blob);
       Services.scriptloader.loadSubScript(blobUrl, sb);
     } catch (e) {
       throw WrapPrivileged.wrap(e);
@@ -294,17 +296,17 @@ class SpecialPowersAPI {
       // winds up spinning the event loop when loading HTTP URLs. That
       // leads to unexpected out-of-order operations if the child sends
       // a message immediately after loading the script.
       scriptArgs.function = {
         body: this._readUrlAsString(urlOrFunction),
       };
       scriptArgs.url = urlOrFunction;
     }
-    this._sendSyncMessage("SPLoadChromeScript",
+    this.sendAsyncMessage("SPLoadChromeScript",
                           scriptArgs);
 
     // Returns a MessageManager like API in order to be
     // able to communicate with this chrome script
     let listeners = [];
     let chromeScript = {
       addMessageListener: (name, listener) => {
         listeners.push({ name, listener });
@@ -319,64 +321,64 @@ class SpecialPowersAPI {
 
       removeMessageListener: (name, listener) => {
         listeners = listeners.filter(
           o => (o.name != name || o.listener != listener)
         );
       },
 
       sendAsyncMessage: (name, message) => {
-        this._sendSyncMessage("SPChromeScriptMessage",
+        this.sendAsyncMessage("SPChromeScriptMessage",
                               { id, name, message });
       },
 
-      sendQuery: async (name, message) => {
-        // Send a sync query and pretend it's async. This will become a
-        // real async `sendQuery` call after the JSWindowActor
-        // migration.
-        return this._sendSyncMessage("SPChromeScriptMessage",
-                                     { id, name, message })[0][0];
+      sendQuery: (name, message) => {
+        return this.sendQuery("SPChromeScriptMessage",
+                              { id, name, message });
       },
 
       destroy: () => {
         listeners = [];
         this._removeMessageListener("SPChromeScriptMessage", chromeScript);
         this._removeMessageListener("SPChromeScriptAssert", chromeScript);
       },
 
       receiveMessage: (aMessage) => {
         let messageId = aMessage.json.id;
         let name = aMessage.json.name;
         let message = aMessage.json.message;
-        if (this.mm) {
-          message = new StructuredCloneHolder(message).deserialize(this.mm.content);
+        if (this.contentWindow) {
+          message = new StructuredCloneHolder(message).deserialize(this.contentWindow);
         }
         // Ignore message from other chrome script
         if (messageId != id)
-          return;
+          return null;
 
+        let result;
         if (aMessage.name == "SPChromeScriptMessage") {
-          listeners.filter(o => (o.name == name))
-                   .forEach(o => o.listener(message));
+          for (let listener of listeners.filter(o => o.name == name)) {
+            result = listener.listener(message);
+          }
         } else if (aMessage.name == "SPChromeScriptAssert") {
           assert(aMessage.json);
         }
+        return result;
       },
     };
     this._addMessageListener("SPChromeScriptMessage", chromeScript);
     this._addMessageListener("SPChromeScriptAssert", chromeScript);
 
     let assert = json => {
       // An assertion has been done in a mochitest chrome script
       let {name, err, message, stack} = json;
 
       // Try to fetch a test runner from the mochitest
       // in order to properly log these assertions and notify
       // all usefull log observers
-      let window = this.window.get();
+      let window = this.contentWindow;
       let parentRunner, repr = o => o;
       if (window) {
         window = window.wrappedJSObject;
         parentRunner = window.TestRunner;
         if (window.repr) {
           repr = window.repr;
         }
       }
@@ -404,18 +406,18 @@ class SpecialPowersAPI {
         // When we are running only a single mochitest, there is no test runner
         dump(msg + "\n");
       }
     };
 
     return this.wrap(chromeScript);
   }
 
-  importInMainProcess(importString) {
-    var message = this._sendSyncMessage("SPImportInMainProcess", importString)[0];
+  async importInMainProcess(importString) {
+    var message = await this.sendQuery("SPImportInMainProcess", importString);
     if (message.hadError) {
       throw new Error("SpecialPowers.importInMainProcess failed with error " + message.errorMessage);
     }
   }
 
   get Services() {
     return WrapPrivileged.wrap(Services);
   }
@@ -431,17 +433,17 @@ class SpecialPowersAPI {
    * Convenient shortcuts to the standard Components abbreviations.
    */
   get Cc() { return WrapPrivileged.wrap(this.getFullComponents().classes); }
   get Ci() { return WrapPrivileged.wrap(this.getFullComponents().interfaces); }
   get Cu() { return WrapPrivileged.wrap(this.getFullComponents().utils); }
   get Cr() { return WrapPrivileged.wrap(this.getFullComponents().results); }
 
   getDOMWindowUtils(aWindow) {
-    if (aWindow == this.window.get() && this.DOMWindowUtils != null)
+    if (aWindow == this.contentWindow && this.DOMWindowUtils != null)
       return this.DOMWindowUtils;
 
     return bindDOMWindowUtils(aWindow);
   }
 
   /*
    * A method to get a DOMParser that can't parse XUL.
    */
@@ -450,84 +452,74 @@ class SpecialPowersAPI {
     // nullprincipal), it won't be able to parse XUL by default.
     return WrapPrivileged.wrap(new DOMParser());
   }
 
   get InspectorUtils() { return WrapPrivileged.wrap(InspectorUtils); }
 
   get PromiseDebugging() { return WrapPrivileged.wrap(PromiseDebugging); }
 
-  waitForCrashes(aExpectingProcessCrash) {
-    return new Promise((resolve, reject) => {
-      if (!aExpectingProcessCrash) {
-        resolve();
-      }
+  async waitForCrashes(aExpectingProcessCrash) {
+    if (!aExpectingProcessCrash) {
+      return;
+    }
 
-      var crashIds = this._encounteredCrashDumpFiles.filter((filename) => {
-        return ((filename.length === 40) && filename.endsWith(".dmp"));
-      }).map((id) => {
-        return id.slice(0, -4); // Strip the .dmp extension to get the ID
-      });
+    var crashIds = this._encounteredCrashDumpFiles.filter((filename) => {
+      return ((filename.length === 40) && filename.endsWith(".dmp"));
+    }).map((id) => {
+      return id.slice(0, -4); // Strip the .dmp extension to get the ID
+    });
 
-      let self = this;
-      function messageListener(msg) {
-        self._removeMessageListener("SPProcessCrashManagerWait", messageListener);
-        resolve();
-      }
-
-      this._addMessageListener("SPProcessCrashManagerWait", messageListener);
-      this._sendAsyncMessage("SPProcessCrashManagerWait", {
-        crashIds,
-      });
+    await this.sendQuery("SPProcessCrashManagerWait", {
+      crashIds,
     });
   }
 
-  removeExpectedCrashDumpFiles(aExpectingProcessCrash) {
+  async removeExpectedCrashDumpFiles(aExpectingProcessCrash) {
     var success = true;
     if (aExpectingProcessCrash) {
       var message = {
         op: "delete-crash-dump-files",
         filenames: this._encounteredCrashDumpFiles,
       };
-      if (!this._sendSyncMessage("SPProcessCrashService", message)[0]) {
+      if (!await this.sendQuery("SPProcessCrashService", message)) {
         success = false;
       }
     }
     this._encounteredCrashDumpFiles.length = 0;
     return success;
   }
 
-  findUnexpectedCrashDumpFiles() {
+  async findUnexpectedCrashDumpFiles() {
     var self = this;
     var message = {
       op: "find-crash-dump-files",
       crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles,
     };
-    var crashDumpFiles = this._sendSyncMessage("SPProcessCrashService", message)[0];
+    var crashDumpFiles = await this.sendQuery("SPProcessCrashService", message);
     crashDumpFiles.forEach(function(aFilename) {
       self._unexpectedCrashDumpFiles[aFilename] = true;
     });
     return crashDumpFiles;
   }
 
   removePendingCrashDumpFiles() {
     var message = {
       op: "delete-pending-crash-dump-files",
     };
-    var removed = this._sendSyncMessage("SPProcessCrashService", message)[0];
-    return removed;
+    return this.sendQuery("SPProcessCrashService", message);
   }
 
   _setTimeout(callback) {
     // for mochitest-browser
     if (typeof this.chromeWindow != "undefined")
       this.chromeWindow.setTimeout(callback, 0);
     // for mochitest-plain
     else
-      this.mm.content.setTimeout(callback, 0);
+      this.contentWindow.setTimeout(callback, 0);
   }
 
   _delayCallbackTwice(callback) {
      let delayedCallback = () => {
        let delayAgain = (aCallback) => {
          // Using this._setTimeout doesn't work here
          // It causes failures in mochtests that use
          // multiple pushPrefEnv calls
@@ -664,17 +656,17 @@ class SpecialPowersAPI {
    * NOTICE: there is no implementation of _addMessageListener in
    * ChromePowers.js
    */
   registerObservers(topic) {
     var msg = {
       "op": "add",
       "observerTopic": topic,
     };
-    this._sendSyncMessage("SPObserverService", msg);
+    return this.sendQuery("SPObserverService", msg);
   }
 
   permChangedProxy(aMessage) {
     let permission = aMessage.json.permission;
     let aData = aMessage.json.aData;
     this._permissionObserver.observe(permission, aData);
   }
 
@@ -704,18 +696,18 @@ class SpecialPowersAPI {
     while (this._permissionsUndoStack.length > 1)
       this.popPermissions(null);
 
     return this.popPermissions(callback);
   }
 
 
   setTestPluginEnabledState(newEnabledState, pluginName) {
-    return this._sendSyncMessage("SPSetTestPluginEnabledState",
-                                 { newEnabledState, pluginName })[0];
+    return this.sendQuery("SPSetTestPluginEnabledState",
+                          { newEnabledState, pluginName });
   }
 
   /*
     Iterate through one atomic set of permissions actions and perform allow/deny as appropriate.
     All actions performed must modify the relevant permission.
   */
   _applyPermissions() {
     if (this._applyingPermissions || this._pendingPermissions.length <= 0) {
@@ -736,17 +728,17 @@ class SpecialPowersAPI {
     this._permissionObserver._nextCallback = function() {
         self._applyingPermissions = false;
         // Now apply any permissions that may have been queued while we were applying
         self._applyPermissions();
     };
 
     for (var idx in pendingActions) {
       var perm = pendingActions[idx];
-      this._sendSyncMessage("SPPermissionManager", perm)[0];
+      this.sendAsyncMessage("SPPermissionManager", perm);
     }
   }
 
   /**
    * Helper to resolve a promise by calling the resolve function and call an
    * optional callback.
    */
   _resolveAndCallOptionalCallback(resolveFn, callback = null) {
@@ -781,17 +773,17 @@ class SpecialPowersAPI {
    * the behavior of this method is undefined.
    *
    * (Implementation note: _prefEnvUndoStack is a stack of values to revert to,
    * not values which have been set!)
    *
    * TODO: complex values for original cleanup?
    *
    */
-  pushPrefEnv(inPrefs, callback = null) {
+  async _pushPrefEnv(inPrefs) {
     var prefs = Services.prefs;
 
     var pref_string = [];
     pref_string[prefs.PREF_INT] = "INT";
     pref_string[prefs.PREF_BOOL] = "BOOL";
     pref_string[prefs.PREF_STRING] = "CHAR";
 
     var pendingActions = [];
@@ -850,35 +842,42 @@ class SpecialPowersAPI {
         } else {
           cleanupTodo.action = "set";
         }
         cleanupActions.push(cleanupTodo);
       }
     }
 
     return new Promise(resolve => {
-      let done = this._resolveAndCallOptionalCallback.bind(this, resolve, callback);
       if (pendingActions.length > 0) {
         // The callback needs to be delayed twice. One delay is because the pref
         // service doesn't guarantee the order it calls its observers in, so it
         // may notify the observer holding the callback before the other
         // observers have been notified and given a chance to make the changes
         // that the callback checks for. The second delay is because pref
         // observers often defer making their changes by posting an event to the
         // event loop.
         this._prefEnvUndoStack.push(cleanupActions);
         this._pendingPrefs.push([pendingActions,
-                                 this._delayCallbackTwice(done)]);
+                                 this._delayCallbackTwice(resolve)]);
         this._applyPrefs();
       } else {
-        this._setTimeout(done);
+        this._setTimeout(resolve);
       }
     });
   }
 
+  pushPrefEnv(inPrefs, callback = null) {
+    let promise = this._pushPrefEnv(inPrefs);
+    if (callback) {
+      promise.then(callback);
+    }
+    return promise;
+  }
+
   popPrefEnv(callback = null) {
     return new Promise(resolve => {
       let done = this._resolveAndCallOptionalCallback.bind(this, resolve, callback);
       if (this._prefEnvUndoStack.length > 0) {
         // See pushPrefEnv comment regarding delay.
         let cb = this._delayCallbackTwice(done);
         /* Each pop will have a valid block of preferences */
         this._pendingPrefs.push([this._prefEnvUndoStack.pop(), cb]);
@@ -899,17 +898,17 @@ class SpecialPowersAPI {
     });
   }
 
   _isPrefActionNeeded(prefAction) {
     if (prefAction.action === "clear") {
       return Services.prefs.prefHasUserValue(prefAction.name);
     } else if (prefAction.action === "set") {
       try {
-        let currentValue  = this._getPref(prefAction.name, prefAction.type, {});
+        let currentValue = this._getPref(prefAction.name, prefAction.type, {});
         return currentValue != prefAction.value;
       } catch (e) {
         // If the preference is not defined yet, setting the value will have an effect.
         return true;
       }
     }
     // Only "clear" and "set" actions are supported.
     return false;
@@ -930,16 +929,17 @@ class SpecialPowersAPI {
     var pendingActions = transaction[0];
     var callback = transaction[1];
 
     // Filter out all the pending actions that will not have any effect.
     pendingActions = pendingActions.filter(action => {
       return this._isPrefActionNeeded(action);
     });
 
+
     var self = this;
     let onPrefActionsApplied = function() {
       self._setTimeout(callback);
       self._setTimeout(function() {
         self._applyingPrefs = false;
         // Now apply any prefs that may have been queued while we were applying
         self._applyPrefs();
       });
@@ -976,16 +976,19 @@ class SpecialPowersAPI {
   }
   _removeObserverProxy(notification) {
     if (notification in this._proxiedObservers) {
       this._removeMessageListener(notification, this._proxiedObservers[notification]);
     }
   }
 
   addObserver(obs, notification, weak) {
+    // Make sure the parent side exists, or we won't get any notifications.
+    this.sendAsyncMessage("Wakeup");
+
     this._addObserverProxy(notification);
     obs = Cu.waiveXrays(obs);
     if (typeof obs == "object" && obs.observe.name != "SpecialPowersCallbackWrapper")
       obs.observe = WrapPrivileged.wrapCallback(obs.observe);
     Services.obs.addObserver(obs, notification, weak);
   }
   removeObserver(obs, notification) {
     this._removeObserverProxy(notification);
@@ -1056,17 +1059,17 @@ class SpecialPowersAPI {
   }
   getIntPref(...args) {
     return Services.prefs.getIntPref(...args);
   }
   getCharPref(...args) {
     return Services.prefs.getCharPref(...args);
   }
   getComplexValue(prefName, iid) {
-    return this._getPref(prefName, "COMPLEX", { iid });
+    return Services.prefs.getComplexValue(prefName, iid);
   }
 
   getParentBoolPref(prefName, defaultValue) {
     return this._getParentPref(prefName, "BOOL", { defaultValue });
   }
   getParentIntPref(prefName, defaultValue) {
     return this._getParentPref(prefName, "INT", { defaultValue });
   }
@@ -1090,50 +1093,58 @@ class SpecialPowersAPI {
 
   // Mimic the clearUserPref API
   clearUserPref(prefName) {
     let msg = {
       op: "clear",
       prefName,
       prefType: "",
     };
-    this._sendSyncMessage("SPPrefService", msg);
+    return this.sendQuery("SPPrefService", msg);
   }
 
   // Private pref functions to communicate to chrome
-  _getPref(prefName, prefType, { defaultValue, iid }) {
+  async _getParentPref(prefName, prefType, { defaultValue, iid }) {
     let msg = {
       op: "get",
       prefName,
       prefType,
       iid, // Only used with complex prefs
       defaultValue, // Optional default value
     };
-    let val = this._sendSyncMessage("SPPrefService", msg);
-    if (val == null || val[0] == null) {
+    let val = await this.sendQuery("SPPrefService", msg);
+    if (val == null) {
       throw new Error(`Error getting pref '${prefName}'`);
     }
-    return val[0];
+    return val;
+  }
+  _getPref(prefName, prefType, { defaultValue }) {
+    switch (prefType) {
+      case "BOOL":
+        return Services.prefs.getBoolPref(prefName);
+      case "INT":
+        return Services.prefs.getIntPref(prefName);
+      case "CHAR":
+        return Services.prefs.getCharPref(prefName);
+    }
+    return undefined;
   }
   _setPref(prefName, prefType, prefValue, iid) {
     let msg = {
       op: "set",
       prefName,
       prefType,
       iid, // Only used with complex prefs
       prefValue,
     };
-    return this._sendSyncMessage("SPPrefService", msg)[0];
+    return this.sendQuery("SPPrefService", msg);
   }
 
-  _getDocShell(window) {
-    return window.docShell;
-  }
   _getMUDV(window) {
-    return this._getDocShell(window).contentViewer;
+    return window.docShell.contentViewer;
   }
   // XXX: these APIs really ought to be removed, they're not e10s-safe.
   // (also they're pretty Firefox-specific)
   _getTopChromeWindow(window) {
     return window.docShell.rootTreeItem.domWindow
                  .QueryInterface(Ci.nsIDOMChromeWindow);
   }
   _getAutoCompletePopup(window) {
@@ -1154,34 +1165,34 @@ class SpecialPowersAPI {
     return WrapPrivileged.wrap(tmp.FormHistory);
   }
   getFormFillController(window) {
     return Cc["@mozilla.org/satchel/form-fill-controller;1"]
              .getService(Ci.nsIFormFillController);
   }
   attachFormFillControllerTo(window) {
     this.getFormFillController()
-        .attachPopupElementToBrowser(this._getDocShell(window),
+        .attachPopupElementToBrowser(window.docShell,
                                      this._getAutoCompletePopup(window));
   }
   detachFormFillControllerFrom(window) {
-    this.getFormFillController().detachFromBrowser(this._getDocShell(window));
+    this.getFormFillController().detachFromBrowser(window.docShell);
   }
   isBackButtonEnabled(window) {
     return !this._getTopChromeWindow(window).document
                                       .getElementById("Browser:Back")
                                       .hasAttribute("disabled");
   }
   // XXX end of problematic APIs
 
   addChromeEventListener(type, listener, capture, allowUntrusted) {
-    this.mm.addEventListener(type, listener, capture, allowUntrusted);
+    this.docShell.chromeEventHandler.addEventListener(type, listener, capture, allowUntrusted);
   }
   removeChromeEventListener(type, listener, capture) {
-    this.mm.removeEventListener(type, listener, capture);
+    this.docShell.chromeEventHandler.removeEventListener(type, listener, capture);
   }
 
   // Note: each call to registerConsoleListener MUST be paired with a
   // call to postConsoleSentinel; when the callback receives the
   // sentinel it will unregister itself (_after_ calling the
   // callback).  SimpleTest.expectConsoleMessages does this for you.
   // If you register more than one console listener, a call to
   // postConsoleSentinel will zap all of them.
@@ -1225,17 +1236,17 @@ class SpecialPowersAPI {
   emulateMedium(window, mediaType) {
     this._getMUDV(window).emulateMedium(mediaType);
   }
   stopEmulatingMedium(window) {
     this._getMUDV(window).stopEmulatingMedium();
   }
 
   snapshotWindowWithOptions(win, rect, bgcolor, options) {
-    var el = this.window.get().document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+    var el = this.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
     if (rect === undefined) {
       rect = { top: win.scrollY, left: win.scrollX,
                width: win.innerWidth, height: win.innerHeight };
     }
     if (bgcolor === undefined) {
       bgcolor = "rgb(255,255,255)";
     }
     if (options === undefined) {
@@ -1445,38 +1456,33 @@ class SpecialPowersAPI {
     return Services.focus.focusedWindow;
   }
 
   focus(aWindow) {
     // This is called inside TestRunner._makeIframe without aWindow, because of assertions in oop mochitests
     // With aWindow, it is called in SimpleTest.waitForFocus to allow popup window opener focus switching
     if (aWindow)
       aWindow.focus();
-    var mm = this.mm;
-    if (aWindow) {
-      let windowMM = aWindow.docShell.messageManager;
-      if (windowMM) {
-        mm = windowMM;
-      }
-      /*
-       * Otherwise (e.g. XUL chrome windows from mochitest-chrome which won't
-       * have a message manager) just stick with "global".
-       */
+
+    try {
+      let actor = (aWindow ? aWindow.getWindowGlobalChild().getActor("SpecialPowers")
+                           : this);
+      actor.sendAsyncMessage("SpecialPowers.Focus", {});
+    } catch (e) {
+      Cu.reportError(e);
     }
-    mm.sendAsyncMessage("SpecialPowers.Focus", {});
   }
 
   getClipboardData(flavor, whichClipboard) {
     if (whichClipboard === undefined)
       whichClipboard = Services.clipboard.kGlobalClipboard;
 
     var xferable = Cc["@mozilla.org/widget/transferable;1"].
                    createInstance(Ci.nsITransferable);
-    xferable.init(this._getDocShell(this.chromeWindow || this.mm.content.window)
-                      .QueryInterface(Ci.nsILoadContext));
+    xferable.init(this.docShell);
     xferable.addDataFlavor(flavor);
     Services.clipboard.getData(xferable, whichClipboard);
     var data = {};
     try {
       xferable.getTransferData(flavor, data);
     } catch (e) {}
     data = data.value || null;
     if (data == null)
@@ -1563,17 +1569,17 @@ class SpecialPowersAPI {
       let uri = Services.io.newURI(arg.url);
       let attrs = arg.originAttributes || {};
       principal = secMan.createCodebasePrincipal(uri, attrs);
     }
 
     return principal;
   }
 
-  addPermission(type, allow, arg, expireType, expireTime) {
+  async addPermission(type, allow, arg, expireType, expireTime) {
     let principal = this._getPrincipalFromArg(arg);
     if (principal.isSystemPrincipal) {
       return; // nothing to do
     }
 
     let permission;
     if (typeof allow !== "boolean") {
       permission = allow;
@@ -1586,128 +1592,111 @@ class SpecialPowersAPI {
       "op": "add",
       "type": type,
       "permission": permission,
       "principal": principal,
       "expireType": (typeof expireType === "number") ? expireType : 0,
       "expireTime": (typeof expireTime === "number") ? expireTime : 0,
     };
 
-    this._sendSyncMessage("SPPermissionManager", msg);
+    await this.sendQuery("SPPermissionManager", msg);
   }
 
-  removePermission(type, arg) {
+  async removePermission(type, arg) {
     let principal = this._getPrincipalFromArg(arg);
     if (principal.isSystemPrincipal) {
       return; // nothing to do
     }
 
     var msg = {
       "op": "remove",
       "type": type,
       "principal": principal,
     };
 
-    this._sendSyncMessage("SPPermissionManager", msg);
+    await this.sendQuery("SPPermissionManager", msg);
   }
 
-  hasPermission(type, arg) {
+  async hasPermission(type, arg) {
     let principal = this._getPrincipalFromArg(arg);
     if (principal.isSystemPrincipal) {
       return true; // system principals have all permissions
     }
 
     var msg = {
       "op": "has",
       "type": type,
       "principal": principal,
     };
 
-    return this._sendSyncMessage("SPPermissionManager", msg)[0];
+    return this.sendQuery("SPPermissionManager", msg);
   }
 
-  testPermission(type, value, arg) {
+  async testPermission(type, value, arg) {
     let principal = this._getPrincipalFromArg(arg);
     if (principal.isSystemPrincipal) {
       return true; // system principals have all permissions
     }
 
     var msg = {
       "op": "test",
       "type": type,
       "value": value,
       "principal": principal,
     };
-    return this._sendSyncMessage("SPPermissionManager", msg)[0];
+    return this.sendQuery("SPPermissionManager", msg);
   }
 
   isContentWindowPrivate(win) {
     return PrivateBrowsingUtils.isContentWindowPrivate(win);
   }
 
-  notifyObserversInParentProcess(subject, topic, data) {
+  async notifyObserversInParentProcess(subject, topic, data) {
     if (subject) {
       throw new Error("Can't send subject to another process!");
     }
     if (this.isMainProcess()) {
       this.notifyObservers(subject, topic, data);
       return;
     }
     var msg = {
       "op": "notify",
       "observerTopic": topic,
       "observerData": data,
     };
-    this._sendSyncMessage("SPObserverService", msg);
+    await this.sendQuery("SPObserverService", msg);
   }
 
   removeAllServiceWorkerData() {
-    return WrapPrivileged.wrap(this._removeServiceWorkerData("SPRemoveAllServiceWorkers"));
+    return this.sendQuery("SPRemoveAllServiceWorkers", {});
   }
 
   removeServiceWorkerDataForExampleDomain() {
-    return WrapPrivileged.wrap(this._removeServiceWorkerData("SPRemoveServiceWorkerDataForExampleDomain"));
+    return this.sendQuery("SPRemoveServiceWorkerDataForExampleDomain", {});
   }
 
   cleanUpSTSData(origin, flags) {
-    return this._sendSyncMessage("SPCleanUpSTSData", {origin, flags: flags || 0});
+    return this.sendQuery("SPCleanUpSTSData", {origin, flags: flags || 0});
   }
 
-  requestDumpCoverageCounters(cb) {
+  async requestDumpCoverageCounters(cb) {
     // We want to avoid a roundtrip between child and parent.
     if (!PerTestCoverageUtils.enabled) {
-      return Promise.resolve();
+      return;
     }
 
-    return new Promise(resolve => {
-      let messageListener = _ => {
-        this._removeMessageListener("SPRequestDumpCoverageCounters", messageListener);
-        resolve();
-      };
-
-      this._addMessageListener("SPRequestDumpCoverageCounters", messageListener);
-      this._sendAsyncMessage("SPRequestDumpCoverageCounters", {});
-    });
+    await this.sendQuery("SPRequestDumpCoverageCounters", {});
   }
 
-  requestResetCoverageCounters(cb) {
+  async requestResetCoverageCounters(cb) {
     // We want to avoid a roundtrip between child and parent.
     if (!PerTestCoverageUtils.enabled) {
-      return Promise.resolve();
+      return;
     }
-
-    return new Promise(resolve => {
-      let messageListener = _ => {
-        this._removeMessageListener("SPRequestResetCoverageCounters", messageListener);
-        resolve();
-      };
-
-      this._addMessageListener("SPRequestResetCoverageCounters", messageListener);
-      this._sendAsyncMessage("SPRequestResetCoverageCounters", {});
-    });
+    await this.sendQuery("SPRequestResetCoverageCounters", {});
   }
 
   loadExtension(ext, handler) {
     if (this._extensionListeners == null) {
       this._extensionListeners = new Set();
 
       this._addMessageListener("SPExtensionMessage", msg => {
         for (let listener of this._extensionListeners) {
@@ -1741,32 +1730,32 @@ class SpecialPowersAPI {
 
     let sp = this;
     let state = "uninitialized";
     let extension = {
       get state() { return state; },
 
       startup() {
         state = "pending";
-        sp._sendAsyncMessage("SPStartupExtension", {id});
+        sp.sendAsyncMessage("SPStartupExtension", {id});
         return startupPromise;
       },
 
       unload() {
         state = "unloading";
-        sp._sendAsyncMessage("SPUnloadExtension", {id});
+        sp.sendAsyncMessage("SPUnloadExtension", {id});
         return unloadPromise;
       },
 
       sendMessage(...args) {
-        sp._sendAsyncMessage("SPExtensionMessage", {id, args});
+        sp.sendAsyncMessage("SPExtensionMessage", {id, args});
       },
     };
 
-    this._sendAsyncMessage("SPLoadExtension", {ext, id});
+    this.sendAsyncMessage("SPLoadExtension", {ext, id});
 
     let listener = (msg) => {
       if (msg.data.id == id) {
         if (msg.data.type == "extensionStarted") {
           state = "running";
           resolveStartup();
         } else if (msg.data.type == "extensionSetId") {
           extension.id = msg.data.args[0];
@@ -1774,38 +1763,38 @@ class SpecialPowersAPI {
         } else if (msg.data.type == "extensionFailed") {
           state = "failed";
           rejectStartup("startup failed");
         } else if (msg.data.type == "extensionUnloaded") {
           this._extensionListeners.delete(listener);
           state = "unloaded";
           resolveUnload();
         } else if (msg.data.type in handler) {
-          handler[msg.data.type](...Cu.cloneInto(msg.data.args, this.window));
+          handler[msg.data.type](...Cu.cloneInto(msg.data.args, this.contentWindow));
         } else {
           dump(`Unexpected: ${msg.data.type}\n`);
         }
       }
     };
 
     this._extensionListeners.add(listener);
     return extension;
   }
 
   invalidateExtensionStorageCache() {
     this.notifyObserversInParentProcess(null, "extension-invalidate-storage-cache", "");
   }
 
   allowMedia(window, enable) {
-    this._getDocShell(window).allowMedia = enable;
+    window.docShell.allowMedia = enable;
   }
 
   createChromeCache(name, url) {
     let principal = this._getPrincipalFromArg(url);
-    return WrapPrivileged.wrap(new this.mm.content.CacheStorage(name, principal));
+    return WrapPrivileged.wrap(new this.contentWindow.CacheStorage(name, principal));
   }
 
   loadChannelAndReturnStatus(url, loadUsingSystemPrincipal) {
     const BinaryInputStream =
         Components.Constructor("@mozilla.org/binaryinputstream;1",
                                "nsIBinaryInputStream",
                                "setInputStream");
 
@@ -1881,23 +1870,23 @@ class SpecialPowersAPI {
   }
 
   observeMutationEvents(mo, node, nativeAnonymousChildList, subtree) {
     WrapPrivileged.unwrap(mo).observe(WrapPrivileged.unwrap(node),
                                       {nativeAnonymousChildList, subtree});
   }
 
   doCommand(window, cmd) {
-    return this._getDocShell(window).doCommand(cmd);
+    return window.docShell.doCommand(cmd);
   }
 
   setCommandNode(window, node) {
-    return this._getDocShell(window).contentViewer
-               .QueryInterface(Ci.nsIContentViewerEdit)
-               .setCommandNode(node);
+    return window.docShell.contentViewer
+                 .QueryInterface(Ci.nsIContentViewerEdit)
+                 .setCommandNode(node);
   }
 
   /* Bug 1339006 Runnables of nsIURIClassifier.classify may be labeled by
    * SystemGroup, but some test cases may run as web content. That would assert
    * when trying to enter web content from a runnable labeled by the
    * SystemGroup. To avoid that, we run classify from SpecialPowers which is
    * chrome-privileged and allowed to run inside SystemGroup
    */
rename from testing/specialpowers/content/SpecialPowersObserverAPI.jsm
rename to testing/specialpowers/content/SpecialPowersAPIParent.jsm
--- a/testing/specialpowers/content/SpecialPowersObserverAPI.jsm
+++ b/testing/specialpowers/content/SpecialPowersAPIParent.jsm
@@ -1,15 +1,15 @@
 /* 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";
 
-var EXPORTED_SYMBOLS = ["SpecialPowersObserverAPI", "SpecialPowersError"];
+var EXPORTED_SYMBOLS = ["SpecialPowersAPIParent", "SpecialPowersError"];
 
 var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   ExtensionData: "resource://gre/modules/Extension.jsm",
   ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.jsm",
   PerTestCoverageUtils: "resource://testing-common/PerTestCoverageUtils.jsm",
@@ -67,18 +67,19 @@ function getTestPlugin(pluginName) {
     if (tag.name == name) {
       return tag;
     }
   }
 
   return null;
 }
 
-class SpecialPowersObserverAPI {
+class SpecialPowersAPIParent extends JSWindowActorParent {
   constructor() {
+    super();
     this._crashDumpDir = null;
     this._processCrashObserversRegistered = false;
     this._chromeScriptListeners = [];
     this._extensions = new Map();
   }
 
   _observe(aSubject, aTopic, aData) {
     function addDumpIDToMessage(propertyName) {
@@ -112,17 +113,17 @@ class SpecialPowersObserverAPI {
           }
         } else { // ipc:content-shutdown
           if (!aSubject.hasKey("abnormal")) {
             return; // This is a normal shutdown, ignore it
           }
 
           addDumpIDToMessage("dumpID");
         }
-        this._sendAsyncMessage("SPProcessCrashService", message);
+        this.sendAsyncMessage("SPProcessCrashService", message);
         break;
     }
   }
 
   _getCrashDumpDir() {
     if (!this._crashDumpDir) {
       this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
       this._crashDumpDir.append("minidumps");
@@ -199,23 +200,16 @@ class SpecialPowersObserverAPI {
       }
     }
     return removed;
   }
 
   _getURI(url) {
     return Services.io.newURI(url);
   }
-
-  _sendReply(aMessage, aReplyName, aReplyMsg) {
-    let mm = aMessage.target.frameLoader
-                     .messageManager;
-    mm.sendAsyncMessage(aReplyName, aReplyMsg);
-  }
-
   _notifyCategoryAndObservers(subject, topic, data) {
     const serviceMarker = "service,";
 
     // First create observers from the category manager.
 
     let observers = [];
 
     for (let {value: contractID} of Services.catMan.enumerateCategory(topic)) {
@@ -250,17 +244,17 @@ class SpecialPowersObserverAPI {
       } catch (e) { }
     });
   }
 
   /**
    * messageManager callback function
    * This will get requests from our API in the window and process them in chrome for it
    **/
-  _receiveMessageAPI(aMessage) { // eslint-disable-line complexity
+  receiveMessage(aMessage) { // eslint-disable-line complexity
     // We explicitly return values in the below code so that this function
     // doesn't trigger a flurry of warnings about "does not always return
     // a value".
     switch (aMessage.name) {
       case "SPPrefService": {
         let prefs = Services.prefs;
         let prefType = aMessage.json.prefType.toUpperCase();
         let { prefName, prefValue, iid, defaultValue } = aMessage.json;
@@ -340,21 +334,17 @@ class SpecialPowersObserverAPI {
         }
         return undefined; // See comment at the beginning of this function.
       }
 
       case "SPProcessCrashManagerWait": {
         let promises = aMessage.json.crashIds.map((crashId) => {
           return Services.crashmanager.ensureCrashIsPresent(crashId);
         });
-
-        Promise.all(promises).then(() => {
-          this._sendReply(aMessage, "SPProcessCrashManagerWait", {});
-        });
-        return undefined; // See comment at the beginning of this function.
+        return Promise.all(promises);
       }
 
       case "SPPermissionManager": {
         let msg = aMessage.json;
         let principal = msg.principal;
 
         switch (msg.op) {
           case "add":
@@ -389,17 +379,16 @@ class SpecialPowersObserverAPI {
       case "SPObserverService": {
         let topic = aMessage.json.observerTopic;
         switch (aMessage.json.op) {
           case "notify":
             let data = aMessage.json.observerData;
             Services.obs.notifyObservers(null, topic, data);
             break;
           case "add":
-            this._registerObservers._self = this;
             this._registerObservers._add(topic);
             break;
           default:
             throw new SpecialPowersError("Invalid operation for SPObserverervice");
         }
         return undefined; // See comment at the beginning of this function.
       }
 
@@ -419,41 +408,39 @@ class SpecialPowersObserverAPI {
 
         // Setup a chrome sandbox that has access to sendAsyncMessage
         // and {add,remove}MessageListener in order to communicate with
         // the mochitest.
         let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
         let sandboxOptions = Object.assign({wantGlobalProperties: ["ChromeUtils"]},
                                            aMessage.json.sandboxOptions);
         let sb = Cu.Sandbox(systemPrincipal, sandboxOptions);
-        let mm = aMessage.target.frameLoader
-                         .messageManager;
         sb.sendAsyncMessage = (name, message) => {
-          mm.sendAsyncMessage("SPChromeScriptMessage",
-                              { id, name, message });
+          this.sendAsyncMessage("SPChromeScriptMessage",
+                                { id, name, message });
         };
         sb.addMessageListener = (name, listener) => {
           this._chromeScriptListeners.push({ id, name, listener });
         };
         sb.removeMessageListener = (name, listener) => {
           let index = this._chromeScriptListeners.findIndex(function(obj) {
             return obj.id == id && obj.name == name && obj.listener == listener;
           });
           if (index >= 0) {
             this._chromeScriptListeners.splice(index, 1);
           }
         };
-        sb.browserElement = aMessage.target;
+        sb.actorParent = this.manager;
 
         // Also expose assertion functions
-        let reporter = function(err, message, stack) {
+        let reporter = (err, message, stack) => {
           // Pipe assertions back to parent process
-          mm.sendAsyncMessage("SPChromeScriptAssert",
-                              { id, name: scriptName, err, message,
-                                stack });
+          this.sendAsyncMessage("SPChromeScriptAssert",
+                                { id, name: scriptName, err, message,
+                                  stack });
         };
         Object.defineProperty(sb, "assert", {
           get() {
             let scope = Cu.createObjectIn(sb);
             Services.scriptloader.loadSubScript("resource://specialpowers/Assert.jsm",
                                                 scope);
 
             let assert = new scope.Assert(reporter);
@@ -474,19 +461,23 @@ class SpecialPowersObserverAPI {
         }
         return undefined; // See comment at the beginning of this function.
       }
 
       case "SPChromeScriptMessage": {
         let id = aMessage.json.id;
         let name = aMessage.json.name;
         let message = aMessage.json.message;
-        return this._chromeScriptListeners
-                   .filter(o => (o.name == name && o.id == id))
-                   .map(o => o.listener(message));
+        let result;
+        for (let listener of this._chromeScriptListeners) {
+          if (listener.name == name && listener.id == id) {
+            result = listener.listener(message);
+          }
+        }
+        return result;
       }
 
       case "SPImportInMainProcess": {
         var message = { hadError: false, errorMessage: null };
         try {
           ChromeUtils.import(aMessage.data);
         } catch (e) {
           message.hadError = true;
@@ -501,27 +492,21 @@ class SpecialPowersObserverAPI {
         let uri = Services.io.newURI(origin);
         let sss = Cc["@mozilla.org/ssservice;1"].
                   getService(Ci.nsISiteSecurityService);
         sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, flags);
         return undefined;
       }
 
       case "SPRequestDumpCoverageCounters": {
-        PerTestCoverageUtils.afterTest().then(() =>
-          this._sendReply(aMessage, "SPRequestDumpCoverageCounters", {})
-        );
-        return undefined; // See comment at the beginning of this function.
+        return PerTestCoverageUtils.afterTest();
       }
 
       case "SPRequestResetCoverageCounters": {
-        PerTestCoverageUtils.beforeTest().then(() =>
-          this._sendReply(aMessage, "SPRequestResetCoverageCounters", {})
-        );
-        return undefined; // See comment at the beginning of this function.
+        return PerTestCoverageUtils.beforeTest();
       }
 
       case "SPCheckServiceWorkers": {
         let swm = Cc["@mozilla.org/serviceworkers/manager;1"]
                     .getService(Ci.nsIServiceWorkerManager);
         let regs = swm.getAllRegistrations();
 
         // XXX This code is shared with specialpowers.js.
@@ -534,22 +519,22 @@ class SpecialPowersObserverAPI {
       }
 
       case "SPLoadExtension": {
         let id = aMessage.data.id;
         let ext = aMessage.data.ext;
         let extension = ExtensionTestCommon.generate(ext);
 
         let resultListener = (...args) => {
-          this._sendReply(aMessage, "SPExtensionMessage", {id, type: "testResult", args});
+          this.sendAsyncMessage("SPExtensionMessage", {id, type: "testResult", args});
         };
 
         let messageListener = (...args) => {
           args.shift();
-          this._sendReply(aMessage, "SPExtensionMessage", {id, type: "testMessage", args});
+          this.sendAsyncMessage("SPExtensionMessage", {id, type: "testMessage", args});
         };
 
         // Register pass/fail handlers.
         extension.on("test-result", resultListener);
         extension.on("test-eq", resultListener);
         extension.on("test-log", resultListener);
         extension.on("test-done", resultListener);
 
@@ -567,17 +552,17 @@ class SpecialPowersObserverAPI {
           if (!ext) {
             // ext is only set by the "startup" event from Extension.jsm.
             // Unfortunately ext-backgroundPage.js emits an event with the same
             // name, but without the extension object as parameter.
             return;
           }
           // ext is always the "real" Extension object, even when "extension"
           // is a MockExtension.
-          this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionSetId", args: [ext.id, ext.uuid]});
+          this.sendAsyncMessage("SPExtensionMessage", {id, type: "extensionSetId", args: [ext.id, ext.uuid]});
         });
 
         // Make sure the extension passes the packaging checks when
         // they're run on a bare archive rather than a running instance,
         // as the add-on manager runs them.
         let extensionData = new ExtensionData(extension.rootURI);
         extensionData.loadManifest().then(
           () => {
@@ -596,20 +581,20 @@ class SpecialPowersObserverAPI {
         ).then(async () => {
           // browser tests do not call startup in ExtensionXPCShellUtils or MockExtension,
           // in that case we have an ID here and we need to set the override.
           if (extension.id) {
             await ExtensionTestCommon.setIncognitoOverride(extension);
           }
           return extension.startup();
         }).then(() => {
-          this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionStarted", args: []});
+          this.sendAsyncMessage("SPExtensionMessage", {id, type: "extensionStarted", args: []});
         }).catch(e => {
           dump(`Extension startup failed: ${e}\n${e.stack}`);
-          this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionFailed", args: []});
+          this.sendAsyncMessage("SPExtensionMessage", {id, type: "extensionFailed", args: []});
         });
         return undefined;
       }
 
       case "SPExtensionMessage": {
         let id = aMessage.data.id;
         let extension = this._extensions.get(id);
         extension.testMessage(...aMessage.data.args);
@@ -617,34 +602,28 @@ class SpecialPowersObserverAPI {
       }
 
       case "SPUnloadExtension": {
         let id = aMessage.data.id;
         let extension = this._extensions.get(id);
         this._extensions.delete(id);
         let done = async () => {
           await extension._uninstallPromise;
-          this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionUnloaded", args: []});
+          this.sendAsyncMessage("SPExtensionMessage", {id, type: "extensionUnloaded", args: []});
         };
         extension.shutdown().then(done, done);
         return undefined;
       }
 
       case "SPRemoveAllServiceWorkers": {
-        ServiceWorkerCleanUp.removeAll().then(() => {
-          this._sendReply(aMessage, "SPServiceWorkerCleanupComplete", { id: aMessage.data.id });
-        });
-        return undefined;
+        return ServiceWorkerCleanUp.removeAll();
       }
 
       case "SPRemoveServiceWorkerDataForExampleDomain": {
-        ServiceWorkerCleanUp.removeFromHost("example.com").then(() => {
-          this._sendReply(aMessage, "SPServiceWorkerCleanupComplete", { id: aMessage.data.id });
-        });
-        return undefined;
+        return ServiceWorkerCleanUp.removeFromHost("example.com");
       }
 
       default:
         throw new SpecialPowersError(`Unrecognized Special Powers API: ${aMessage.name}`);
     }
 
     // We throw an exception before reaching this explicit return because
     // we should never be arriving here anyway.
rename from testing/specialpowers/content/SpecialPowers.jsm
rename to testing/specialpowers/content/SpecialPowersChild.jsm
--- a/testing/specialpowers/content/SpecialPowers.jsm
+++ b/testing/specialpowers/content/SpecialPowersChild.jsm
@@ -2,275 +2,180 @@
  * 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/. */
 /* This code is loaded in every child process that is started by mochitest in
  * order to be used as a replacement for UniversalXPConnect
  */
 
 var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-var EXPORTED_SYMBOLS = ["SpecialPowers", "attachSpecialPowersToWindow"];
+var EXPORTED_SYMBOLS = ["SpecialPowers", "SpecialPowersChild", "attachSpecialPowersToWindow"];
 
 const {bindDOMWindowUtils, SpecialPowersAPI} = ChromeUtils.import("resource://specialpowers/SpecialPowersAPI.jsm");
+const {ExtensionUtils} = ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 Cu.forcePermissiveCOWs();
 
-class SpecialPowers extends SpecialPowersAPI {
-  constructor(window, mm) {
+class SpecialPowersChild extends SpecialPowersAPI {
+  constructor() {
     super();
 
-    this.mm = mm;
+    this._windowID = null;
+    this.DOMWindowUtils = null;
 
-    this.window = Cu.getWeakReference(window);
-    this._windowID = window.windowUtils.currentInnerWindowID;
     this._encounteredCrashDumpFiles = [];
     this._unexpectedCrashDumpFiles = { };
     this._crashDumpDir = null;
     this._serviceWorkerRegistered = false;
     this._serviceWorkerCleanUpRequests = new Map();
-    this.DOMWindowUtils = bindDOMWindowUtils(window);
     Object.defineProperty(this, "Components", {
         configurable: true, enumerable: true, value: this.getFullComponents(),
     });
-    this._pongHandlers = [];
-    this._messageListener = this._messageReceived.bind(this);
-    this._grandChildFrameMM = null;
     this._createFilesOnError = null;
     this._createFilesOnSuccess = null;
-    this.SP_SYNC_MESSAGES = ["SPChromeScriptMessage",
-                             "SPLoadChromeScript",
-                             "SPImportInMainProcess",
-                             "SPObserverService",
-                             "SPPermissionManager",
-                             "SPPrefService",
-                             "SPProcessCrashService",
-                             "SPSetTestPluginEnabledState",
-                             "SPCleanUpSTSData",
-                             "SPCheckServiceWorkers"];
+
+    this._messageListeners = new ExtensionUtils.DefaultMap(() => new Set());
+  }
+
+  handleEvent(aEvent) {
+    this.attachToWindow();
+  }
 
-    this.SP_ASYNC_MESSAGES = ["SpecialPowers.Focus",
-                              "SpecialPowers.Quit",
-                              "SpecialPowers.CreateFiles",
-                              "SpecialPowers.RemoveFiles",
-                              "SPPingService",
-                              "SPLoadExtension",
-                              "SPProcessCrashManagerWait",
-                              "SPStartupExtension",
-                              "SPUnloadExtension",
-                              "SPExtensionMessage",
-                              "SPRequestDumpCoverageCounters",
-                              "SPRequestResetCoverageCounters",
-                              "SPRemoveAllServiceWorkers",
-                              "SPRemoveServiceWorkerDataForExampleDomain"];
-    mm.addMessageListener("SPPingService", this._messageListener);
-    mm.addMessageListener("SPServiceWorkerRegistered", this._messageListener);
-    mm.addMessageListener("SpecialPowers.FilesCreated", this._messageListener);
-    mm.addMessageListener("SpecialPowers.FilesError", this._messageListener);
-    mm.addMessageListener("SPServiceWorkerCleanupComplete", this._messageListener);
-    let self = this;
-    Services.obs.addObserver(function onInnerWindowDestroyed(subject, topic, data) {
-      var id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
-      if (self._windowID === id) {
-        Services.obs.removeObserver(onInnerWindowDestroyed, "inner-window-destroyed");
-        try {
-          mm.removeMessageListener("SPPingService", self._messageListener);
-          mm.removeMessageListener("SpecialPowers.FilesCreated", self._messageListener);
-          mm.removeMessageListener("SpecialPowers.FilesError", self._messageListener);
-          mm.removeMessageListener("SPServiceWorkerCleanupComplete", self._messageListener);
-        } catch (e) {
-          // Ignore the exception which the message manager has been destroyed.
-          if (e.result != Cr.NS_ERROR_ILLEGAL_VALUE) {
-            throw e;
-          }
-        }
+  attachToWindow() {
+    let window = this.contentWindow;
+    if (!window.wrappedJSObject.SpecialPowers) {
+      this._windowID = window.windowUtils.currentInnerWindowID;
+      this.DOMWindowUtils = bindDOMWindowUtils(window);
+
+      window.wrappedJSObject.SpecialPowers = this;
+      if (this.IsInNestedFrame) {
+        this.addPermission("allowXULXBL", true, window.document);
       }
-    }, "inner-window-destroyed");
+    }
+  }
+
+  get window() {
+    return this.contentWindow;
   }
 
   toString() { return "[SpecialPowers]"; }
   sanityCheck() { return "foo"; }
 
-  _sendSyncMessage(msgname, msg) {
-    if (!this.SP_SYNC_MESSAGES.includes(msgname)) {
-      dump("TEST-INFO | specialpowers.js |  Unexpected SP message: " + msgname + "\n");
-    }
-    let result = this.mm.sendSyncMessage(msgname, msg);
-    return Cu.cloneInto(result, this);
-  }
-
-  _sendAsyncMessage(msgname, msg) {
-    if (!this.SP_ASYNC_MESSAGES.includes(msgname)) {
-      dump("TEST-INFO | specialpowers.js |  Unexpected SP message: " + msgname + "\n");
-    }
-    this.mm.sendAsyncMessage(msgname, msg);
-  }
-
   _addMessageListener(msgname, listener) {
-    this.mm.addMessageListener(msgname, listener);
-    this.mm.sendAsyncMessage("SPPAddNestedMessageListener", { name: msgname });
+    this._messageListeners.get(msgname).add(listener);
   }
 
   _removeMessageListener(msgname, listener) {
-    this.mm.removeMessageListener(msgname, listener);
+    this._messageListeners.get(msgname).delete(listener);
   }
 
   registerProcessCrashObservers() {
-    this.mm.addMessageListener("SPProcessCrashService", this._messageListener);
-    this.mm.sendSyncMessage("SPProcessCrashService", { op: "register-observer" });
+    this.sendAsyncMessage("SPProcessCrashService", { op: "register-observer" });
   }
 
   unregisterProcessCrashObservers() {
-    this.mm.removeMessageListener("SPProcessCrashService", this._messageListener);
-    this.mm.sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" });
+    this.sendAsyncMessage("SPProcessCrashService", { op: "unregister-observer" });
   }
 
-  _messageReceived(aMessage) {
+  receiveMessage(aMessage) {
+    if (this._messageListeners.has(aMessage.name)) {
+      for (let listener of this._messageListeners.get(aMessage.name)) {
+        try {
+          if (typeof listener === "function") {
+            listener(aMessage);
+          } else {
+            listener.receiveMessage(aMessage);
+          }
+        } catch (e) {
+          Cu.reportError(e);
+        }
+      }
+    }
+
     switch (aMessage.name) {
       case "SPProcessCrashService":
         if (aMessage.json.type == "crash-observed") {
           for (let e of aMessage.json.dumpIDs) {
             this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
           }
         }
         break;
 
-      case "SPPingService":
-        if (aMessage.json.op == "pong") {
-          var handler = this._pongHandlers.shift();
-          if (handler) {
-            handler();
-          }
-          if (this._grandChildFrameMM) {
-            this._grandChildFrameMM.sendAsyncMessage("SPPingService", { op: "pong" });
-          }
-        }
-        break;
-
       case "SPServiceWorkerRegistered":
         this._serviceWorkerRegistered = aMessage.data.registered;
         break;
 
       case "SpecialPowers.FilesCreated":
         var createdHandler = this._createFilesOnSuccess;
         this._createFilesOnSuccess = null;
         this._createFilesOnError = null;
         if (createdHandler) {
-          createdHandler(Cu.cloneInto(aMessage.data, this.mm.content));
+          createdHandler(Cu.cloneInto(aMessage.data, this.contentWindow));
         }
         break;
 
       case "SpecialPowers.FilesError":
         var errorHandler = this._createFilesOnError;
         this._createFilesOnSuccess = null;
         this._createFilesOnError = null;
         if (errorHandler) {
           errorHandler(aMessage.data);
         }
         break;
-
-      case "SPServiceWorkerCleanupComplete": {
-        let id = aMessage.data.id;
-        // It's possible for us to receive requests for other SpecialPowers
-        // instances, ignore them.
-        if (this._serviceWorkerCleanUpRequests.has(id)) {
-          let resolve = this._serviceWorkerCleanUpRequests.get(id);
-          this._serviceWorkerCleanUpRequests.delete(id);
-          resolve();
-        }
-        break;
-      }
     }
 
     return true;
   }
 
   quit() {
-    this.mm.sendAsyncMessage("SpecialPowers.Quit", {});
+    this.sendAsyncMessage("SpecialPowers.Quit", {});
   }
 
   // fileRequests is an array of file requests. Each file request is an object.
   // A request must have a field |name|, which gives the base of the name of the
   // file to be created in the profile directory. If the request has a |data| field
   // then that data will be written to the file.
   createFiles(fileRequests, onCreation, onError) {
-    if (this._createFilesOnSuccess || this._createFilesOnError) {
-      onError("Already waiting for SpecialPowers.createFiles() to finish.");
-      return;
-    }
-
-    this._createFilesOnSuccess = onCreation;
-    this._createFilesOnError = onError;
-    this.mm.sendAsyncMessage("SpecialPowers.CreateFiles", fileRequests);
+    return this.sendQuery("SpecialPowers.CreateFiles", fileRequests).then(onCreation, onError);
   }
 
   // Remove the files that were created using |SpecialPowers.createFiles()|.
   // This will be automatically called by |SimpleTest.finish()|.
   removeFiles() {
-    this.mm.sendAsyncMessage("SpecialPowers.RemoveFiles", {});
+    this.sendAsyncMessage("SpecialPowers.RemoveFiles", {});
   }
 
   executeAfterFlushingMessageQueue(aCallback) {
-    this._pongHandlers.push(aCallback);
-    this.mm.sendAsyncMessage("SPPingService", { op: "ping" });
+    return this.sendQuery("Ping").then(aCallback);
   }
 
-  registeredServiceWorkers() {
+  async registeredServiceWorkers() {
     // For the time being, if parent_intercept is false, we can assume that
     // ServiceWorkers registered by the current test are all known to the SWM in
     // this process.
     if (!Services.prefs.getBoolPref("dom.serviceWorkers.parent_intercept", false)) {
       let swm = Cc["@mozilla.org/serviceworkers/manager;1"]
                   .getService(Ci.nsIServiceWorkerManager);
       let regs = swm.getAllRegistrations();
 
-      // XXX This is shared with SpecialPowersObserverAPI.js
+      // XXX This is shared with SpecialPowersAPIParent.jsm
       let workers = new Array(regs.length);
       for (let i = 0; i < workers.length; ++i) {
         let { scope, scriptSpec } = regs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
         workers[i] = { scope, scriptSpec };
       }
 
       return workers;
     }
 
     // Please see the comment in SpecialPowersObserver.jsm above
     // this._serviceWorkerListener's assignment for what this returns.
     if (this._serviceWorkerRegistered) {
       // This test registered at least one service worker. Send a synchronous
       // call to the parent to make sure that it called unregister on all of its
       // service workers.
-      let { workers } = this._sendSyncMessage("SPCheckServiceWorkers")[0];
+      let { workers } = await this.sendQuery("SPCheckServiceWorkers");
       return workers;
     }
 
     return [];
   }
-
-  _removeServiceWorkerData(messageName) {
-    return new Promise(resolve => {
-      let id = Cc["@mozilla.org/uuid-generator;1"]
-                 .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
-      this._serviceWorkerCleanUpRequests.set(id, resolve);
-      this._sendAsyncMessage(messageName, { id });
-    });
-  }
 }
-
-// Attach our API to the window.
-function attachSpecialPowersToWindow(aWindow, mm) {
-  try {
-    if ((aWindow !== null) &&
-        (aWindow !== undefined) &&
-        (aWindow.wrappedJSObject) &&
-        !(aWindow.wrappedJSObject.SpecialPowers)) {
-      let sp = new SpecialPowers(aWindow, mm);
-      aWindow.wrappedJSObject.SpecialPowers = sp;
-      if (sp.IsInNestedFrame) {
-        sp.addPermission("allowXULXBL", true, aWindow.document);
-      }
-    }
-  } catch (ex) {
-    dump("TEST-INFO | specialpowers.js |  Failed to attach specialpowers to window exception: " + ex + "\n");
-  }
-}
-
-this.SpecialPowers = SpecialPowers;
-this.attachSpecialPowersToWindow = attachSpecialPowersToWindow;
rename from testing/specialpowers/content/SpecialPowersObserver.jsm
rename to testing/specialpowers/content/SpecialPowersParent.jsm
--- a/testing/specialpowers/content/SpecialPowersObserver.jsm
+++ b/testing/specialpowers/content/SpecialPowersParent.jsm
@@ -4,119 +4,80 @@
 "use strict";
 
 // Based on:
 // https://bugzilla.mozilla.org/show_bug.cgi?id=549539
 // https://bug549539.bugzilla.mozilla.org/attachment.cgi?id=429661
 // https://developer.mozilla.org/en/XPCOM/XPCOM_changes_in_Gecko_1.9.3
 // https://developer.mozilla.org/en/how_to_build_an_xpcom_component_in_javascript
 
-var EXPORTED_SYMBOLS = ["SpecialPowersObserver"];
+var EXPORTED_SYMBOLS = ["SpecialPowersParent"];
 
 var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-const MESSAGE_NAMES = [
-  "SPCheckServiceWorkers",
-  "SPChromeScriptMessage",
-  "SPCleanUpSTSData",
-  "SPExtensionMessage",
-  "SPImportInMainProcess",
-  "SPLoadChromeScript",
-  "SPLoadExtension",
-  "SPObserverService",
-  "SPPermissionManager",
-  "SPPingService",
-  "SPPrefService",
-  "SPProcessCrashManagerWait",
-  "SPProcessCrashService",
-  "SPQuotaManager",
-  "SPRemoveAllServiceWorkers",
-  "SPRemoveServiceWorkerDataForExampleDomain",
-  "SPRequestDumpCoverageCounters",
-  "SPRequestResetCoverageCounters",
-  "SPSetTestPluginEnabledState",
-  "SPStartupExtension",
-  "SPUnloadExtension",
-  "SpecialPowers.CreateFiles",
-  "SpecialPowers.Focus",
-  "SpecialPowers.Quit",
-  "SpecialPowers.RemoveFiles",
-];
+const {SpecialPowersAPIParent} = ChromeUtils.import("resource://specialpowers/SpecialPowersAPIParent.jsm");
 
-const FRAME_SCRIPTS = [
-  "resource://specialpowers/specialpowersFrameScript.js",
-];
-
-
-const {SpecialPowersObserverAPI} = ChromeUtils.import("resource://specialpowers/SpecialPowersObserverAPI.jsm");
-
-class SpecialPowersObserver extends SpecialPowersObserverAPI {
+class SpecialPowersParent extends SpecialPowersAPIParent {
   constructor() {
     super();
-    this._initialized = false;
     this._messageManager = Services.mm;
     this._serviceWorkerListener = null;
+
+    this._observer = this.observe.bind(this);
+
+    this.didDestroy = this.uninit.bind(this);
+
+    this._registerObservers = {
+      _self: this,
+      _topics: [],
+      _add(topic) {
+        if (!this._topics.includes(topic)) {
+          this._topics.push(topic);
+          Services.obs.addObserver(this, topic);
+        }
+      },
+      observe(aSubject, aTopic, aData) {
+        var msg = { aData };
+        switch (aTopic) {
+          case "perm-changed":
+            var permission = aSubject.QueryInterface(Ci.nsIPermission);
+
+            // specialPowersAPI will consume this value, and it is used as a
+            // fake permission, but only type will be used.
+            //
+            // We need to ensure that it looks the same as a real permission,
+            // so we fake these properties.
+            msg.permission = {
+              principal: {
+                originAttributes: {},
+              },
+              type: permission.type,
+            };
+          default:
+            this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
+        }
+      },
+    };
+
+    this.init();
   }
 
   observe(aSubject, aTopic, aData) {
     if (aTopic == "http-on-modify-request") {
       if (aSubject instanceof Ci.nsIChannel) {
         let uri = aSubject.URI.spec;
-        this._sendAsyncMessage("specialpowers-http-notify-request", { uri });
+        this.sendAsyncMessage("specialpowers-http-notify-request", { uri });
       }
     } else {
       this._observe(aSubject, aTopic, aData);
     }
   }
 
-  _loadFrameScript() {
-    // Register for any messages our API needs us to handle
-    for (let name of MESSAGE_NAMES) {
-      this._messageManager.addMessageListener(name, this);
-    }
-
-    for (let script of FRAME_SCRIPTS) {
-      this._messageManager.loadFrameScript(script, true);
-    }
-    this._createdFiles = null;
-  }
-
-  _unloadFrameScript() {
-    for (let name of MESSAGE_NAMES) {
-      this._messageManager.removeMessageListener(name, this);
-    }
-
-    for (let script of FRAME_SCRIPTS) {
-      this._messageManager.removeDelayedFrameScript(script);
-    }
-  }
-
-  _sendAsyncMessage(msgname, msg) {
-    this._messageManager.broadcastAsyncMessage(msgname, msg);
-  }
-
-  _receiveMessage(aMessage) {
-    return this._receiveMessageAPI(aMessage);
-  }
-
   init() {
-    if (this._initialized) {
-      throw new Error("Already initialized");
-    }
-
-    // Register special testing modules.
-    var testsURI = Services.dirsvc.get("ProfD", Ci.nsIFile);
-    testsURI.append("tests.manifest");
-    var manifestFile = Services.io.newFileURI(testsURI).
-                         QueryInterface(Ci.nsIFileURL).file;
-
-    Components.manager.QueryInterface(Ci.nsIComponentRegistrar).
-                   autoRegister(manifestFile);
-
-    Services.obs.addObserver(this, "http-on-modify-request");
+    Services.obs.addObserver(this._observer, "http-on-modify-request");
 
     // We would like to check that tests don't leave service workers around
     // after they finish, but that information lives in the parent process.
     // Ideally, we'd be able to tell the child processes whenever service
     // workers are registered or unregistered so they would know at all times,
     // but service worker lifetimes are complicated enough to make that
     // difficult. For the time being, let the child process know when a test
     // registers a service worker so it can ask, synchronously, at the end if
@@ -129,88 +90,73 @@ class SpecialPowersObserver extends Spec
         self.onRegister();
       },
 
       onUnregister() {
         // no-op
       },
     };
     swm.addListener(this._serviceWorkerListener);
-
-    this._loadFrameScript();
-
-    this._initialized = true;
   }
 
   uninit() {
-    if (!this._initialized) {
-      throw new Error("Not initialized");
-    }
-    this._initialized = false;
-
     var obs = Services.obs;
-    obs.removeObserver(this, "http-on-modify-request");
-    this._registerObservers._topics.forEach((element) => {
+    obs.removeObserver(this._observer, "http-on-modify-request");
+    this._registerObservers._topics.splice(0).forEach((element) => {
       obs.removeObserver(this._registerObservers, element);
     });
     this._removeProcessCrashObservers();
 
     let swm = Cc["@mozilla.org/serviceworkers/manager;1"]
                 .getService(Ci.nsIServiceWorkerManager);
     swm.removeListener(this._serviceWorkerListener);
-
-    this._unloadFrameScript();
   }
 
   _addProcessCrashObservers() {
     if (this._processCrashObserversRegistered) {
       return;
     }
 
-    Services.obs.addObserver(this, "plugin-crashed");
-    Services.obs.addObserver(this, "ipc:content-shutdown");
+    Services.obs.addObserver(this._observer, "plugin-crashed");
+    Services.obs.addObserver(this._observer, "ipc:content-shutdown");
     this._processCrashObserversRegistered = true;
   }
 
   _removeProcessCrashObservers() {
     if (!this._processCrashObserversRegistered) {
       return;
     }
 
-    Services.obs.removeObserver(this, "plugin-crashed");
-    Services.obs.removeObserver(this, "ipc:content-shutdown");
+    Services.obs.removeObserver(this._observer, "plugin-crashed");
+    Services.obs.removeObserver(this._observer, "ipc:content-shutdown");
     this._processCrashObserversRegistered = false;
   }
 
   /**
    * messageManager callback function
    * This will get requests from our API in the window and process them in chrome for it
    **/
   receiveMessage(aMessage) {
     switch (aMessage.name) {
-      case "SPPingService":
-        if (aMessage.json.op == "ping") {
-          aMessage.target.frameLoader
-                  .messageManager
-                  .sendAsyncMessage("SPPingService", { op: "pong" });
-        }
-        break;
+      case "Ping":
+        return undefined;
       case "SpecialPowers.Quit":
         Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
         break;
       case "SpecialPowers.Focus":
-        aMessage.target.focus();
+        this.manager.rootFrameLoader.ownerElement.focus();
         break;
       case "SpecialPowers.CreateFiles":
-        let filePaths = [];
-        if (!this._createdFiles) {
-          this._createdFiles = [];
-        }
-        let createdFiles = this._createdFiles;
-        try {
+        return (async () => {
+          let filePaths = [];
+          if (!this._createdFiles) {
+            this._createdFiles = [];
+          }
+          let createdFiles = this._createdFiles;
+
           let promises = [];
           aMessage.data.forEach(function(request) {
             const filePerms = 0o666;
             let testFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
             if (request.name) {
               testFile.appendRelativePath(request.name);
             } else {
               testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, filePerms);
@@ -223,77 +169,40 @@ class SpecialPowersObserver extends Spec
             }
             outStream.close();
             promises.push(File.createFromFileName(testFile.path, request.options).then(function(file) {
               filePaths.push(file);
             }));
             createdFiles.push(testFile);
           });
 
-          Promise.all(promises).then(function() {
-            aMessage.target.frameLoader
-                    .messageManager
-                    .sendAsyncMessage("SpecialPowers.FilesCreated", filePaths);
-          }, function(e) {
-            aMessage.target.frameLoader
-                    .messageManager
-                    .sendAsyncMessage("SpecialPowers.FilesError", e.toString());
-          });
-        } catch (e) {
-            aMessage.target.frameLoader
-                    .messageManager
-                    .sendAsyncMessage("SpecialPowers.FilesError", e.toString());
-        }
+          await Promise.all(promises);
+          return filePaths;
+        })().catch(e => {
+          Cu.reportError(e);
+          return Promise.reject(String(e));
+        });
 
-        break;
       case "SpecialPowers.RemoveFiles":
         if (this._createdFiles) {
           this._createdFiles.forEach(function(testFile) {
             try {
               testFile.remove(false);
             } catch (e) {}
           });
           this._createdFiles = null;
         }
         break;
+
+      case "Wakeup":
+        break;
+
       default:
-        return this._receiveMessage(aMessage);
+        return super.receiveMessage(aMessage);
     }
     return undefined;
   }
 
   onRegister() {
-    this._sendAsyncMessage("SPServiceWorkerRegistered",
+    this.sendAsyncMessage("SPServiceWorkerRegistered",
       { registered: true });
   }
 }
-
-SpecialPowersObserver.prototype._registerObservers = {
-  _self: null,
-  _topics: [],
-  _add(topic) {
-    if (!this._topics.includes(topic)) {
-      this._topics.push(topic);
-      Services.obs.addObserver(this, topic);
-    }
-  },
-  observe(aSubject, aTopic, aData) {
-    var msg = { aData };
-    switch (aTopic) {
-      case "perm-changed":
-        var permission = aSubject.QueryInterface(Ci.nsIPermission);
-
-        // specialPowersAPI will consume this value, and it is used as a
-        // fake permission, but only type will be used.
-        //
-        // We need to ensure that it looks the same as a real permission,
-        // so we fake these properties.
-        msg.permission = {
-          principal: {
-            originAttributes: {},
-          },
-          type: permission.type,
-        };
-      default:
-        this._self._sendAsyncMessage("specialpowers-" + aTopic, msg);
-    }
-  },
-};
deleted file mode 100644
--- a/testing/specialpowers/content/specialpowersFrameScript.js
+++ /dev/null
@@ -1,26 +0,0 @@
-"use strict";
-
-/* globals attachSpecialPowersToWindow */
-
-let mm = this;
-
-ChromeUtils.import("resource://specialpowers/SpecialPowersAPI.jsm", this);
-ChromeUtils.import("resource://specialpowers/SpecialPowers.jsm", this);
-
-// This is a frame script, so it may be running in a content process.
-// In any event, it is targeted at a specific "tab", so we listen for
-// the DOMWindowCreated event to be notified about content windows
-// being created in this context.
-
-function SpecialPowersManager() {
-  addEventListener("DOMWindowCreated", this, false);
-}
-
-SpecialPowersManager.prototype = {
-  handleEvent: function handleEvent(aEvent) {
-    var window = aEvent.target.defaultView;
-    attachSpecialPowersToWindow(window, mm);
-  },
-};
-
-var specialpowersmanager = new SpecialPowersManager();
--- a/testing/specialpowers/moz.build
+++ b/testing/specialpowers/moz.build
@@ -14,18 +14,17 @@ FINAL_TARGET_FILES += [
     'schema.json',
 ]
 
 FINAL_TARGET_FILES.content += [
     '../modules/Assert.jsm',
     'content/MockColorPicker.jsm',
     'content/MockFilePicker.jsm',
     'content/MockPermissionPrompt.jsm',
-    'content/SpecialPowers.jsm',
     'content/SpecialPowersAPI.jsm',
-    'content/specialpowersFrameScript.js',
-    'content/SpecialPowersObserver.jsm',
-    'content/SpecialPowersObserverAPI.jsm',
+    'content/SpecialPowersAPIParent.jsm',
+    'content/SpecialPowersChild.jsm',
+    'content/SpecialPowersParent.jsm',
     'content/WrapPrivileged.jsm',
 ]
 
 with Files("**"):
     BUG_COMPONENT = ("Testing", "Mochitest")
--- a/toolkit/components/normandy/test/browser/browser_actions_AddonStudyAction.js
+++ b/toolkit/components/normandy/test/browser/browser_actions_AddonStudyAction.js
@@ -724,18 +724,16 @@ decorate_task(
   ensureAddonCleanup,
   AddonStudies.withStudies([
     addonStudyFactory({active: true, addonId: "missingAddon@example.com", studyEndDate: null}),
   ]),
   withSendEventStub,
   async function unenrollMissingAddonTest([study], sendEventStub) {
     const action = new AddonStudyAction();
 
-    SimpleTest.waitForExplicitFinish();
-    SimpleTest.monitorConsole(() => SimpleTest.finish(), [{message: /could not uninstall addon/i}]);
     await action.unenroll(study.recipeId);
 
     sendEventStub.assertEvents(
       [["unenroll", "addon_study", study.slug, {
         addonId: study.addonId,
         addonVersion: study.addonVersion,
         reason: "unknown",
       }]]
--- a/toolkit/components/normandy/test/browser/browser_actions_BranchedAddonStudyAction.js
+++ b/toolkit/components/normandy/test/browser/browser_actions_BranchedAddonStudyAction.js
@@ -611,18 +611,16 @@ decorate_task(
   ensureAddonCleanup,
   AddonStudies.withStudies([
     branchedAddonStudyFactory({active: true, addonId: "missingAddon@example.com"}),
   ]),
   withSendEventStub,
   async function unenrollMissingAddonTest([study], sendEventStub) {
     const action = new BranchedAddonStudyAction();
 
-    SimpleTest.waitForExplicitFinish();
-    SimpleTest.monitorConsole(() => SimpleTest.finish(), [{message: /could not uninstall addon/i}]);
     await action.unenroll(study.recipeId);
 
     sendEventStub.assertEvents(
       [["unenroll", "addon_study", study.name, {
         addonId: study.addonId,
         addonVersion: study.addonVersion,
         reason: "unknown",
       }]]