Bug 1225473 - Support Service workers in child process. r=janx
authorAlexandre Poirot <poirot.alex@gmail.com>
Thu, 03 Dec 2015 06:42:34 -0800
changeset 309625 310bf739e766fb1755e48c3e53f9d9aa73352f56
parent 309624 3858001d34b3f237ce11f9685284bebc36515a2d
child 309626 20fc590db5655f299da87df165845d2f0127fbb5
push id5513
push userraliiev@mozilla.com
push dateMon, 25 Jan 2016 13:55:34 +0000
treeherdermozilla-beta@5ee97dd05b5c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanx
bugs1225473
milestone45.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 1225473 - Support Service workers in child process. r=janx
devtools/client/aboutdebugging/components/target-list.js
devtools/client/aboutdebugging/components/workers.js
devtools/client/aboutdebugging/test/browser.ini
devtools/client/aboutdebugging/test/browser_service_workers.js
devtools/client/aboutdebugging/test/head.js
devtools/client/aboutdebugging/test/service-workers/empty-sw.html
devtools/client/aboutdebugging/test/service-workers/empty-sw.js
devtools/server/actors/child-process.js
devtools/server/actors/root.js
devtools/server/actors/worker.js
devtools/server/content-server.jsm
--- a/devtools/client/aboutdebugging/components/target-list.js
+++ b/devtools/client/aboutdebugging/components/target-list.js
@@ -22,16 +22,16 @@ exports.TargetListComponent = React.crea
   displayName: "TargetListComponent",
 
   render() {
     let client = this.props.client;
     let targets = this.props.targets.sort(LocaleCompare).map(target => {
       return React.createElement(TargetComponent, { client, target });
     });
     return (
-      React.createElement("div", { className: "targets" },
+      React.createElement("div", { id: this.props.id, className: "targets" },
         React.createElement("h4", null, this.props.name),
         targets.length > 0 ? targets :
           React.createElement("p", null, Strings.GetStringFromName("nothing"))
       )
     );
   },
 });
--- a/devtools/client/aboutdebugging/components/workers.js
+++ b/devtools/client/aboutdebugging/components/workers.js
@@ -9,16 +9,18 @@
 loader.lazyRequireGetter(this, "Ci",
   "chrome", true);
 loader.lazyRequireGetter(this, "React",
   "devtools/client/shared/vendor/react");
 loader.lazyRequireGetter(this, "TargetListComponent",
   "devtools/client/aboutdebugging/components/target-list", true);
 loader.lazyRequireGetter(this, "Services");
 
+loader.lazyImporter(this, "Task", "resource://gre/modules/Task.jsm");
+
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 const WorkerIcon = "chrome://devtools/skin/images/debugging-workers.svg";
 
 exports.WorkersComponent = React.createClass({
   displayName: "WorkersComponent",
 
   getInitialState() {
@@ -27,45 +29,50 @@ exports.WorkersComponent = React.createC
         service: [],
         shared: [],
         other: []
       }
     };
   },
 
   componentDidMount() {
-    this.props.client.addListener("workerListChanged", this.update);
+    let client = this.props.client;
+    client.addListener("workerListChanged", this.update);
+    client.addListener("processListChanged", this.update);
     this.update();
   },
 
   componentWillUnmount() {
-    this.props.client.removeListener("workerListChanged", this.update);
+    let client = this.props.client;
+    client.removeListener("processListChanged", this.update);
+    client.removeListener("workerListChanged", this.update);
   },
 
   render() {
     let client = this.props.client;
     let workers = this.state.workers;
     return React.createElement("div", { className: "inverted-icons" },
       React.createElement(TargetListComponent, {
+        id: "service-workers",
         name: Strings.GetStringFromName("serviceWorkers"),
         targets: workers.service, client }),
       React.createElement(TargetListComponent, {
+        id: "shared-workers",
         name: Strings.GetStringFromName("sharedWorkers"),
         targets: workers.shared, client }),
       React.createElement(TargetListComponent, {
+        id: "other-workers",
         name: Strings.GetStringFromName("otherWorkers"),
         targets: workers.other, client })
     );
   },
 
   update() {
-    let client = this.props.client;
     let workers = this.getInitialState().workers;
-    client.mainRoot.listWorkers(response => {
-      let forms = response.workers;
+    this.getWorkerForms().then(forms => {
       forms.forEach(form => {
         let worker = {
           name: form.url,
           icon: WorkerIcon,
           actorID: form.actor
         };
         switch (form.type) {
           case Ci.nsIWorkerDebugger.TYPE_SERVICE:
@@ -78,10 +85,34 @@ exports.WorkersComponent = React.createC
             break;
           default:
             worker.type = "worker";
             workers.other.push(worker);
         }
       });
       this.setState({ workers });
     });
-  }
+  },
+
+  getWorkerForms: Task.async(function*() {
+    let client = this.props.client;
+
+    // List workers from the Parent process
+    let result = yield client.mainRoot.listWorkers();
+    let forms = result.workers;
+
+    // And then from the Child processes
+    let { processes } = yield client.mainRoot.listProcesses();
+    for (let process of processes) {
+      // Ignore parent process
+      if (process.parent) {
+        continue;
+      }
+      let { form } = yield client.getProcess(process.id);
+      let processActor = form.actor;
+      let { workers } = yield client.request({to: processActor,
+                                              type: "listWorkers"});
+      forms = forms.concat(workers);
+    }
+
+    return forms;
+  }),
 });
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -1,9 +1,12 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
   addons/unpacked/bootstrap.js
   addons/unpacked/install.rdf
