Bug 1221892 - Extend the debugger protocol to get the matching service worker registration;r=janx
☠☠ backed out by 4a1f206dfc25 ☠ ☠
authorEddy Bruel <ejpbruel@mozilla.com>
Fri, 13 Nov 2015 10:35:50 +0100
changeset 308704 d6d640c4b8c2097f3a780f6420c197dc698a8183
parent 308703 3da6ad8bb8a99becd01f23da263d0e7496c3d5e6
child 308705 9e64e9666b4062358e4c81deb3b775b7cb18e84b
child 308792 a5e97e258a47c5401626d8f315584a8e9df629e9
child 308808 92e3b44611e0248f1ddbd98fdb59d5c844500884
push id7514
push users.kaspari@gmail.com
push dateFri, 13 Nov 2015 14:12:41 +0000
reviewersjanx
bugs1221892
milestone45.0a1
Bug 1221892 - Extend the debugger protocol to get the matching service worker registration;r=janx
devtools/client/debugger/test/mochitest/browser.ini
devtools/client/debugger/test/mochitest/browser_dbg_getserviceworkerregistration.js
devtools/client/debugger/test/mochitest/code_getserviceworkerregistration-worker.js
devtools/client/debugger/test/mochitest/getserviceworkerregistration/doc_getserviceworkerregistration-tab.html
devtools/client/debugger/test/mochitest/head.js
devtools/server/actors/webbrowser.js
devtools/server/actors/worker.js
devtools/shared/client/main.js
toolkit/components/telemetry/Histograms.json
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -20,16 +20,17 @@ support-files =
   code_frame-script.js
   code_function-jump-01.js
   code_function-search-01.js
   code_function-search-02.js
   code_function-search-03.js
   code_location-changes.js
   code_listworkers-worker1.js
   code_listworkers-worker2.js
+  code_getserviceworkerregistration-worker.js
   code_math.js
   code_math.map
   code_math.min.js
   code_math_bogus_map.js
   code_same-line-functions.js
   code_script-eval.js
   code_script-switching-01.js
   code_script-switching-02.js
@@ -107,16 +108,17 @@ support-files =
   doc_step-out.html
   doc_terminate-on-tab-close.html
   doc_watch-expressions.html
   doc_watch-expression-button.html
   doc_with-frame.html
   doc_WorkerActor.attach-tab1.html
   doc_WorkerActor.attach-tab2.html
   doc_WorkerActor.attachThread-tab.html
+  getserviceworkerregistration/doc_getserviceworkerregistration-tab.html
   head.js
   sjs_random-javascript.sjs
   testactors.js
 
 [browser_dbg_aaa_run_first_leaktest.js]
 skip-if = e10s && debug
 [browser_dbg_addonactor.js]
 tags = addons
@@ -214,16 +216,17 @@ skip-if = e10s && debug
 [browser_dbg_conditional-breakpoints-04.js]
 skip-if = e10s && debug
 [browser_dbg_conditional-breakpoints-05.js]
 skip-if = e10s && debug
 [browser_dbg_console-eval.js]
 skip-if = e10s && debug
 [browser_dbg_console-named-eval.js]
 skip-if = e10s && debug
