Bug 1129662: sdk/page-worker should use a remote page. r=krizsa
authorDave Townsend <dtownsend@oxymoronical.com>
Fri, 16 Oct 2015 13:22:28 -0700
changeset 270376 5cdf66dfef92364072336ce276f9f2c70d421718
parent 270375 3189c9d88f1357c98dbd7c08c8615af138268807
child 270377 963e002aaa31eb7bb3789ccf9df1a9a7e9a01279
push id86
push usercbook@mozilla.com
push dateWed, 04 Nov 2015 14:00:24 +0000
reviewerskrizsa
bugs1129662
milestone45.0a1
Bug 1129662: sdk/page-worker should use a remote page. r=krizsa This makes page-worker load its pages in the remote process. It does so by creating a single frame in the hidden window used to ensure we have a remote process when necessary and then a module in the remote process is used to create windowless browsers to load the pages. This does break one API, getActiveView, but I don't think we should be maintaining that and it has been unstable since its inception anyway. Once downside, the l10n module now has to use the observer service to detect documents rather than the DOM event, this might be causing more CPOW traffic since that observer notification is shimmed so we may need to use the shim waiver there.
addon-sdk/moz.build
addon-sdk/source/lib/sdk/content/l10n-html.js
addon-sdk/source/lib/sdk/content/page-worker.js
addon-sdk/source/lib/sdk/content/sandbox.js
addon-sdk/source/lib/sdk/content/worker-child.js
addon-sdk/source/lib/sdk/frame/utils.js
addon-sdk/source/lib/sdk/page-worker.js
addon-sdk/source/lib/sdk/remote/parent.js
addon-sdk/source/test/test-addon-extras.js
addon-sdk/source/test/test-page-worker.js
--- a/addon-sdk/moz.build
+++ b/addon-sdk/moz.build
@@ -244,16 +244,17 @@ EXTRA_JS_MODULES.commonjs.sdk.content +=
     'source/lib/sdk/content/content-worker.js',
     'source/lib/sdk/content/content.js',
     'source/lib/sdk/content/context-menu.js',
     'source/lib/sdk/content/events.js',
     'source/lib/sdk/content/l10n-html.js',
     'source/lib/sdk/content/loader.js',
     'source/lib/sdk/content/mod.js',
     'source/lib/sdk/content/page-mod.js',
+    'source/lib/sdk/content/page-worker.js',
     'source/lib/sdk/content/sandbox.js',
     'source/lib/sdk/content/tab-events.js',
     'source/lib/sdk/content/thumbnail.js',
     'source/lib/sdk/content/utils.js',
     'source/lib/sdk/content/worker-child.js',
     'source/lib/sdk/content/worker.js',
 ]
 
--- a/addon-sdk/source/lib/sdk/content/l10n-html.js
+++ b/addon-sdk/source/lib/sdk/content/l10n-html.js
@@ -6,16 +6,17 @@
 module.metadata = {
   "stability": "unstable"
 };
 
 const { Ci } = require("chrome");
 const core = require("../l10n/core");
 const { loadSheet, removeSheet } = require("../stylesheet/utils");
 const { process, frames } = require("../remote/child");
+const { Services } = require("resource://gre/modules/Services.jsm");
 
 const assetsURI = require('../self').data.url();
 
 const hideSheetUri = "data:text/css,:root {visibility: hidden !important;}";
 
 function translateElementAttributes(element) {
   // Translateable attributes
   const attrList = ['title', 'accesskey', 'alt', 'label', 'placeholder'];
@@ -75,17 +76,17 @@ function onDocumentReady2Translate(event
     if (document.defaultView)
       removeSheet(document.defaultView, hideSheetUri, 'user');
   }
   catch(e) {
     console.exception(e);
   }
 }
 
