From ca5c1dfc8d252dd90e07f262e6cffabf4056ebc0 Mon Sep 17 00:00:00 2001 From: Eddy Bruel Date: Fri, 13 Nov 2015 10:35:50 +0100 Subject: [PATCH] Bug 1221892 - Extend the debugger protocol to get the matching service worker registration;r=janx --- .../debugger/test/mochitest/browser.ini | 3 + ...rowser_dbg_getserviceworkerregistration.js | 73 +++++++++ ...ode_getserviceworkerregistration-worker.js | 1 + .../doc_getserviceworkerregistration-tab.html | 10 ++ .../client/debugger/test/mochitest/head.js | 19 +++ devtools/server/actors/webbrowser.js | 142 +++++++++++++++++- devtools/server/actors/worker.js | 21 +++ devtools/shared/client/main.js | 9 +- toolkit/components/telemetry/Histograms.json | 18 +++ 9 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 devtools/client/debugger/test/mochitest/browser_dbg_getserviceworkerregistration.js create mode 100644 devtools/client/debugger/test/mochitest/code_getserviceworkerregistration-worker.js create mode 100644 devtools/client/debugger/test/mochitest/getserviceworkerregistration/doc_getserviceworkerregistration-tab.html diff --git a/devtools/client/debugger/test/mochitest/browser.ini b/devtools/client/debugger/test/mochitest/browser.ini index 267de1c9a572..53294c2b6e29 100644 --- a/devtools/client/debugger/test/mochitest/browser.ini +++ b/devtools/client/debugger/test/mochitest/browser.ini @@ -25,6 +25,7 @@ support-files = 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 @@ -112,6 +113,7 @@ support-files = 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 @@ -219,6 +221,7 @@ skip-if = e10s && debug 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] diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_getserviceworkerregistration.js b/devtools/client/debugger/test/mochitest/browser_dbg_getserviceworkerregistration.js new file mode 100644 index 000000000000..31335e9ad301 --- /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(); + }); + }); +} diff --git a/devtools/client/debugger/test/mochitest/code_getserviceworkerregistration-worker.js b/devtools/client/debugger/test/mochitest/code_getserviceworkerregistration-worker.js new file mode 100644 index 000000000000..3918c74e4463 --- /dev/null +++ b/devtools/client/debugger/test/mochitest/code_getserviceworkerregistration-worker.js @@ -0,0 +1 @@ +"use strict"; diff --git a/devtools/client/debugger/test/mochitest/getserviceworkerregistration/doc_getserviceworkerregistration-tab.html b/devtools/client/debugger/test/mochitest/getserviceworkerregistration/doc_getserviceworkerregistration-tab.html new file mode 100644 index 000000000000..9f48883a5561 --- /dev/null +++ b/devtools/client/debugger/test/mochitest/getserviceworkerregistration/doc_getserviceworkerregistration-tab.html @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/devtools/client/debugger/test/mochitest/head.js b/devtools/client/debugger/test/mochitest/head.js index 6758138edbf7..2c2713629cf5 100644 --- a/devtools/client/debugger/test/mochitest/head.js +++ b/devtools/client/debugger/test/mochitest/head.js @@ -1066,6 +1066,15 @@ function listWorkers(tabClient) { }); } +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) { @@ -1095,6 +1104,16 @@ function waitForWorkerListChanged(tabClient) { }); } +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) { diff --git a/devtools/server/actors/webbrowser.js b/devtools/server/actors/webbrowser.js index f3c9dde3b9e0..136cecce3f90 100644 --- a/devtools/server/actors/webbrowser.js +++ b/devtools/server/actors/webbrowser.js @@ -18,11 +18,18 @@ 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: @@ -112,6 +119,34 @@ function sendShutdownEvent() { 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: @@ -739,6 +774,9 @@ function TabActor(aConnection) 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 @@ -1121,6 +1159,107 @@ TabActor.prototype = { 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. @@ -1840,7 +1979,8 @@ TabActor.prototype.requestTypes = { "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; diff --git a/devtools/server/actors/worker.js b/devtools/server/actors/worker.js index 05b339ab292d..17578f718687 100644 --- a/devtools/server/actors/worker.js +++ b/devtools/server/actors/worker.js @@ -220,3 +220,24 @@ WorkerActorList.prototype = { }; 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; diff --git a/devtools/shared/client/main.js b/devtools/shared/client/main.js index 420777f55fc2..7c59ea96e1fd 100644 --- a/devtools/shared/client/main.js +++ b/devtools/shared/client/main.js @@ -160,6 +160,7 @@ const UnsolicitedNotifications = { "reflowActivity": "reflowActivity", "addonListChanged": "addonListChanged", "workerListChanged": "workerListChanged", + "serviceWorkerRegistrationChanged": "serviceWorkerRegistrationChanged", "tabNavigated": "tabNavigated", "frameUpdate": "frameUpdate", "pageError": "pageError", @@ -1226,7 +1227,7 @@ function TabClient(aClient, aForm) { this.thread = null; this.request = this.client.request; this.traits = aForm.traits || {}; - this.events = ["workerListChanged"]; + this.events = ["workerListChanged", "serviceWorkerRegistrationChanged"]; } TabClient.prototype = { @@ -1336,6 +1337,12 @@ TabClient.prototype = { telemetry: "LISTWORKERS" }), + getServiceWorkerRegistration: DebuggerClient.requester({ + type: "getServiceWorkerRegistration", + }, { + telemetry: "GETSERVICEWORKERREGISTRATION" + }), + attachWorker: function (aWorkerActor, aOnResponse) { this.client.attachWorker(aWorkerActor, aOnResponse); } diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index e61b9dfc4747..8a802d2732cb 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -6133,6 +6133,24 @@ "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",