+[browser_dbg_getserviceworkerregistration.js]
 [browser_dbg_server-conditional-bp-01.js]
 skip-if = e10s && debug
 [browser_dbg_server-conditional-bp-02.js]
 skip-if = e10s && debug
 [browser_dbg_server-conditional-bp-03.js]
 skip-if = e10s && debug
 [browser_dbg_server-conditional-bp-04.js]
 skip-if = e10s && debug
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_getserviceworkerregistration.js
@@ -0,0 +1,73 @@
+var TAB_URL = EXAMPLE_URL + "getserviceworkerregistration/doc_getserviceworkerregistration-tab.html";
+var WORKER_URL = "../code_getserviceworkerregistration-worker.js";
+var SCOPE1_URL = EXAMPLE_URL;
+var SCOPE2_URL = EXAMPLE_URL + "getserviceworkerregistration/";
+
+function test() {
+  SpecialPowers.pushPrefEnv({'set': [
+    ["dom.serviceWorkers.enabled", true],
+    ["dom.serviceWorkers.testing.enabled", true],
+  ]}, function () {
+    Task.spawn(function* () {
+      DebuggerServer.init();
+      DebuggerServer.addBrowserActors();
+
+      let client = new DebuggerClient(DebuggerServer.connectPipe());
+      yield connect(client);
+
+      let tab = yield addTab(TAB_URL);
+      let { tabs } = yield listTabs(client);
+      let [, tabClient] = yield attachTab(client, findTab(tabs, TAB_URL));
+
+      info("Check that the getting service worker registration is initially " +
+           "empty.");
+      let { registration } = yield getServiceWorkerRegistration(tabClient);
+      is(registration, null);
+
+      info("Register a service worker in the same scope as the page, and " +
+           "check that it becomes the current service worker registration.");
+      executeSoon(() => {
+        evalInTab(tab, "promise1 = navigator.serviceWorker.register('" +
+                        WORKER_URL + "', { scope: '" + SCOPE1_URL + "' });");
+      });
+      yield waitForServiceWorkerRegistrationChanged(tabClient);
+      ({ registration } = yield getServiceWorkerRegistration(tabClient));
+      is(registration.scope, SCOPE1_URL);
+
+      info("Register a second service worker with a more specific scope, and " +
+           "check that it becomes the current service worker registration.");
+      executeSoon(() => {
+        evalInTab(tab, "promise2 = promise1.then(function () { " +
+                       "return navigator.serviceWorker.register('" +
+                       WORKER_URL + "', { scope: '" + SCOPE2_URL + "' }); });");
+      });
+      yield waitForServiceWorkerRegistrationChanged(tabClient);
+      ({ registration } = yield getServiceWorkerRegistration(tabClient));
+      is(registration.scope, SCOPE2_URL);
+
+      info("Unregister the second service worker, and check that the " +
+           "first service worker becomes the current service worker " +
+           "registration again.");
+      executeSoon(() => {
+        evalInTab(tab, "promise2.then(function (registration) { " +
+                       "registration.unregister(); });")
+      });
+      yield waitForServiceWorkerRegistrationChanged(tabClient);
+      ({ registration } = yield getServiceWorkerRegistration(tabClient));
+      is(registration.scope, SCOPE1_URL);
+
+      info("Unregister the first service worker, and check that the current " +
+           "service worker registration becomes empty again.");
+      executeSoon(() => {
+        evalInTab(tab, "promise1.then(function (registration) { " +
+                       "registration.unregister(); });");
+      });
+      yield waitForServiceWorkerRegistrationChanged(tabClient);
+      ({ registration } = yield getServiceWorkerRegistration(tabClient));
+      is(registration, null);
+
+      yield close(client);
+      finish();
+    });
+  });
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/code_getserviceworkerregistration-worker.js
@@ -0,0 +1,1 @@
+"use strict";
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/getserviceworkerregistration/doc_getserviceworkerregistration-tab.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8"/>
+  </head>
+  <body>
+    <!-- This page is deliberately in a subdirectory, so we can register a
+         service worker which scope is less specific than the page itself. -->
+  </body>
+</html>
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -1061,16 +1061,25 @@ function listWorkers(tabClient) {
   info("Listing workers.");
   return new Promise(function (resolve) {
     tabClient.listWorkers(function (response) {
       resolve(response);
     });
   });
 }
 
+function getServiceWorkerRegistration(tabClient) {
+  info("Getting current service worker registration.");
+  return new Promise(function (resolve) {
+    tabClient.getServiceWorkerRegistration(function (response) {
+      resolve(response);
+    });
+  });
+}
+
 function findWorker(workers, url) {
   info("Finding worker with url '" + url + "'.");
   for (let worker of workers) {
     if (worker.url === url) {
       return worker;
     }
   }
   return null;
@@ -1090,16 +1099,26 @@ function waitForWorkerListChanged(tabCli
   return new Promise(function (resolve) {
     tabClient.addListener("workerListChanged", function listener() {
       tabClient.removeListener("workerListChanged", listener);
       resolve();
     });
   });
 }
 
+function waitForServiceWorkerRegistrationChanged(tabClient) {
+  info("Waiting for current service worker registration to change.");
+  return new Promise(function (resolve) {
+    tabClient.addListener("serviceWorkerRegistrationChanged", function listener() {
+      tabClient.removeListener("serviceWorkerRegistrationChanged", listener);
+      resolve();
+    });
+  });
+}
+
 function attachThread(workerClient, options) {
   info("Attaching to thread.");
   return new Promise(function(resolve, reject) {
     workerClient.attachThread(options, function (response, threadClient) {
       resolve([response, threadClient]);
     });
   });
 }
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -13,21 +13,28 @@ var { ActorPool, createExtraActors, appe
 var { DebuggerServer } = require("devtools/server/main");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 var { assert, dbg_assert } = DevToolsUtils;
 var { TabSources } = require("./utils/TabSources");
 var makeDebugger = require("./utils/make-debugger");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(
+  this, "swm",
+  "@mozilla.org/serviceworkers/manager;1",
+  "nsIServiceWorkerManager"
+);
+
 loader.lazyRequireGetter(this, "RootActor", "devtools/server/actors/root", true);
 loader.lazyRequireGetter(this, "ThreadActor", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "BrowserAddonActor", "devtools/server/actors/addon", true);
 loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker", true);
+loader.lazyRequireGetter(this, "ServiceWorkerRegistrationActor", "devtools/server/actors/worker", true);
 loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
 
 // Assumptions on events module:
 // events needs to be dispatched synchronously,
 // by calling the listeners in the order or registration.
 loader.lazyRequireGetter(this, "events", "sdk/event/core");
 
 loader.lazyRequireGetter(this, "StyleSheetActor", "devtools/server/actors/stylesheets", true);
@@ -108,16 +115,44 @@ function sendShutdownEvent() {
     evt.initEvent("Debugger:Shutdown", true, false);
     win.document.documentElement.dispatchEvent(evt);
   }
 }
 
 exports.sendShutdownEvent = sendShutdownEvent;
 
 /**
+ * Returns the service worker registration that matches the given client URL, or
+ * null if no such registration exists.
+ *
+ * The service worker specification defines the service worker registration that
+ * matches a given client URL as the registration which scope is the longest
+ * prefix of the client URL (see section 9.15 for details).
+ */
+function matchServiceWorkerRegistration(clientURL) {
+  let matchingRegistration = null;
+
+  let array = swm.getAllRegistrations();
+  for (let index = 0; index < array.length; ++index) {
+    let registration =
+      array.queryElementAt(index, Ci.nsIServiceWorkerRegistrationInfo);
+    if (clientURL.indexOf(registration.scope) !== 0) {
+      continue;
+    }
+
+    if (matchingRegistration === null ||
+        matchingRegistration.scope.length < registration.scope.length) {
+      matchingRegistration = registration;
+    }
+  }
+
+  return matchingRegistration;
+}
+
+/**
  * Construct a root actor appropriate for use in a server running in a
  * browser. The returned root actor:
  * - respects the factories registered with DebuggerServer.addGlobalActor,
  * - uses a BrowserTabList to supply tab actors,
  * - sends all navigator:browser window documents a Debugger:Shutdown event
  *   when it exits.
  *
  * * @param aConnection DebuggerServerConnection
@@ -734,16 +769,19 @@ function TabActor(aConnection)
     // Do not require to send reconfigure request to reset the document state
     // to what it was before using the TabActor
     noTabReconfigureOnClose: true
   };
 
   this._workerActorList = null;
   this._workerActorPool = null;
   this._onWorkerActorListChanged = this._onWorkerActorListChanged.bind(this);
+
+  this._serviceWorkerRegistrationActor = null;
+  this._mustNotifyServiceWorkerRegistrationChanged = false;
 }
 
 // XXX (bug 710213): TabActor attach/detach/exit/disconnect is a
 // *complete* mess, needs to be rethought asap.
 
 TabActor.prototype = {
   traits: null,
 
@@ -1116,16 +1154,117 @@ TabActor.prototype = {
     });
   },
 
   _onWorkerActorListChanged: function () {
     this._workerActorList.onListChanged = null;
     this.conn.sendActorEvent(this.actorID, "workerListChanged");
   },
 
+  /**
+   * Gets the current service worker registration for this tab. The current
+   * service worker registration is the registration which scope URL forms the
+   * longest prefix of the URL of the current page in the tab (see
+   * matchServiceWorkerRegistration for details).
+   *
+   * This request works similar to a live list, in the sense that it will send
+   * a one-shot notification when the current service worker registration
+   * changes, provided the client has sent this request at least once since the
+   * last notification was sent.
+   */
+  onGetServiceWorkerRegistration: function () {
+    // The actor for the current service worker registration. This will
+    // initially be set to null. Whenever possible, we will reuse the actor for
+    // the previous service worker registration. Otherwise, a new actor for the
+    // current service worker registration will be created, provided such a
+    // registration exists.
+    let actor = null;
+
+    // Get the current service worker registration and compare it against the
+    // previous service worker registration.
+    //
+    // Note that we can obtain the previous service worker registration from the
+    // actor for the previous service worker registration. If no such actor
+    // exists, the previous service registration was null.
+    let registration = matchServiceWorkerRegistration(this.url);
+    if ((this._serviceWorkerRegistrationActor === null &&
+         registration === null) ||
+        (this._serviceWorkerRegistrationActor !== null &&
+         this._serviceWorkerRegistrationActor.registration === registration)) {
+      // The previous service worker registration equals the current service
+      // worker registration, so reuse the actor for the previous service
+      // worker registration.
+      actor = this._serviceWorkerRegistrationActor;
+    }
+    else {
+      // The previous service worker registration does not equal the current
+      // service worker registration, so remove the actor for the previous
+      // service worker registration from the tab actor pool (this will cause
+      // the actor to be destroyed).
+      if (this._serviceWorkerRegistrationActor) {
+        this._tabPool.removeActor(this._serviceWorkerRegistrationActor);
+      }
+
+      // If there is no service worker registration that matches the URL of the
+      // page in the tab, the current service worker registration will be null.
+      // In that case, no actor should be created.
+      if (registration !== null) {
+        // Create a new actor for the current service worker registration, and
+        // add it to the tab actor pool. We use the tab actor pool because we
+        // don't want the actor to persist when we detach from the tab.
+        actor = new ServiceWorkerRegistrationActor(registration);
+        this._tabPool.addActor(actor);
+      }
+
+      // Cache the actor for the current service worker registration. On
+      // subsequent requests, this will become the actor for the previous
+      // service worker registration.
+      this._serviceWorkerRegistrationActor = actor;
+    }
+
+    // Make sure we send a one-shot notification when the current service worker
+    // registration changes.
+    if (!this._mustNotifyServiceWorkerRegistrationChanged) {
+      swm.addListener(this);
+      this._mustNotifyServiceWorkerRegistrationChanged = true;
+    }
+
+    // Return the actor for the current service worker registration, or null if
+    // no such registration exists.
+    return {
+      "registration": actor !== null ? actor.form() : null
+    }
+  },
+
+  _notifyServiceWorkerRegistrationChanged: function () {
+    this.conn.sendActorEvent(this.actorID, "serviceWorkerRegistrationChanged");
+    swm.removeListener(this);
+    this._mustNotifyServiceWorkerRegistrationChanged = false;
+  },
+
+  onRegister: function () {
+    let registration = matchServiceWorkerRegistration(this.url);
+    if ((this._serviceWorkerRegistrationActor === null &&
+         registration !== null) ||
+        (this._serviceWorkerRegistrationActor !== null &&
+         this._serviceWorkerRegistrationActor.registration !== registration)) {
+      this._notifyServiceWorkerRegistrationChanged();
+    }
+  },
+
+  onUnregister: function () {
+    let registration = matchServiceWorkerRegistration(this.url);
+    if ((this._serviceWorkerRegistrationActor === null &&
+         registration !== null) ||
+        (this._serviceWorkerRegistrationActor !== null &&
+         this._serviceWorkerRegistrationActor.registration !== registration)) {
+      this._notifyServiceWorkerRegistrationChanged();
+    }
+  },
+
   observe: function (aSubject, aTopic, aData) {
     // Ignore any event that comes before/after the tab actor is attached
     // That typically happens during firefox shutdown.
     if (!this.attached) {
       return;
     }
     if (aTopic == "webnavigation-create") {
       aSubject.QueryInterface(Ci.nsIDocShell);
@@ -1835,17 +1974,18 @@ TabActor.prototype = {
 TabActor.prototype.requestTypes = {
   "attach": TabActor.prototype.onAttach,
   "detach": TabActor.prototype.onDetach,
   "reload": TabActor.prototype.onReload,
   "navigateTo": TabActor.prototype.onNavigateTo,
   "reconfigure": TabActor.prototype.onReconfigure,
   "switchToFrame": TabActor.prototype.onSwitchToFrame,
   "listFrames": TabActor.prototype.onListFrames,
-  "listWorkers": TabActor.prototype.onListWorkers
+  "listWorkers": TabActor.prototype.onListWorkers,
+  "getServiceWorkerRegistration": TabActor.prototype.onGetServiceWorkerRegistration
 };
 
 exports.TabActor = TabActor;
 
 /**
  * Creates a tab actor for handling requests to a single in-process
  * <browser> tab. Most of the implementation comes from TabActor.
  *
--- a/devtools/server/actors/worker.js
+++ b/devtools/server/actors/worker.js
@@ -215,8 +215,29 @@ WorkerActorList.prototype = {
   onUnregister: function (dbg) {
     if (matchWorkerDebugger(dbg, this._options)) {
       this._notifyListChanged();
     }
   }
 };
 
 exports.WorkerActorList = WorkerActorList;
+
+function ServiceWorkerRegistrationActor(registration) {
+  this._registration = registration;
+}
+
+ServiceWorkerRegistrationActor.prototype = {
+  get registration() {
+    return this._registration;
+  },
+
+  actorPrefix: "serviceWorkerRegistration",
+
+  form: function () {
+    return {
+      actor: this.actorID,
+      scope: this._registration.scope
+    };
+  }
+};
+
+exports.ServiceWorkerRegistrationActor = ServiceWorkerRegistrationActor;
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -155,16 +155,17 @@ const UnsolicitedNotifications = {
   "networkEventUpdate": "networkEventUpdate",
   "newGlobal": "newGlobal",
   "newScript": "newScript",
   "tabDetached": "tabDetached",
   "tabListChanged": "tabListChanged",
   "reflowActivity": "reflowActivity",
   "addonListChanged": "addonListChanged",
   "workerListChanged": "workerListChanged",
+  "serviceWorkerRegistrationChanged": "serviceWorkerRegistrationChanged",
   "tabNavigated": "tabNavigated",
   "frameUpdate": "frameUpdate",
   "pageError": "pageError",
   "documentLoad": "documentLoad",
   "enteredFrame": "enteredFrame",
   "exitedFrame": "exitedFrame",
   "appOpen": "appOpen",
   "appClose": "appClose",
@@ -1221,17 +1222,17 @@ function TabClient(aClient, aForm) {
   this.client = aClient;
   this._actor = aForm.from;
   this._threadActor = aForm.threadActor;
   this.javascriptEnabled = aForm.javascriptEnabled;
   this.cacheDisabled = aForm.cacheDisabled;
   this.thread = null;
   this.request = this.client.request;
   this.traits = aForm.traits || {};
-  this.events = ["workerListChanged"];
+  this.events = ["workerListChanged", "serviceWorkerRegistrationChanged"];
 }
 
 TabClient.prototype = {
   get actor() { return this._actor },
   get _transport() { return this.client._transport; },
 
   /**
    * Attach to a thread actor.
@@ -1331,16 +1332,22 @@ TabClient.prototype = {
   }),
 
   listWorkers: DebuggerClient.requester({
     type: "listWorkers"
   }, {
     telemetry: "LISTWORKERS"
   }),
 
+  getServiceWorkerRegistration: DebuggerClient.requester({
+    type: "getServiceWorkerRegistration",
+  }, {
+    telemetry: "GETSERVICEWORKERREGISTRATION"
+  }),
+
   attachWorker: function (aWorkerActor, aOnResponse) {
     this.client.attachWorker(aWorkerActor, aOnResponse);
   }
 };
 
 eventSource(TabClient.prototype);
 
 function WorkerClient(aClient, aForm) {
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6128,16 +6128,34 @@
   "DEVTOOLS_DEBUGGER_RDP_REMOTE_LISTWORKERS_MS": {
     "alert_emails": ["dev-developer-tools@lists.mozilla.org", "jan@mozilla.com"],
     "expires_in_version": "55",
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "50",
     "description": "The time (in milliseconds) that it took a 'listWorkers' request to go round trip."
   },
+  "DEVTOOLS_DEBUGGER_RDP_LOCAL_GETSERVICEWORKERREGISTRATION_MS": {
+    "alert_emails": ["dev-developer-tools@lists.mozilla.org", "ejpbruel@mozilla.com"],
+    "expires_in_version": "50",
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "50",
+    "bug_numbers": [1221892],
+    "description": "The time (in milliseconds) that it took a 'getServiceWorkerRegistration' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_REMOTE_GETSERVICEWORKERREGISTRATION_MS": {
+    "alert_emails": ["dev-developer-tools@lists.mozilla.org", "ejpbruel@mozilla.com"],
+    "expires_in_version": "50",
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "50",
+    "bug_numbers": [1221892],
+    "description": "The time (in milliseconds) that it took a 'getServiceWorkerRegistration' request to go round trip."
+  },
   "DEVTOOLS_DEBUGGER_RDP_LOCAL_LISTPROCESSES_MS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "1000",
     "description": "The time (in milliseconds) that it took a 'listProcesses' request to go round trip."
   },
   "DEVTOOLS_DEBUGGER_RDP_REMOTE_LISTPROCESSES_MS": {