-function onContentWindow({ target: document }) {
+function onContentWindow(document) {
   // Accept only HTML documents
   if (!(document instanceof Ci.nsIDOMHTMLDocument))
     return;
 
   // Bug 769483: data:URI documents instanciated with nsIDOMParser
   // have a null `location` attribute at this time
   if (!document.location)
     return;
@@ -104,18 +105,25 @@ function onContentWindow({ target: docum
   }
   // Wait for DOM tree to be built before applying localization
   document.addEventListener("DOMContentLoaded", onDocumentReady2Translate,
                             false);
 }
 
 // Listen to creation of content documents in order to translate them as soon
 // as possible in their loading process
-const ON_CONTENT = "DOMDocElementInserted";
+const ON_CONTENT = "document-element-inserted";
+let enabled = false;
 function enable() {
-  frames.addEventListener(ON_CONTENT, onContentWindow, true);
+  if (enabled)
+    return;
+  Services.obs.addObserver(onContentWindow, ON_CONTENT, false);
+  enabled = true;
 }
 process.port.on("sdk/l10n/html/enable", enable);
 
 function disable() {
-  frames.removeEventListener(ON_CONTENT, onContentWindow, true);
+  if (!enabled)
+    return;
+  Services.obs.removeObserver(onContentWindow, ON_CONTENT);
+  enabled = false;
 }
 process.port.on("sdk/l10n/html/disable", disable);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/page-worker.js
@@ -0,0 +1,153 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { frames } = require("../remote/child");
+const { Class } = require("../core/heritage");
+const { Disposable } = require('../core/disposable');
+const { data } = require("../self");
+const { once } = require("../dom/events");
+const { getAttachEventType } = require("./utils");
+const { Rules } = require('../util/rules');
+const { uuid } = require('../util/uuid');
+const { WorkerChild } = require("./worker-child");
+const { Cc, Ci, Cu } = require("chrome");
+const { observe } = require("../event/chrome");
+const { on } = require("../event/core");
+
+const appShell = Cc["@mozilla.org/appshell/appShellService;1"].getService(Ci.nsIAppShellService);
+
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+const pages = new Map();
+
+const DOC_INSERTED = "document-element-inserted";
+
+function isValidURL(page, url) {
+  return !page.rules || page.rules.matchesAny(url);
+}
+
+const ChildPage = Class({
+  implements: [ Disposable ],
+  setup: function(frame, id, options) {
+    this.id = id;
+    this.frame = frame;
+    this.options = options;
+
+    this.webNav = appShell.createWindowlessBrowser(false);
+    this.docShell.allowJavascript = this.options.allow.script;
+
+    // Accessing the browser's window forces the initial about:blank document to
+    // be created before we start listening for notifications
+    this.contentWindow;
+
+    this.webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+
+    pages.set(this.id, this);
+
+    this.contentURL = options.contentURL;
+
+    if (options.include) {
+      this.rules = Rules();
+      this.rules.add.apply(this.rules, [].concat(options.include));
+    }
+  },
+
+  dispose: function() {
+    pages.delete(this.id);
+    this.webProgress.removeProgressListener(this);
+    this.webNav = null;
+  },
+
+  attachWorker: function() {
+    if (!isValidURL(this, this.contentWindow.location.href))
+      return;
+
+    this.options.id = uuid().toString();
+    this.options.window = this.contentWindow;
+    this.frame.port.emit("sdk/frame/connect", this.id, {
+      id: this.options.id,
+      url: this.contentWindow.document.documentURIObject.spec
+    });
+    new WorkerChild(this.options);
+  },
+
+  get docShell() {
+    return this.webNav.QueryInterface(Ci.nsIInterfaceRequestor)
+                      .getInterface(Ci.nsIDocShell);
+  },
+
+  get webProgress() {
+    return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIWebProgress);
+  },
+
+  get contentWindow() {
+    return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIDOMWindow);
+  },
+
+  get contentURL() {
+    return this.options.contentURL;
+  },
+  set contentURL(url) {
+    this.options.contentURL = url;
+
+    url = this.options.contentURL ? data.url(this.options.contentURL) : "about:blank";
+    this.webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
+  },
+
+  onLocationChange: function(progress, request, location, flags) {
+    // Ignore inner-frame events
+    if (progress != this.webProgress)
+      return;
+    // Ignore events that don't change the document
+    if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
+      return;
+
+    let event = getAttachEventType(this.options);
+    // Attaching at the start of the load is handled by the
+    // document-element-inserted listener.
+    if (event == DOC_INSERTED)
+      return;
+
+    once(this.contentWindow, event, () => {
+      this.attachWorker();
+    }, false);
+  },
+
+  QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"])
+});
+
+on(observe(DOC_INSERTED), "data", ({ target }) => {
+  let page = Array.from(pages.values()).find(p => p.contentWindow.document === target);
+  if (!page)
+    return;
+
+  if (getAttachEventType(page.options) == DOC_INSERTED)
+    page.attachWorker();
+});
+
+frames.port.on("sdk/frame/create", (frame, id, options) => {
+  new ChildPage(frame, id, options);
+});
+
+frames.port.on("sdk/frame/set", (frame, id, params) => {
+  let page = pages.get(id);
+  if (!page)
+    return;
+
+  if ("allowScript" in params)
+    page.docShell.allowJavascript = params.allowScript;
+  if ("contentURL" in params)
+    page.contentURL = params.contentURL;
+});
+
+frames.port.on("sdk/frame/destroy", (frame, id) => {
+  let page = pages.get(id);
+  if (!page)
+    return;
+
+  page.destroy();
+});
--- a/addon-sdk/source/lib/sdk/content/sandbox.js
+++ b/addon-sdk/source/lib/sdk/content/sandbox.js
@@ -48,17 +48,17 @@ const secMan = Cc["@mozilla.org/scriptse
 
 const JS_VERSION = '1.8';
 
 // Tests whether this window is loaded in a tab
 function isWindowInTab(window) {
   if (isChildLoader) {
     let { frames } = require('../remote/child');
     let frame = frames.getFrameForWindow(window.top);
-    return frame.isTab;
+    return frame && frame.isTab;
   }
   else {
     // The deprecated sync worker API still does everything in the main process
     return getTabForContentWindow(window);
   }
 }
 
 const WorkerSandbox = Class({
--- a/addon-sdk/source/lib/sdk/content/worker-child.js
+++ b/addon-sdk/source/lib/sdk/content/worker-child.js
@@ -46,17 +46,19 @@ const WorkerChild = Class({
     for (let topic in EVENTS)
       system.on(topic, this.observe);
 
     this.receive = this.receive.bind(this);
     process.port.on('sdk/worker/message', this.receive);
 
     this.sandbox = WorkerSandbox(this, this.window);
 
-    this.frozen = false;
+    // If the document is still loading wait for it to finish before passing on
+    // received messages
+    this.frozen = this.window.document.readyState == "loading";
     this.frozenMessages = [];
     this.on('pageshow', () => {
       this.frozen = false;
       this.frozenMessages.forEach(args => this.sandbox.emit(...args));
       this.frozenMessages = [];
     });
     this.on('pagehide', () => {
       this.frozen = true;
--- a/addon-sdk/source/lib/sdk/frame/utils.js
+++ b/addon-sdk/source/lib/sdk/frame/utils.js
@@ -52,48 +52,41 @@ function create(target, options) {
   let document = target.ownerDocument;
 
   let frame = document.createElementNS(namespaceURI, nodeName);
   // Type="content" is mandatory to enable stuff here:
   // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1776
   frame.setAttribute('type', options.type || 'content');
   frame.setAttribute('src', options.uri || 'about:blank');
 
+  // Must set the remote attribute before attaching the frame to the document
+  if (remote && isXUL) {
+    // We remove XBL binding to avoid execution of code that is not going to
+    // work because browser has no docShell attribute in remote mode
+    // (for example)
+    frame.setAttribute('style', '-moz-binding: none;');
+    frame.setAttribute('remote', 'true');
+  }
+
   target.appendChild(frame);
 
   // Load in separate process if `options.remote` is `true`.
   // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1347
-  if (remote) {
-    if (isXUL) {
-      // We remove XBL binding to avoid execution of code that is not going to
-      // work because browser has no docShell attribute in remote mode
-      // (for example)
-      frame.setAttribute('style', '-moz-binding: none;');
-      frame.setAttribute('remote', 'true');
-    }
-    else {
-      frame.QueryInterface(Ci.nsIMozBrowserFrame);
-      frame.createRemoteFrameLoader(null);
-    }
+  if (remote && !isXUL) {
+    frame.QueryInterface(Ci.nsIMozBrowserFrame);
+    frame.createRemoteFrameLoader(null);
   }
 
-
-
   // If browser is remote it won't have a `docShell`.
   if (!remote) {
     let docShell = getDocShell(frame);
     docShell.allowAuth = options.allowAuth || false;
     docShell.allowJavascript = options.allowJavascript || false;
     docShell.allowPlugins = options.allowPlugins || false;
-
-    // Control whether the document can move/resize the window. Requires
-    // recently added platform capability, so we test to avoid exceptions
-    // in cases where capability is not present yet.
-    if ("allowWindowControl" in docShell && "allowWindowControl" in options)
-      docShell.allowWindowControl = !!options.allowWindowControl;
+    docShell.allowWindowControl = options.allowWindowControl || false;
   }
 
   return frame;
 }
 exports.create = create;
 
 function swapFrameLoaders(from, to) {
   return from.QueryInterface(Ci.nsIFrameLoaderOwner).swapFrameLoaders(to);
--- a/addon-sdk/source/lib/sdk/page-worker.js
+++ b/addon-sdk/source/lib/sdk/page-worker.js
@@ -3,195 +3,192 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 module.metadata = {
   "stability": "stable"
 };
 
 const { Class } = require('./core/heritage');
-const { on, emit, off, setListeners } = require('./event/core');
-const { filter, pipe, map, merge: streamMerge, stripListeners } = require('./event/utils');
-const { detach, attach, destroy, WorkerHost } = require('./content/utils');
-const { Worker } = require('./deprecated/sync-worker');
+const { ns } = require('./core/namespace');
+const { pipe, stripListeners } = require('./event/utils');
+const { connect, destroy, WorkerHost } = require('./content/utils');
+const { Worker } = require('./content/worker');
 const { Disposable } = require('./core/disposable');
-const { WeakReference } = require('./core/reference');
 const { EventTarget } = require('./event/target');
-const { unload } = require('./system/unload');
-const { events, streamEventsFrom } = require('./content/events');
-const { getAttachEventType } = require('./content/utils');
+const { setListeners } = require('./event/core');
 const { window } = require('./addon/window');
-const { getParentWindow } = require('./window/utils');
 const { create: makeFrame, getDocShell } = require('./frame/utils');
 const { contract } = require('./util/contract');
 const { contract: loaderContract } = require('./content/loader');
-const { has } = require('./util/array');
 const { Rules } = require('./util/rules');
 const { merge } = require('./util/object');
-const { data } = require('./self');
-const { getActiveView } = require("./view/core");
+const { uuid } = require('./util/uuid');
+const { useRemoteProcesses, remoteRequire, frames } = require("./remote/parent");
+remoteRequire("sdk/content/page-worker");
+
+const workers = new WeakMap();
+const pages = new Map();
+
+const internal = ns();
 
-const views = new WeakMap();
-const workers = new WeakMap();
-const pages = new WeakMap();
+let workerFor = (page) => workers.get(page);
+let isDisposed = (page) => !pages.has(internal(page).id);
+
+// The frame is used to ensure we have a remote process to load workers in
+let remoteFrame = null;
+let framePromise = null;
+function getFrame() {
+  if (framePromise)
+    return framePromise;
 
-const readyEventNames = [
-  'DOMContentLoaded',
-  'document-element-inserted',
-  'load'
-];
+  framePromise = new Promise(resolve => {
+    let view = makeFrame(window.document, {
+      namespaceURI: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+      nodeName: "iframe",
+      type: "content",
+      remote: useRemoteProcesses,
+      uri: "about:blank"
+    });
 
-function workerFor(page) {
-  return workers.get(page);
-}
-function pageFor(view) {
-  return pages.get(view);
-}
-function viewFor(page) {
-  return views.get(page);
-}
-function isDisposed (page) {
-  return !views.get(page, false);
+    // Wait for the remote side to connect
+    let listener = (frame) => {
+      if (frame.frameElement != view)
+        return;
+      frames.off("attach", listener);
+      remoteFrame = frame;
+      resolve(frame);
+    }
+    frames.on("attach", listener);
+  });
+  return framePromise;
 }
 
 var pageContract = contract(merge({
   allow: {
     is: ['object', 'undefined', 'null'],
     map: function (allow) { return { script: !allow || allow.script !== false }}
   },
   onMessage: {
     is: ['function', 'undefined']
   },
   include: {
     is: ['string', 'array', 'regexp', 'undefined']
   },
   contentScriptWhen: {
-    is: ['string', 'undefined']
+    is: ['string', 'undefined'],
+    map: (when) => when || "end"
   }
 }, loaderContract.rules));
 
 function enableScript (page) {
   getDocShell(viewFor(page)).allowJavascript = true;
 }
 
 function disableScript (page) {
   getDocShell(viewFor(page)).allowJavascript = false;
 }
 
 function Allow (page) {
   return {
-    get script() { return getDocShell(viewFor(page)).allowJavascript; },
-    set script(value) { return value ? enableScript(page) : disableScript(page); }
-  };
-}
+    get script() {
+      return internal(page).options.allow.script;
+    },
+    set script(value) {
+      internal(page).options.allow.script = value;
 
-function injectWorker ({page}) {
-  let worker = workerFor(page);
-  let view = viewFor(page);
-  if (isValidURL(page, view.contentDocument.URL))
-    attach(worker, view.contentWindow);
+      if (isDisposed(page))
+        return;
+
+      remoteFrame.port.emit("sdk/frame/set", internal(page).id, { allowScript: value });
+    }
+  };
 }
 
 function isValidURL(page, url) {
   return !page.rules || page.rules.matchesAny(url);
 }
 
 const Page = Class({
   implements: [
     EventTarget,
-    Disposable,
-    WeakReference
+    Disposable
   ],
   extends: WorkerHost(workerFor),
   setup: function Page(options) {
-    let page = this;
     options = pageContract(options);
-
-    let uri = options.contentURL;
+    // Sanitize the options
+    if ("contentScriptOptions" in options)
+      options.contentScriptOptions = JSON.stringify(options.contentScriptOptions);
 
-    let view = makeFrame(window.document, {
-      nodeName: 'iframe',
-      type: 'content',
-      uri: uri ? data.url(uri) : '',
-      allowJavascript: options.allow.script,
-      allowPlugins: true,
-      allowAuth: true
-    });
-    view.setAttribute('data-src', uri);
+    internal(this).id = uuid().toString();
+    internal(this).options = options;
 
-    ['contentScriptFile', 'contentScript', 'contentScriptWhen']
-      .forEach(prop => page[prop] = options[prop]);
+    for (let prop of ['contentScriptFile', 'contentScript', 'contentScriptWhen']) {
+      this[prop] = options[prop];
+    }
 
-    views.set(this, view);
-    pages.set(view, this);
+    pages.set(internal(this).id, this);
 
     // Set listeners on the {Page} object itself, not the underlying worker,
     // like `onMessage`, as it gets piped
     setListeners(this, options);
     let worker = new Worker(stripListeners(options));
     workers.set(this, worker);
     pipe(worker, this);
 
-    if (this.include || options.include) {
+    if (options.include) {
       this.rules = Rules();
-      this.rules.add.apply(this.rules, [].concat(this.include || options.include));
+      this.rules.add.apply(this.rules, [].concat(options.include));
     }
+
+    getFrame().then(frame => {
+      if (isDisposed(this))
+        return;
+
+      frame.port.emit("sdk/frame/create", internal(this).id, stripListeners(options));
+    });
   },
   get allow() { return Allow(this); },
   set allow(value) {
-    let allowJavascript = pageContract({ allow: value }).allow.script;
-    return allowJavascript ? enableScript(this) : disableScript(this);
+    if (isDisposed(this))
+      return;
+    this.allow.script = pageContract({ allow: value }).allow.script;
   },
-  get contentURL() { return viewFor(this).getAttribute("data-src") },
+  get contentURL() {
+    return internal(this).options.contentURL;
+  },
   set contentURL(value) {
-    if (!isValidURL(this, value)) return;
-    let view = viewFor(this);
-    let contentURL = pageContract({ contentURL: value }).contentURL;
+    if (!isValidURL(this, value))
+      return;
+    internal(this).options.contentURL = value;
+    if (isDisposed(this))
+      return;
 
-    // page-worker doesn't have a model like other APIs, so to be consitent
-    // with the behavior "what you set is what you get", we need to store
-    // the original `contentURL` given.
-    // Even if XUL elements doesn't support `dataset`, properties, to
-    // indicate that is a custom attribute the syntax "data-*" is used.
-    view.setAttribute('data-src', contentURL);
-    view.setAttribute('src', data.url(contentURL));
+    remoteFrame.port.emit("sdk/frame/set", internal(this).id, { contentURL: value });
   },
   dispose: function () {
-    if (isDisposed(this)) return;
-    let view = viewFor(this);
-    if (view.parentNode) view.parentNode.removeChild(view);
-    views.delete(this);
-    destroy(workers.get(this));
+    if (isDisposed(this))
+      return;
+    pages.delete(internal(this).id);
+    let worker = workerFor(this);
+    if (worker)
+      destroy(worker);
+    remoteFrame.port.emit("sdk/frame/destroy", internal(this).id);
+
+    // Destroy the remote frame if all the pages have been destroyed
+    if (pages.size == 0) {
+      framePromise = null;
+      remoteFrame.frameElement.remove();
+      remoteFrame = null;
+    }
   },
   toString: function () { return '[object Page]' }
 });
 
 exports.Page = Page;
 
-var pageEvents = streamMerge([events, streamEventsFrom(window)]);
-var readyEvents = filter(pageEvents, isReadyEvent);
-var formattedEvents = map(readyEvents, function({target, type}) {
-  return { type: type, page: pageFromDoc(target) };
+frames.port.on("sdk/frame/connect", (frame, id, params) => {
+  let page = pages.get(id);
+  if (!page)
+    return;
+  connect(workerFor(page), frame, params);
 });
-var pageReadyEvents = filter(formattedEvents, function({page, type}) {
-  return getAttachEventType(page) === type});
-on(pageReadyEvents, 'data', injectWorker);
-
-function isReadyEvent ({type}) {
-  return has(readyEventNames, type);
-}
-
-/*
- * Takes a document, finds its doc shell tree root and returns the
- * matching Page instance if found
- */
-function pageFromDoc(doc) {
-  let parentWindow = getParentWindow(doc.defaultView), page;
-  if (!parentWindow) return;
-
-  let frames = parentWindow.document.getElementsByTagName('iframe');
-  for (let i = frames.length; i--;)
-    if (frames[i].contentDocument === doc && (page = pageFor(frames[i])))
-      return page;
-  return null;
-}
-
-getActiveView.define(Page, viewFor);
--- a/addon-sdk/source/lib/sdk/remote/parent.js
+++ b/addon-sdk/source/lib/sdk/remote/parent.js
@@ -24,16 +24,21 @@ const { when } = require('../system/unlo
 const { EventTarget } = require('../event/target');
 const { emit } = require('../event/core');
 const system = require('../system/events');
 const { EventParent } = require('./utils');
 const options = require('@loader/options');
 const loaderModule = require('toolkit/loader');
 const { getTabForBrowser } = require('../tabs/utils');
 
+const appInfo = Cc["@mozilla.org/xre/app-info;1"].
+                getService(Ci.nsIXULRuntime);
+
+exports.useRemoteProcesses = appInfo.browserTabsRemoteAutostart;
+
 // Chose the right function for resolving relative a module id
 var moduleResolve;
 if (options.isNative) {
   moduleResolve = (id, requirer) => loaderModule.nodeResolve(id, requirer, { rootURI: options.rootURI });
 }
 else {
   moduleResolve = loaderModule.resolve;
 }
@@ -169,17 +174,20 @@ const Frame = Class({
     let frameLoader = node.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
     ns(this).messageManager = frameLoader.messageManager;
 
     ns(this).messageReceived = frameMessageReceived.bind(this);
     ns(this).messageManager.addMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
 
     this.port = new EventTarget();
     this.port.emit = (...args) => {
-      ns(this).messageManager.sendAsyncMessage('sdk/remote/frame/message', {
+      let manager = ns(this).messageManager;
+      if (!manager)
+        return;
+      manager.sendAsyncMessage('sdk/remote/frame/message', {
         loaderID,
         args
       });
     };
 
     frameMap.set(ns(this).messageManager, this);
   },
 
--- a/addon-sdk/source/test/test-addon-extras.js
+++ b/addon-sdk/source/test/test-addon-extras.js
@@ -58,114 +58,13 @@ exports["test changing result from addon
     assert.equal(result, true, "result is a boolean");
     loader.unload();
     done();
   });
 
   panel.port.emit("get-result");
 }
 
-exports["test window result from addon extras in panel"] = function*(assert) {
-  let loader = Loader(module, null, null, {
-    modules: {
-      "sdk/self": merge({}, self, {
-        data: merge({}, self.data, {url: fixtures.url})
-      })
-    }
-  });
-
-  const { Panel } = loader.require('sdk/panel');
-  const { Page } = loader.require('sdk/page-worker');
-  const { getActiveView } = loader.require("sdk/view/core");
-  const { getDocShell } = loader.require('sdk/frame/utils');
-  const { events } = loader.require("sdk/content/sandbox/events");
-  const { on } = loader.require("sdk/event/core");
-  const { isAddonContent } = loader.require("sdk/content/utils");
-
-  // make a page worker and wait for it to load
-  var page = yield new Promise(resolve => {
-    assert.pass("Creating the background page");
-
-    let page = Page({
-      contentURL: "./test.html",
-      contentScriptWhen: "end",
-      contentScript: "self.port.emit('end', unsafeWindow.getTestURL() + '')"
-    });
-
-    page.port.once("end", (url) => {
-      assert.equal(url, fixtures.url("./test.html"), "url is correct");
-      resolve(page);
-    });
-  });
-  assert.pass("Created the background page");
-
-  var extrasVal = {
-    test: function() {
-      assert.pass("start test function");
-      let frame = getActiveView(page);
-      let window = getUnsafeWindow(frame.contentWindow);
-      assert.equal(typeof window.getTestURL, "function", "window.getTestURL is a function");
-      return window;
-    }
-  };
-
-  on(events, "content-script-before-inserted", ({ window, worker }) => {
-    let url = window.location.href;
-    assert.pass("content-script-before-inserted " + url);
-
-    if (isAddonContent({ contentURL: url })) {
-      let extraStuff = Cu.cloneInto(extrasVal, window, {
-        cloneFunctions: true
-      });
-      getUnsafeWindow(window).extras = extraStuff;
-
-      assert.pass("content-script-before-inserted done!");
-    }
-  });
-
-  let panel = Panel({
-    contentURL: "./test-addon-extras-window.html"
-  });
-
-
-  yield new Promise(resolve => {
-    panel.port.once("result1", (result) => {
-      assert.equal(result, fixtures.url("./test.html"), "result1 is a window");
-      resolve();
-    });
-
-    assert.pass("emit get-result");
-    panel.port.emit("get-result");
-  });
-
-  page.destroy();
-
-
-  page = yield new Promise(resolve => {
-    let page = Page({
-      contentURL: "./index.html",
-      contentScriptWhen: "end",
-      contentScript: "self.port.emit('end')"
-    });
-    page.port.once("end", () => resolve(page));
-  });
-
-
-  yield new Promise(resolve => {
-    panel.port.once("result2", (result) => {
-      assert.equal(result, fixtures.url("./index.html"), "result2 is a window");
-      resolve();
-    });
-
-    assert.pass("emit get-result");
-    panel.port.emit("get-result");
-  });
-
-  loader.unload();
-}
-
-
-
 function getUnsafeWindow (win) {
   return win.wrappedJSObject || win;
 }
 
 require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-page-worker.js
+++ b/addon-sdk/source/test/test-page-worker.js
@@ -322,29 +322,16 @@ exports.testAllowScript = function(asser
       done();
     },
     allow: { script: true },
     contentURL: "data:text/html;charset=utf-8,<script>document.documentElement.setAttribute('foo', 3);</script>",
     contentScript: "self.postMessage(document.documentElement.hasAttribute('foo') && " +
                    "                 document.documentElement.getAttribute('foo') == 3)",
     contentScriptWhen: "ready"
   });
-
-  let frame = getActiveView(page);
-  assert.equal(getDocShell(frame).allowJavascript, true, "allowJavascript is true");
-}
-
-exports.testGetActiveViewAndDestroy = function(assert) {
-  let page = Page({
-    contentURL: "data:text/html;charset=utf-8,<title>test</title>"
-  });
-  let frame = getActiveView(page);
-  assert.ok(frame.parentNode, "there is a parent node");
-  page.destroy();
-  assert.ok(!frame.parentNode, "there is not a parent node");
 }
 
 exports.testPingPong = function(assert, done) {
   let page = Page({
     contentURL: 'data:text/html;charset=utf-8,ping-pong',
     contentScript: 'self.on("message", message => self.postMessage("pong"));'
       + 'self.postMessage("ready");',
     onMessage: function(message) {