+  service-workers/empty-sw.html
+  service-workers/empty-sw.js
 
 [browser_addons_install.js]
+[browser_service_workers.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Service workers can't be loaded from chrome://,
+// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
+const HTTP_ROOT = CHROME_ROOT.replace("chrome://mochitests/content/",
+                                      "http://mochi.test:8888/");
+const SERVICE_WORKER = HTTP_ROOT + "service-workers/empty-sw.js";
+const TAB_URL = HTTP_ROOT + "service-workers/empty-sw.html";
+
+function waitForWorkersUpdate(document) {
+  return new Promise(done => {
+    var observer = new MutationObserver(function(mutations) {
+      observer.disconnect();
+      done();
+    });
+    var target = document.getElementById("service-workers");
+    observer.observe(target, { childList: true });
+  });
+}
+
+add_task(function *() {
+  yield new Promise(done => {
+    let options = {"set": [
+                    ["dom.serviceWorkers.testing.enabled", true],
+                  ]};
+    SpecialPowers.pushPrefEnv(options, done);
+  });
+
+  let { tab, document } = yield openAboutDebugging("workers");
+
+  let swTab = yield addTab(TAB_URL);
+
+  yield waitForWorkersUpdate(document);
+
+  // Check that the service worker appears in the UI
+  let names = [...document.querySelectorAll("#service-workers .target-name")];
+  names = names.map(element => element.textContent);
+  ok(names.includes(SERVICE_WORKER), "The service worker url appears in the list: " + names);
+
+  // Use message manager to work with e10s
+  let frameScript = function () {
+    // Retrieve the `sw` promise created in the html page
+    let { sw } = content.wrappedJSObject;
+    sw.then(function (registration) {
+      registration.unregister().then(function (success) {
+        dump("SW unregistered: " + success + "\n");
+      },
+      function (e) {
+        dump("SW not unregistered; " + e + "\n");
+      });
+    });
+  };
+  swTab.linkedBrowser.messageManager.loadFrameScript("data:,(" + encodeURIComponent(frameScript) + ")()", true);
+
+  yield waitForWorkersUpdate(document);
+
+  // Check that the service worker disappeared from the UI
+  names = [...document.querySelectorAll("#service-workers .target-name")];
+  names = names.map(element => element.textContent);
+  ok(!names.includes(SERVICE_WORKER), "The service worker url is no longer in the list: " + names);
+
+  yield removeTab(swTab);
+  yield closeAboutDebugging(tab);
+});
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -11,19 +11,23 @@ const DevToolsUtils = require("devtools/
 DevToolsUtils.testing = true;
 
 const CHROME_ROOT = gTestPath.substr(0, gTestPath.lastIndexOf("/") + 1);
 
 registerCleanupFunction(() => {
   DevToolsUtils.testing = false;
 });
 
-function openAboutDebugging() {
+function openAboutDebugging(page) {
   info("opening about:debugging");
-  return addTab("about:debugging").then(tab => {
+  let url = "about:debugging";
+  if (page) {
+    url += "#" + page;
+  }
+  return addTab(url).then(tab => {
     let browser = tab.linkedBrowser;
     return {
       tab,
       document: browser.contentDocument,
       window: browser.contentWindow
     };
   });
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/empty-sw.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+var sw = navigator.serviceWorker.register("empty-sw.js");
+sw.then(
+  function (registration) {
+    dump("SW registered\n");
+  },
+  function (e) {
+    dump("SW not registered: " + e + "\n");
+  }
+);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/empty-sw.js
@@ -0,0 +1,1 @@
+// Empty, just test registering.
--- a/devtools/server/actors/child-process.js
+++ b/devtools/server/actors/child-process.js
@@ -9,16 +9,18 @@ const { Cc, Ci, Cu } = require("chrome")
 const { ChromeDebuggerActor } = require("devtools/server/actors/script");
 const { WebConsoleActor } = require("devtools/server/actors/webconsole");
 const makeDebugger = require("devtools/server/actors/utils/make-debugger");
 const { ActorPool } = require("devtools/server/main");
 const Services = require("Services");
 const { assert } = require("devtools/shared/DevToolsUtils");
 const { TabSources } = require("./utils/TabSources");
 
+loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker", true);
+
 function ChildProcessActor(aConnection) {
   this.conn = aConnection;
   this._contextPool = new ActorPool(this.conn);
   this.conn.addActorPool(this._contextPool);
   this.threadActor = null;
 
   // Use a see-everything debugger
   this.makeDebugger = makeDebugger.bind(null, {
@@ -27,16 +29,20 @@ function ChildProcessActor(aConnection) 
   });
 
   // Scope into which the webconsole executes:
   // An empty sandbox with chrome privileges
   let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
     .createInstance(Ci.nsIPrincipal);
   let sandbox = Cu.Sandbox(systemPrincipal);
   this._consoleScope = sandbox;
+
+  this._workerList = null;
+  this._workerActorPool = null;
+  this._onWorkerListChanged = this._onWorkerListChanged.bind(this);
 }
 exports.ChildProcessActor = ChildProcessActor;
 
 ChildProcessActor.prototype = {
   actorPrefix: "process",
 
   get isRootActor() {
     return true;
@@ -82,25 +88,59 @@ ChildProcessActor.prototype = {
 
       traits: {
         highlightable: false,
         networkMonitor: false,
       },
     };
   },
 
+  onListWorkers: function () {
+    if (!this._workerList) {
+      this._workerList = new WorkerActorList({});
+    }
+    return this._workerList.getList().then(actors => {
+      let pool = new ActorPool(this.conn);
+      for (let actor of actors) {
+        pool.addActor(actor);
+      }
+
+      this.conn.removeActorPool(this._workerActorPool);
+      this._workerActorPool = pool;
+      this.conn.addActorPool(this._workerActorPool);
+
+      this._workerList.onListChanged = this._onWorkerListChanged;
+
+      return {
+        "from": this.actorID,
+        "workers": actors.map(actor => actor.form())
+      };
+    });
+  },
+
+  _onWorkerListChanged: function () {
+    this.conn.send({ from: this.actorID, type: "workerListChanged" });
+    this._workerList.onListChanged = null;
+  },
+
   disconnect: function() {
     this.conn.removeActorPool(this._contextPool);
     this._contextPool = null;
+
+    // Tell the live lists we aren't watching any more.
+    if (this._workerList) {
+      this._workerList.onListChanged = null;
+    }
   },
 
   preNest: function() {
     // TODO: freeze windows
     // window mediator doesn't work in child.
     // it doesn't throw, but doesn't return any window
   },
 
   postNest: function() {
   },
 };
 
 ChildProcessActor.prototype.requestTypes = {
+  "listWorkers": ChildProcessActor.prototype.onListWorkers,
 };
--- a/devtools/server/actors/root.js
+++ b/devtools/server/actors/root.js
@@ -202,16 +202,19 @@ RootActor.prototype = {
   disconnect: function() {
     /* Tell the live lists we aren't watching any more. */
     if (this._parameters.tabList) {
       this._parameters.tabList.onListChanged = null;
     }
     if (this._parameters.addonList) {
       this._parameters.addonList.onListChanged = null;
     }
+    if (this._parameters.workerList) {
+      this._parameters.workerList.onListChanged = null;
+    }
     if (typeof this._parameters.onShutdown === 'function') {
       this._parameters.onShutdown();
     }
     this._extraActors = null;
     this.conn = null;
     this._tabActorPool = null;
     this._globalActorPool = null;
     this._parameters = null;
--- a/devtools/server/actors/worker.js
+++ b/devtools/server/actors/worker.js
@@ -188,16 +188,19 @@ WorkerActorList.prototype = {
   get onListChanged() {
     return this._onListChanged;
   },
 
   set onListChanged(onListChanged) {
     if (typeof onListChanged !== "function" && onListChanged !== null) {
       throw new Error("onListChanged must be either a function or null.");
     }
+    if (onListChanged === this._onListChanged) {
+      return;
+    }
 
     if (this._mustNotify) {
       if (this._onListChanged === null && onListChanged !== null) {
         wdm.addListener(this);
       }
       if (this._onListChanged !== null && onListChanged === null) {
         wdm.removeListener(this);
       }
--- a/devtools/server/content-server.jsm
+++ b/devtools/server/content-server.jsm
@@ -8,24 +8,17 @@ const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 
 const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 const { DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 
 this.EXPORTED_SYMBOLS = ["init"];
 
-var started = false;
-
 function init(msg) {
-  if (started) {
-    return;
-  }
-  started = true;
-
   // Init a custom, invisible DebuggerServer, in order to not pollute
   // the debugger with all devtools modules, nor break the debugger itself with using it
   // in the same process.
   let devtools = new DevToolsLoader();
   devtools.invisibleToDebugger = true;
   devtools.main("devtools/server/main");
   let { DebuggerServer, ActorPool } = devtools;
 
@@ -56,11 +49,10 @@ function init(msg) {
 
   let response = {actor: actor.form()};
   mm.sendAsyncMessage("debug:content-process-actor", response);
 
   mm.addMessageListener("debug:content-process-destroy", function onDestroy() {
     mm.removeMessageListener("debug:content-process-destroy", onDestroy);
 
     DebuggerServer.destroy();
-    started = false;
   });
 }