toolkit/components/social/FrameWorker.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/social/FrameWorker.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,219 @@
     1.4 +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ts=2 et sw=2 tw=80: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/.
     1.9 + */
    1.10 +
    1.11 +/*
    1.12 + * This is an implementation of a "Shared Worker" using a remote browser
    1.13 + * in the hidden DOM window.  This is the implementation that lives in the
    1.14 + * "chrome process".  See FrameWorkerContent for code that lives in the
    1.15 + * "content" process and which sets up a sandbox for the worker.
    1.16 + */
    1.17 +
    1.18 +"use strict";
    1.19 +
    1.20 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    1.21 +
    1.22 +Cu.import("resource://gre/modules/Services.jsm");
    1.23 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.24 +Cu.import("resource://gre/modules/MessagePortBase.jsm");
    1.25 +Cu.import("resource://gre/modules/Promise.jsm");
    1.26 +
    1.27 +XPCOMUtils.defineLazyModuleGetter(this, "SocialService",
    1.28 +  "resource://gre/modules/SocialService.jsm");
    1.29 +
    1.30 +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    1.31 +const HTML_NS = "http://www.w3.org/1999/xhtml";
    1.32 +
    1.33 +this.EXPORTED_SYMBOLS = ["getFrameWorkerHandle"];
    1.34 +
    1.35 +var workerCache = {}; // keyed by URL.
    1.36 +var _nextPortId = 1;
    1.37 +
    1.38 +// Retrieves a reference to a WorkerHandle associated with a FrameWorker and a
    1.39 +// new ClientPort.
    1.40 +this.getFrameWorkerHandle =
    1.41 + function getFrameWorkerHandle(url, clientWindow, name, origin, exposeLocalStorage = false) {
    1.42 +  // prevent data/about urls - see bug 891516
    1.43 +  if (['http', 'https'].indexOf(Services.io.newURI(url, null, null).scheme) < 0)
    1.44 +    throw new Error("getFrameWorkerHandle requires http/https urls");
    1.45 +
    1.46 +  // See if we already have a worker with this URL.
    1.47 +  let existingWorker = workerCache[url];
    1.48 +  if (!existingWorker) {
    1.49 +    // create a remote browser and _Worker object - this will message the
    1.50 +    // remote browser to do the content side of things.
    1.51 +    let browserPromise = makeRemoteBrowser();
    1.52 +    let options = { url: url, name: name, origin: origin,
    1.53 +                    exposeLocalStorage: exposeLocalStorage };
    1.54 +
    1.55 +    existingWorker = workerCache[url] = new _Worker(browserPromise, options);
    1.56 +  }
    1.57 +
    1.58 +  // message the content so it can establish a new connection with the worker.
    1.59 +  let portid = _nextPortId++;
    1.60 +  existingWorker.browserPromise.then(browser => {
    1.61 +    browser.messageManager.sendAsyncMessage("frameworker:connect",
    1.62 +                                            { portId: portid });
    1.63 +  });
    1.64 +  // return the pseudo worker object.
    1.65 +  let port = new ParentPort(portid, existingWorker.browserPromise, clientWindow);
    1.66 +  existingWorker.ports.set(portid, port);
    1.67 +  return new WorkerHandle(port, existingWorker);
    1.68 +};
    1.69 +
    1.70 +// A "_Worker" is an internal representation of a worker.  It's never returned
    1.71 +// directly to consumers.
    1.72 +function _Worker(browserPromise, options) {
    1.73 +  this.browserPromise = browserPromise;
    1.74 +  this.options = options;
    1.75 +  this.ports = new Map();
    1.76 +  browserPromise.then(browser => {
    1.77 +    browser.addEventListener("oop-browser-crashed", () => {
    1.78 +      Cu.reportError("FrameWorker remote process crashed");
    1.79 +      notifyWorkerError(options.origin);
    1.80 +    });
    1.81 +
    1.82 +    let mm = browser.messageManager;
    1.83 +    // execute the content script and send the message to bootstrap the content
    1.84 +    // side of the world.
    1.85 +    mm.loadFrameScript("resource://gre/modules/FrameWorkerContent.js", true);
    1.86 +    mm.sendAsyncMessage("frameworker:init", this.options);
    1.87 +    mm.addMessageListener("frameworker:port-message", this);
    1.88 +    mm.addMessageListener("frameworker:notify-worker-error", this);
    1.89 +  });
    1.90 +}
    1.91 +
    1.92 +_Worker.prototype = {
    1.93 +  // Message handler.
    1.94 +  receiveMessage: function(msg) {
    1.95 +    switch (msg.name) {
    1.96 +      case "frameworker:port-message":
    1.97 +        let port = this.ports.get(msg.data.portId);
    1.98 +        port._onmessage(msg.data.data);
    1.99 +        break;
   1.100 +      case "frameworker:notify-worker-error":
   1.101 +        notifyWorkerError(msg.data.origin);
   1.102 +        break;
   1.103 +    }
   1.104 +  }
   1.105 +}
   1.106 +
   1.107 +// This WorkerHandle is exposed to consumers - it has the new port instance
   1.108 +// the consumer uses to communicate with the worker.
   1.109 +// public methods/properties on WorkerHandle should conform to the SharedWorker
   1.110 +// api - currently that's just .port and .terminate()
   1.111 +function WorkerHandle(port, worker) {
   1.112 +  this.port = port;
   1.113 +  this._worker = worker;
   1.114 +}
   1.115 +
   1.116 +WorkerHandle.prototype = {
   1.117 +  // A method to terminate the worker.  The worker spec doesn't define a
   1.118 +  // callback to be made in the worker when this happens, so we just kill the
   1.119 +  // browser element.
   1.120 +  terminate: function terminate() {
   1.121 +    let url = this._worker.options.url;
   1.122 +    if (!(url in workerCache)) {
   1.123 +      // terminating an already terminated worker - ignore it
   1.124 +      return;
   1.125 +    }
   1.126 +    delete workerCache[url];
   1.127 +    // close all the ports we have handed out.
   1.128 +    for (let [portid, port] of this._worker.ports) {
   1.129 +      port.close();
   1.130 +    }
   1.131 +    this._worker.ports.clear();
   1.132 +    this._worker.ports = null;
   1.133 +    this._worker.browserPromise.then(browser => {
   1.134 +      let iframe = browser.ownerDocument.defaultView.frameElement;
   1.135 +      iframe.parentNode.removeChild(iframe);
   1.136 +    });
   1.137 +    // wipe things out just incase other reference have snuck out somehow...
   1.138 +    this._worker.browserPromise = null;
   1.139 +    this._worker = null;
   1.140 +  }
   1.141 +};
   1.142 +
   1.143 +// The port that lives in the parent chrome process.  The other end of this
   1.144 +// port is the "client" port in the content process, which itself is just a
   1.145 +// shim which shuttles messages to/from the worker itself.
   1.146 +function ParentPort(portid, browserPromise, clientWindow) {
   1.147 +  this._clientWindow = clientWindow;
   1.148 +  this._browserPromise = browserPromise;
   1.149 +  AbstractPort.call(this, portid);
   1.150 +}
   1.151 +
   1.152 +ParentPort.prototype = {
   1.153 +  __exposedProps__: {
   1.154 +    onmessage: "rw",
   1.155 +    postMessage: "r",
   1.156 +    close: "r",
   1.157 +    toString: "r"
   1.158 +  },
   1.159 +  __proto__: AbstractPort.prototype,
   1.160 +  _portType: "parent",
   1.161 +
   1.162 +  _dopost: function(data) {
   1.163 +    this._browserPromise.then(browser => {
   1.164 +      browser.messageManager.sendAsyncMessage("frameworker:port-message", data);
   1.165 +    });
   1.166 +  },
   1.167 +
   1.168 +  _onerror: function(err) {
   1.169 +    Cu.reportError("FrameWorker: Port " + this + " handler failed: " + err + "\n" + err.stack);
   1.170 +  },
   1.171 +
   1.172 +  _JSONParse: function(data) {
   1.173 +    if (this._clientWindow) {
   1.174 +      return XPCNativeWrapper.unwrap(this._clientWindow).JSON.parse(data);
   1.175 +    }
   1.176 +    return JSON.parse(data);
   1.177 +  },
   1.178 +
   1.179 +  close: function() {
   1.180 +    if (this._closed) {
   1.181 +      return; // already closed.
   1.182 +    }
   1.183 +    // a leaky abstraction due to the worker spec not specifying how the
   1.184 +    // other end of a port knows it is closing.
   1.185 +    this.postMessage({topic: "social.port-closing"});
   1.186 +    AbstractPort.prototype.close.call(this);
   1.187 +    this._clientWindow = null;
   1.188 +    // this._pendingMessagesOutgoing should still be drained, as a closed
   1.189 +    // port will still get "entangled" quickly enough to deliver the messages.
   1.190 +  }
   1.191 +}
   1.192 +
   1.193 +// Make the <browser remote="true"> element that hosts the worker.
   1.194 +function makeRemoteBrowser() {
   1.195 +  let deferred = Promise.defer();
   1.196 +  let hiddenDoc = Services.appShell.hiddenDOMWindow.document;
   1.197 +  // Create a HTML iframe with a chrome URL, then this can host the browser.
   1.198 +  let iframe = hiddenDoc.createElementNS(HTML_NS, "iframe");
   1.199 +  iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
   1.200 +  iframe.addEventListener("load", function onLoad() {
   1.201 +    iframe.removeEventListener("load", onLoad, true);
   1.202 +    let browser = iframe.contentDocument.createElementNS(XUL_NS, "browser");
   1.203 +    browser.setAttribute("type", "content");
   1.204 +    browser.setAttribute("disableglobalhistory", "true");
   1.205 +    browser.setAttribute("remote", "true");
   1.206 +
   1.207 +    iframe.contentDocument.documentElement.appendChild(browser);
   1.208 +    deferred.resolve(browser);
   1.209 +  }, true);
   1.210 +  hiddenDoc.documentElement.appendChild(iframe);
   1.211 +  return deferred.promise;
   1.212 +}
   1.213 +
   1.214 +function notifyWorkerError(origin) {
   1.215 +  // Try to retrieve the worker's associated provider, if it has one, to set its
   1.216 +  // error state.
   1.217 +  SocialService.getProvider(origin, function (provider) {
   1.218 +    if (provider)
   1.219 +      provider.errorState = "frameworker-error";
   1.220 +    Services.obs.notifyObservers(null, "social:frameworker-error", origin);
   1.221 +  });
   1.222 +}

mercurial