diff options
Diffstat (limited to 'devtools/server/actors/worker')
7 files changed, 801 insertions, 0 deletions
diff --git a/devtools/server/actors/worker/moz.build b/devtools/server/actors/worker/moz.build new file mode 100644 index 0000000000..4c9023879b --- /dev/null +++ b/devtools/server/actors/worker/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + "push-subscription.js", + "service-worker-process.js", + "service-worker-registration-list.js", + "service-worker-registration.js", + "service-worker.js", + "worker-descriptor-actor-list.js", +) diff --git a/devtools/server/actors/worker/push-subscription.js b/devtools/server/actors/worker/push-subscription.js new file mode 100644 index 0000000000..d2fa904383 --- /dev/null +++ b/devtools/server/actors/worker/push-subscription.js @@ -0,0 +1,41 @@ +/* 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 protocol = require("devtools/shared/protocol"); +const { + pushSubscriptionSpec, +} = require("devtools/shared/specs/worker/push-subscription"); + +const PushSubscriptionActor = protocol.ActorClassWithSpec( + pushSubscriptionSpec, + { + initialize(conn, subscription) { + protocol.Actor.prototype.initialize.call(this, conn); + this._subscription = subscription; + }, + + form() { + const subscription = this._subscription; + + // Note: subscription.pushCount & subscription.lastPush are no longer + // returned here because the corresponding getters throw on GeckoView. + // Since they were not used in DevTools they were removed from the + // actor in Bug 1637687. If they are reintroduced, make sure to provide + // meaningful fallback values when debugging a GeckoView runtime. + return { + actor: this.actorID, + endpoint: subscription.endpoint, + quota: subscription.quota, + }; + }, + + destroy() { + this._subscription = null; + protocol.Actor.prototype.destroy.call(this); + }, + } +); +exports.PushSubscriptionActor = PushSubscriptionActor; diff --git a/devtools/server/actors/worker/service-worker-process.js b/devtools/server/actors/worker/service-worker-process.js new file mode 100644 index 0000000000..8d492ab5cd --- /dev/null +++ b/devtools/server/actors/worker/service-worker-process.js @@ -0,0 +1,41 @@ +/* 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/. */ + +/* global addMessageListener */ + +"use strict"; + +/* + * Process script used to control service workers via a DevTools actor. + * Loaded into content processes by the service worker actors. + */ + +const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager +); + +addMessageListener("serviceWorkerRegistration:start", message => { + const { data } = message; + const array = swm.getAllRegistrations(); + + // Find the service worker registration with the desired scope. + for (let i = 0; i < array.length; i++) { + const registration = array.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + // XXX: In some rare cases, `registration.activeWorker` can be null for a + // brief moment (e.g. while the service worker is first installing, or if + // there was an unhandled exception during install that will cause the + // registration to be removed). We can't do much about it here, simply + // ignore these cases. + if (registration.scope === data.scope && registration.activeWorker) { + // Briefly attaching a debugger to the active service worker will cause + // it to start running. + registration.activeWorker.attachDebugger(); + registration.activeWorker.detachDebugger(); + return; + } + } +}); diff --git a/devtools/server/actors/worker/service-worker-registration-list.js b/devtools/server/actors/worker/service-worker-registration-list.js new file mode 100644 index 0000000000..e0b6939a90 --- /dev/null +++ b/devtools/server/actors/worker/service-worker-registration-list.js @@ -0,0 +1,113 @@ +/* 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 { Ci } = require("chrome"); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); +loader.lazyRequireGetter( + this, + "ServiceWorkerRegistrationActor", + "devtools/server/actors/worker/service-worker-registration", + true +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "swm", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +function ServiceWorkerRegistrationActorList(conn) { + this._conn = conn; + this._actors = new Map(); + this._onListChanged = null; + this._mustNotify = false; + this.onRegister = this.onRegister.bind(this); + this.onUnregister = this.onUnregister.bind(this); +} + +ServiceWorkerRegistrationActorList.prototype = { + getList() { + // Create a set of registrations. + const registrations = new Set(); + const array = swm.getAllRegistrations(); + for (let index = 0; index < array.length; ++index) { + registrations.add( + array.queryElementAt(index, Ci.nsIServiceWorkerRegistrationInfo) + ); + } + + // Delete each actor for which we don't have a registration. + for (const [registration] of this._actors) { + if (!registrations.has(registration)) { + this._actors.delete(registration); + } + } + + // Create an actor for each registration for which we don't have one. + for (const registration of registrations) { + if (!this._actors.has(registration)) { + this._actors.set( + registration, + new ServiceWorkerRegistrationActor(this._conn, registration) + ); + } + } + + if (!this._mustNotify) { + if (this._onListChanged !== null) { + swm.addListener(this); + } + this._mustNotify = true; + } + + const actors = []; + for (const [, actor] of this._actors) { + actors.push(actor); + } + + return Promise.resolve(actors); + }, + + 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 (this._mustNotify) { + if (this._onListChanged === null && onListChanged !== null) { + swm.addListener(this); + } + if (this._onListChanged !== null && onListChanged === null) { + swm.removeListener(this); + } + } + this._onListChanged = onListChanged; + }, + + _notifyListChanged() { + this._onListChanged(); + + if (this._onListChanged !== null) { + swm.removeListener(this); + } + this._mustNotify = false; + }, + + onRegister(registration) { + this._notifyListChanged(); + }, + + onUnregister(registration) { + this._notifyListChanged(); + }, +}; + +exports.ServiceWorkerRegistrationActorList = ServiceWorkerRegistrationActorList; diff --git a/devtools/server/actors/worker/service-worker-registration.js b/devtools/server/actors/worker/service-worker-registration.js new file mode 100644 index 0000000000..bb6361f94b --- /dev/null +++ b/devtools/server/actors/worker/service-worker-registration.js @@ -0,0 +1,335 @@ +/* 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 ChromeUtils = require("ChromeUtils"); +const Services = require("Services"); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); +const protocol = require("devtools/shared/protocol"); +const { + serviceWorkerRegistrationSpec, +} = require("devtools/shared/specs/worker/service-worker-registration"); +const { + PushSubscriptionActor, +} = require("devtools/server/actors/worker/push-subscription"); +const { + ServiceWorkerActor, +} = require("devtools/server/actors/worker/service-worker"); + +XPCOMUtils.defineLazyServiceGetter( + this, + "swm", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "PushService", + "@mozilla.org/push/Service;1", + "nsIPushService" +); + +// Lazily load the service-worker-process.js process script only once. +let _serviceWorkerProcessScriptLoaded = false; + +const ServiceWorkerRegistrationActor = protocol.ActorClassWithSpec( + serviceWorkerRegistrationSpec, + { + /** + * Create the ServiceWorkerRegistrationActor + * @param DevToolsServerConnection conn + * The server connection. + * @param ServiceWorkerRegistrationInfo registration + * The registration's information. + */ + initialize(conn, registration) { + protocol.Actor.prototype.initialize.call(this, conn); + this._conn = conn; + this._registration = registration; + this._pushSubscriptionActor = null; + + // A flag to know if preventShutdown has been called and we should + // try to allow the shutdown of the SW when the actor is destroyed + this._preventedShutdown = false; + + this._registration.addListener(this); + + this._createServiceWorkerActors(); + + Services.obs.addObserver(this, PushService.subscriptionModifiedTopic); + }, + + onChange() { + this._destroyServiceWorkerActors(); + this._createServiceWorkerActors(); + this.emit("registration-changed"); + }, + + form() { + const registration = this._registration; + const evaluatingWorker = this._evaluatingWorker.form(); + const installingWorker = this._installingWorker.form(); + const waitingWorker = this._waitingWorker.form(); + const activeWorker = this._activeWorker.form(); + + const newestWorker = + activeWorker || waitingWorker || installingWorker || evaluatingWorker; + + const isMultiE10sWithOldImplementation = + Services.appinfo.browserTabsRemoteAutostart && + !swm.isParentInterceptEnabled(); + return { + actor: this.actorID, + scope: registration.scope, + url: registration.scriptSpec, + evaluatingWorker, + installingWorker, + waitingWorker, + activeWorker, + fetch: newestWorker?.fetch, + // - In old multi e10s: only active registrations are available. + // - In non-e10s or new implementaion: check if we have an active worker + active: isMultiE10sWithOldImplementation ? true : !!activeWorker, + lastUpdateTime: registration.lastUpdateTime, + traits: {}, + }; + }, + + destroy() { + protocol.Actor.prototype.destroy.call(this); + + // Ensure resuming the service worker in case the connection drops + if ( + swm.isParentInterceptEnabled() && + this._registration.activeWorker && + this._preventedShutdown + ) { + this.allowShutdown(); + } + + Services.obs.removeObserver(this, PushService.subscriptionModifiedTopic); + this._registration.removeListener(this); + this._registration = null; + if (this._pushSubscriptionActor) { + this._pushSubscriptionActor.destroy(); + } + this._pushSubscriptionActor = null; + + this._destroyServiceWorkerActors(); + + this._evaluatingWorker = null; + this._installingWorker = null; + this._waitingWorker = null; + this._activeWorker = null; + }, + + /** + * Standard observer interface to listen to push messages and changes. + */ + observe(subject, topic, data) { + const scope = this._registration.scope; + if (data !== scope) { + // This event doesn't concern us, pretend nothing happened. + return; + } + switch (topic) { + case PushService.subscriptionModifiedTopic: + if (this._pushSubscriptionActor) { + this._pushSubscriptionActor.destroy(); + this._pushSubscriptionActor = null; + } + this.emit("push-subscription-modified"); + break; + } + }, + + start() { + if (swm.isParentInterceptEnabled()) { + const { activeWorker } = this._registration; + + // TODO: don't return "started" if there's no active worker. + if (activeWorker) { + // This starts up the Service Worker if it's not already running. + // Note that with parent-intercept (i.e. swm.isParentInterceptEnabled / + // dom.serviceWorkers.parent_intercept=true), the Service Workers exist + // in content processes but are managed from the parent process. This is + // why we call `attachDebugger` here (in the parent process) instead of + // in a process script. + activeWorker.attachDebugger(); + activeWorker.detachDebugger(); + } + + return { type: "started" }; + } + + if (!_serviceWorkerProcessScriptLoaded) { + Services.ppmm.loadProcessScript( + "resource://devtools/server/actors/worker/service-worker-process.js", + true + ); + _serviceWorkerProcessScriptLoaded = true; + } + + // XXX: Send the permissions down to the content process before starting + // the service worker within the content process. As we don't know what + // content process we're starting the service worker in (as we're using a + // broadcast channel to talk to it), we just broadcast the permissions to + // everyone as well. + // + // This call should be replaced with a proper implementation when + // ServiceWorker debugging is improved to support multiple content processes + // correctly. + Services.perms.broadcastPermissionsForPrincipalToAllContentProcesses( + this._registration.principal + ); + + Services.ppmm.broadcastAsyncMessage("serviceWorkerRegistration:start", { + scope: this._registration.scope, + }); + return { type: "started" }; + }, + + unregister() { + const { principal, scope } = this._registration; + const unregisterCallback = { + unregisterSucceeded: function() {}, + unregisterFailed: function() { + console.error("Failed to unregister the service worker for " + scope); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIServiceWorkerUnregisterCallback", + ]), + }; + swm.propagateUnregister(principal, unregisterCallback, scope); + + return { type: "unregistered" }; + }, + + push() { + if (!swm.isParentInterceptEnabled()) { + throw new Error( + "ServiceWorkerRegistrationActor.push can only be used " + + "in parent-intercept mode" + ); + } + + const { principal, scope } = this._registration; + const originAttributes = ChromeUtils.originAttributesToSuffix( + principal.originAttributes + ); + swm.sendPushEvent(originAttributes, scope); + }, + + /** + * Prevent the current active worker to shutdown after the idle timeout. + */ + preventShutdown() { + if (!swm.isParentInterceptEnabled()) { + // In non parent-intercept mode, this is handled by the WorkerDescriptorActor attach(). + throw new Error( + "ServiceWorkerRegistrationActor.preventShutdown can only be used " + + "in parent-intercept mode" + ); + } + + if (!this._registration.activeWorker) { + throw new Error( + "ServiceWorkerRegistrationActor.preventShutdown could not find " + + "activeWorker in parent-intercept mode" + ); + } + + // attachDebugger has to be called from the parent process in parent-intercept mode. + this._registration.activeWorker.attachDebugger(); + this._preventedShutdown = true; + }, + + /** + * Allow the current active worker to shut down again. + */ + allowShutdown() { + if (!swm.isParentInterceptEnabled()) { + // In non parent-intercept mode, this is handled by the WorkerDescriptorActor detach(). + throw new Error( + "ServiceWorkerRegistrationActor.allowShutdown can only be used " + + "in parent-intercept mode" + ); + } + + if (!this._registration.activeWorker) { + throw new Error( + "ServiceWorkerRegistrationActor.allowShutdown could not find " + + "activeWorker in parent-intercept mode" + ); + } + + this._registration.activeWorker.detachDebugger(); + this._preventedShutdown = false; + }, + + getPushSubscription() { + const registration = this._registration; + let pushSubscriptionActor = this._pushSubscriptionActor; + if (pushSubscriptionActor) { + return Promise.resolve(pushSubscriptionActor); + } + return new Promise((resolve, reject) => { + PushService.getSubscription( + registration.scope, + registration.principal, + (result, subscription) => { + if (!subscription) { + resolve(null); + return; + } + pushSubscriptionActor = new PushSubscriptionActor( + this._conn, + subscription + ); + this._pushSubscriptionActor = pushSubscriptionActor; + resolve(pushSubscriptionActor); + } + ); + }); + }, + + _destroyServiceWorkerActors() { + this._evaluatingWorker.destroy(); + this._installingWorker.destroy(); + this._waitingWorker.destroy(); + this._activeWorker.destroy(); + }, + + _createServiceWorkerActors() { + const { + evaluatingWorker, + installingWorker, + waitingWorker, + activeWorker, + } = this._registration; + + this._evaluatingWorker = new ServiceWorkerActor( + this._conn, + evaluatingWorker + ); + this._installingWorker = new ServiceWorkerActor( + this._conn, + installingWorker + ); + this._waitingWorker = new ServiceWorkerActor(this._conn, waitingWorker); + this._activeWorker = new ServiceWorkerActor(this._conn, activeWorker); + + // Add the ServiceWorker actors as children of this ServiceWorkerRegistration actor, + // assigning them valid actorIDs. + this.manage(this._evaluatingWorker); + this.manage(this._installingWorker); + this.manage(this._waitingWorker); + this.manage(this._activeWorker); + }, + } +); + +exports.ServiceWorkerRegistrationActor = ServiceWorkerRegistrationActor; diff --git a/devtools/server/actors/worker/service-worker.js b/devtools/server/actors/worker/service-worker.js new file mode 100644 index 0000000000..d77d5c1e9c --- /dev/null +++ b/devtools/server/actors/worker/service-worker.js @@ -0,0 +1,45 @@ +/* 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 { Ci } = require("chrome"); +const protocol = require("devtools/shared/protocol"); +const { + serviceWorkerSpec, +} = require("devtools/shared/specs/worker/service-worker"); + +const ServiceWorkerActor = protocol.ActorClassWithSpec(serviceWorkerSpec, { + initialize(conn, worker) { + protocol.Actor.prototype.initialize.call(this, conn); + this._worker = worker; + }, + + form() { + if (!this._worker) { + return null; + } + + // handlesFetchEvents is not available if the worker's main script is in the + // evaluating state. + const isEvaluating = + this._worker.state == Ci.nsIServiceWorkerInfo.STATE_PARSED; + const fetch = isEvaluating ? undefined : this._worker.handlesFetchEvents; + + return { + actor: this.actorID, + url: this._worker.scriptSpec, + state: this._worker.state, + fetch, + id: this._worker.id, + }; + }, + + destroy() { + protocol.Actor.prototype.destroy.call(this); + this._worker = null; + }, +}); + +exports.ServiceWorkerActor = ServiceWorkerActor; diff --git a/devtools/server/actors/worker/worker-descriptor-actor-list.js b/devtools/server/actors/worker/worker-descriptor-actor-list.js new file mode 100644 index 0000000000..fa6aa12a75 --- /dev/null +++ b/devtools/server/actors/worker/worker-descriptor-actor-list.js @@ -0,0 +1,212 @@ +/* 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 { Ci } = require("chrome"); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); +loader.lazyRequireGetter( + this, + "WorkerDescriptorActor", + "devtools/server/actors/descriptors/worker", + true +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "wdm", + "@mozilla.org/dom/workers/workerdebuggermanager;1", + "nsIWorkerDebuggerManager" +); + +function matchWorkerDebugger(dbg, options) { + if ("type" in options && dbg.type !== options.type) { + return false; + } + if ("window" in options) { + let window = dbg.window; + while (window !== null && window.parent !== window) { + window = window.parent; + } + + if (window !== options.window) { + return false; + } + } + + return true; +} + +function matchServiceWorker(dbg, origin) { + return ( + dbg.type == Ci.nsIWorkerDebugger.TYPE_SERVICE && + new URL(dbg.url).origin == origin + ); +} + +// When a new worker appears, in some cases (i.e. the debugger is running) we +// want it to pause during registration until a later time (i.e. the debugger +// finishes attaching to the worker). This is an optional WorkderDebuggerManager +// listener that can be installed in addition to the WorkerDescriptorActorList +// listener. It always listens to new workers and pauses any matching filters +// which have been set on it. +// +// Two kinds of filters are supported: +// +// setPauseMatching(true) will pause all workers which match the options strcut +// passed in on creation. +// +// setPauseServiceWorkers(origin) will pause all service workers which have the +// specified origin. +// +// FIXME Bug 1601279 separate WorkerPauser from WorkerDescriptorActorList and give +// it a more consistent interface. +function WorkerPauser(options) { + this._options = options; + this._pauseMatching = null; + this._pauseServiceWorkerOrigin = null; + + this.onRegister = this._onRegister.bind(this); + this.onUnregister = () => {}; + + wdm.addListener(this); +} + +WorkerPauser.prototype = { + destroy() { + wdm.removeListener(this); + }, + + _onRegister(dbg) { + if ( + (this._pauseMatching && matchWorkerDebugger(dbg, this._options)) || + (this._pauseServiceWorkerOrigin && + matchServiceWorker(dbg, this._pauseServiceWorkerOrigin)) + ) { + // Prevent the debuggee from executing in this worker until the debugger + // has finished attaching to it. + dbg.setDebuggerReady(false); + } + }, + + setPauseMatching(shouldPause) { + this._pauseMatching = shouldPause; + }, + + setPauseServiceWorkers(origin) { + this._pauseServiceWorkerOrigin = origin; + }, +}; + +function WorkerDescriptorActorList(conn, options) { + this._conn = conn; + this._options = options; + this._actors = new Map(); + this._onListChanged = null; + this._workerPauser = null; + this._mustNotify = false; + this.onRegister = this.onRegister.bind(this); + this.onUnregister = this.onUnregister.bind(this); +} + +WorkerDescriptorActorList.prototype = { + destroy() { + this.onListChanged = null; + if (this._workerPauser) { + this._workerPauser.destroy(); + this._workerPauser = null; + } + }, + + getList() { + // Create a set of debuggers. + const dbgs = new Set(); + for (const dbg of wdm.getWorkerDebuggerEnumerator()) { + if (matchWorkerDebugger(dbg, this._options)) { + dbgs.add(dbg); + } + } + + // Delete each actor for which we don't have a debugger. + for (const [dbg] of this._actors) { + if (!dbgs.has(dbg)) { + this._actors.delete(dbg); + } + } + + // Create an actor for each debugger for which we don't have one. + for (const dbg of dbgs) { + if (!this._actors.has(dbg)) { + this._actors.set(dbg, new WorkerDescriptorActor(this._conn, dbg)); + } + } + + const actors = []; + for (const [, actor] of this._actors) { + actors.push(actor); + } + + if (!this._mustNotify) { + if (this._onListChanged !== null) { + wdm.addListener(this); + } + this._mustNotify = true; + } + + return Promise.resolve(actors); + }, + + 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); + } + } + this._onListChanged = onListChanged; + }, + + _notifyListChanged() { + this._onListChanged(); + + if (this._onListChanged !== null) { + wdm.removeListener(this); + } + this._mustNotify = false; + }, + + onRegister(dbg) { + if (matchWorkerDebugger(dbg, this._options)) { + this._notifyListChanged(); + } + }, + + onUnregister(dbg) { + if (matchWorkerDebugger(dbg, this._options)) { + this._notifyListChanged(); + } + }, + + get workerPauser() { + if (!this._workerPauser) { + this._workerPauser = new WorkerPauser(this._options); + } + return this._workerPauser; + }, +}; + +exports.WorkerDescriptorActorList = WorkerDescriptorActorList; |