diff options
Diffstat (limited to 'devtools/server/actors/worker')
6 files changed, 686 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..84e606db58 --- /dev/null +++ b/devtools/server/actors/worker/moz.build @@ -0,0 +1,13 @@ +# -*- 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-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..37e6be7fb4 --- /dev/null +++ b/devtools/server/actors/worker/push-subscription.js @@ -0,0 +1,38 @@ +/* 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 { Actor } = require("resource://devtools/shared/protocol.js"); +const { + pushSubscriptionSpec, +} = require("resource://devtools/shared/specs/worker/push-subscription.js"); + +class PushSubscriptionActor extends Actor { + constructor(conn, subscription) { + super(conn, pushSubscriptionSpec); + 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; + super.destroy(); + } +} +exports.PushSubscriptionActor = PushSubscriptionActor; 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..9821108faf --- /dev/null +++ b/devtools/server/actors/worker/service-worker-registration-list.js @@ -0,0 +1,114 @@ +/* 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 { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +loader.lazyRequireGetter( + this, + "ServiceWorkerRegistrationActor", + "resource://devtools/server/actors/worker/service-worker-registration.js", + true +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "swm", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +class ServiceWorkerRegistrationActorList { + constructor(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); + } + + 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..1e5e80ae8b --- /dev/null +++ b/devtools/server/actors/worker/service-worker-registration.js @@ -0,0 +1,264 @@ +/* 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 { Actor } = require("resource://devtools/shared/protocol.js"); +const { + serviceWorkerRegistrationSpec, +} = require("resource://devtools/shared/specs/worker/service-worker-registration.js"); + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { + PushSubscriptionActor, +} = require("resource://devtools/server/actors/worker/push-subscription.js"); +const { + ServiceWorkerActor, +} = require("resource://devtools/server/actors/worker/service-worker.js"); + +XPCOMUtils.defineLazyServiceGetter( + this, + "swm", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "PushService", + "@mozilla.org/push/Service;1", + "nsIPushService" +); + +class ServiceWorkerRegistrationActor extends Actor { + /** + * Create the ServiceWorkerRegistrationActor + * @param DevToolsServerConnection conn + * The server connection. + * @param ServiceWorkerRegistrationInfo registration + * The registration's information. + */ + constructor(conn, registration) { + super(conn, serviceWorkerRegistrationSpec); + 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; + + return { + actor: this.actorID, + scope: registration.scope, + url: registration.scriptSpec, + evaluatingWorker, + installingWorker, + waitingWorker, + activeWorker, + fetch: newestWorker?.fetch, + // Check if we have an active worker + active: !!activeWorker, + lastUpdateTime: registration.lastUpdateTime, + traits: {}, + }; + } + + destroy() { + super.destroy(); + + // Ensure resuming the service worker in case the connection drops + if (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() { + 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 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" }; + } + + unregister() { + const { principal, scope } = this._registration; + const unregisterCallback = { + unregisterSucceeded() {}, + unregisterFailed() { + console.error("Failed to unregister the service worker for " + scope); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIServiceWorkerUnregisterCallback", + ]), + }; + swm.propagateUnregister(principal, unregisterCallback, scope); + + return { type: "unregistered" }; + } + + push() { + 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 (!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 (!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..9185e73e17 --- /dev/null +++ b/devtools/server/actors/worker/service-worker.js @@ -0,0 +1,44 @@ +/* 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 { Actor } = require("resource://devtools/shared/protocol.js"); +const { + serviceWorkerSpec, +} = require("resource://devtools/shared/specs/worker/service-worker.js"); + +class ServiceWorkerActor extends Actor { + constructor(conn, worker) { + super(conn, serviceWorkerSpec); + 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() { + super.destroy(); + 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..10bdb5d5d3 --- /dev/null +++ b/devtools/server/actors/worker/worker-descriptor-actor-list.js @@ -0,0 +1,213 @@ +/* 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 { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +loader.lazyRequireGetter( + this, + "WorkerDescriptorActor", + "resource://devtools/server/actors/descriptors/worker.js", + 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. +class WorkerPauser { + constructor(options) { + this._options = options; + this._pauseMatching = null; + this._pauseServiceWorkerOrigin = null; + + this.onRegister = this._onRegister.bind(this); + this.onUnregister = () => {}; + + wdm.addListener(this); + } + + 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; + } +} + +class WorkerDescriptorActorList { + constructor(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); + } + + 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; |