summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js')
-rw-r--r--devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js320
1 files changed, 320 insertions, 0 deletions
diff --git a/devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js b/devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js
new file mode 100644
index 0000000000..bc0ce2b76d
--- /dev/null
+++ b/devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js
@@ -0,0 +1,320 @@
+/* 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 {
+ WorkersListener,
+ // eslint-disable-next-line mozilla/reject-some-requires
+} = require("resource://devtools/client/shared/workers-listener.js");
+
+const LegacyWorkersWatcher = require("resource://devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js");
+
+class LegacyServiceWorkersWatcher extends LegacyWorkersWatcher {
+ // Holds the current target URL object
+ #currentTargetURL;
+
+ constructor(targetCommand, onTargetAvailable, onTargetDestroyed, commands) {
+ super(targetCommand, onTargetAvailable, onTargetDestroyed);
+ this._registrations = [];
+ this._processTargets = new Set();
+ this.commands = commands;
+
+ // We need to listen for registration changes at least in order to properly
+ // filter service workers by domain when debugging a local tab.
+ //
+ // A WorkerTarget instance has a url property, but it points to the url of
+ // the script, whereas the url property of the ServiceWorkerRegistration
+ // points to the URL controlled by the service worker.
+ //
+ // Historically we have been matching the service worker registration URL
+ // to match service workers for local tab tools (app panel & debugger).
+ // Maybe here we could have some more info on the actual worker.
+ this._workersListener = new WorkersListener(this.rootFront, {
+ registrationsOnly: true,
+ });
+
+ // Note that this is called much more often than when a registration
+ // is created or destroyed. WorkersListener notifies of anything that
+ // potentially impacted workers.
+ // I use it as a shortcut in this first patch. Listening to rootFront's
+ // "serviceWorkerRegistrationListChanged" should be enough to be notified
+ // about registrations. And if we need to also update the
+ // "debuggerServiceWorkerStatus" from here, then we would have to
+ // also listen to "registration-changed" one each registration.
+ this._onRegistrationListChanged = this._onRegistrationListChanged.bind(
+ this
+ );
+ this._onDocumentEvent = this._onDocumentEvent.bind(this);
+
+ // Flag used from the parent class to listen to process targets.
+ // Decision tree is complicated, keep all logic in the parent methods.
+ this._isServiceWorkerWatcher = true;
+ }
+
+ /**
+ * Override from LegacyWorkersWatcher.
+ *
+ * We record all valid service worker targets (ie workers that match a service
+ * worker registration), but we will only notify about the ones which match
+ * the current domain.
+ */
+ _recordWorkerTarget(workerTarget) {
+ return !!this._getRegistrationForWorkerTarget(workerTarget);
+ }
+
+ // Override from LegacyWorkersWatcher.
+ _supportWorkerTarget(workerTarget) {
+ if (!workerTarget.isServiceWorker) {
+ return false;
+ }
+
+ const registration = this._getRegistrationForWorkerTarget(workerTarget);
+ return registration && this._isRegistrationValidForTarget(registration);
+ }
+
+ // Override from LegacyWorkersWatcher.
+ async listen() {
+ // Listen to the current target front.
+ this.target = this.targetCommand.targetFront;
+
+ if (this.targetCommand.descriptorFront.isTabDescriptor) {
+ this.#currentTargetURL = new URL(this.targetCommand.targetFront.url);
+ }
+
+ this._workersListener.addListener(this._onRegistrationListChanged);
+
+ // Fetch the registrations before calling listen, since service workers
+ // might already be available and will need to be compared with the existing
+ // registrations.
+ await this._onRegistrationListChanged();
+
+ if (this.targetCommand.descriptorFront.isTabDescriptor) {
+ await this.commands.resourceCommand.watchResources(
+ [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this._onDocumentEvent,
+ ignoreExistingResources: true,
+ }
+ );
+ }
+
+ await super.listen();
+ }
+
+ // Override from LegacyWorkersWatcher.
+ unlisten(...args) {
+ this._workersListener.removeListener(this._onRegistrationListChanged);
+
+ if (this.targetCommand.descriptorFront.isTabDescriptor) {
+ this.commands.resourceCommand.unwatchResources(
+ [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this._onDocumentEvent,
+ }
+ );
+ }
+
+ super.unlisten(...args);
+ }
+
+ // Override from LegacyWorkersWatcher.
+ async _onProcessAvailable({ targetFront }) {
+ if (this.targetCommand.descriptorFront.isTabDescriptor) {
+ // XXX: This has been ported straight from the current debugger
+ // implementation. Since pauseMatchingServiceWorkers expects an origin
+ // to filter matching workers, it only makes sense when we are debugging
+ // a tab. However in theory, parent process debugging could pause all
+ // service workers without matching anything.
+ try {
+ // To support early breakpoint we need to setup the
+ // `pauseMatchingServiceWorkers` mechanism in each process.
+ await targetFront.pauseMatchingServiceWorkers({
+ origin: this.#currentTargetURL.origin,
+ });
+ } catch (e) {
+ if (targetFront.actorID) {
+ throw e;
+ } else {
+ console.warn(
+ "Process target destroyed while calling pauseMatchingServiceWorkers"
+ );
+ }
+ }
+ }
+
+ this._processTargets.add(targetFront);
+ return super._onProcessAvailable({ targetFront });
+ }
+
+ _shouldDestroyTargetsOnNavigation() {
+ return !!this.targetCommand.destroyServiceWorkersOnNavigation;
+ }
+
+ _onProcessDestroyed({ targetFront }) {
+ this._processTargets.delete(targetFront);
+ return super._onProcessDestroyed({ targetFront });
+ }
+
+ _onDocumentEvent(resources) {
+ for (const resource of resources) {
+ if (
+ resource.resourceType !==
+ this.commands.resourceCommand.TYPES.DOCUMENT_EVENT
+ ) {
+ continue;
+ }
+
+ if (resource.name === "will-navigate") {
+ // We rely on will-navigate as the onTargetAvailable for the top-level frame can
+ // happen after the onTargetAvailable for processes (handled in _onProcessAvailable),
+ // where we need the origin we navigate to.
+ this.#currentTargetURL = new URL(resource.newURI);
+ continue;
+ }
+
+ // Note that we rely on "dom-loading" rather than "will-navigate" because the
+ // destroyed/available callbacks should be triggered after the Debugger
+ // has cleaned up its reducers, which happens on "will-navigate".
+ // On the other end, "dom-complete", which is a better mapping of "navigate", is
+ // happening too late (because of resources being throttled), and would cause failures
+ // in test (like browser_target_command_service_workers_navigation.js), as the new worker
+ // target would already be registered at this point, and seen as something that would
+ // need to be destroyed.
+ if (resource.name === "dom-loading") {
+ const allServiceWorkerTargets = this._getAllServiceWorkerTargets();
+ const shouldDestroy = this._shouldDestroyTargetsOnNavigation();
+
+ for (const target of allServiceWorkerTargets) {
+ const isRegisteredBefore = this.targetCommand.isTargetRegistered(
+ target
+ );
+ if (shouldDestroy && isRegisteredBefore) {
+ // Instruct the target command to notify about the worker target destruction
+ // but do not destroy the front as we want to keep using it.
+ // We will notify about it again via onTargetAvailable.
+ this.onTargetDestroyed(target, { shouldDestroyTargetFront: false });
+ }
+
+ // Note: we call isTargetRegistered again because calls to
+ // onTargetDestroyed might have modified the list of registered targets.
+ const isRegisteredAfter = this.targetCommand.isTargetRegistered(
+ target
+ );
+ const isValidTarget = this._supportWorkerTarget(target);
+ if (isValidTarget && !isRegisteredAfter) {
+ // If the target is still valid for the current top target, call
+ // onTargetAvailable as well.
+ this.onTargetAvailable(target);
+ }
+ }
+ }
+ }
+ }
+
+ async _onRegistrationListChanged() {
+ if (this.targetCommand.isDestroyed()) {
+ return;
+ }
+
+ await this._updateRegistrations();
+
+ // Everything after this point is not strictly necessary for sw support
+ // in the target list, but it makes the behavior closer to the previous
+ // listAllWorkers/WorkersListener pair.
+ const allServiceWorkerTargets = this._getAllServiceWorkerTargets();
+ for (const target of allServiceWorkerTargets) {
+ const hasRegistration = this._getRegistrationForWorkerTarget(target);
+ if (!hasRegistration) {
+ // XXX: At this point the worker target is not really destroyed, but
+ // historically, listAllWorkers* APIs stopped returning worker targets
+ // if worker registrations are no longer available.
+ if (this.targetCommand.isTargetRegistered(target)) {
+ // Only emit onTargetDestroyed if it wasn't already done by
+ // onNavigate (ie the target is still tracked by TargetCommand)
+ this.onTargetDestroyed(target);
+ }
+ // Here we only care about service workers which no longer match *any*
+ // registration. The worker will be completely destroyed soon, remove
+ // it from the legacy worker watcher internal targetsByProcess Maps.
+ this._removeTargetReferences(target);
+ }
+ }
+ }
+
+ // Delete the provided worker target from the internal targetsByProcess Maps.
+ _removeTargetReferences(target) {
+ const allProcessTargets = this._getProcessTargets().filter(t =>
+ this.targetsByProcess.get(t)
+ );
+
+ for (const processTarget of allProcessTargets) {
+ this.targetsByProcess.get(processTarget).delete(target);
+ }
+ }
+
+ async _updateRegistrations() {
+ const {
+ registrations,
+ } = await this.rootFront.listServiceWorkerRegistrations();
+
+ this._registrations = registrations;
+ }
+
+ _getRegistrationForWorkerTarget(workerTarget) {
+ return this._registrations.find(r => {
+ return (
+ r.evaluatingWorker?.id === workerTarget.id ||
+ r.activeWorker?.id === workerTarget.id ||
+ r.installingWorker?.id === workerTarget.id ||
+ r.waitingWorker?.id === workerTarget.id
+ );
+ });
+ }
+
+ _getProcessTargets() {
+ return [...this._processTargets];
+ }
+
+ // Flatten all service worker targets in all processes.
+ _getAllServiceWorkerTargets() {
+ const allProcessTargets = this._getProcessTargets().filter(target =>
+ this.targetsByProcess.get(target)
+ );
+
+ const serviceWorkerTargets = [];
+ for (const target of allProcessTargets) {
+ serviceWorkerTargets.push(...this.targetsByProcess.get(target));
+ }
+ return serviceWorkerTargets;
+ }
+
+ // Check if the registration is relevant for the current target, ie
+ // corresponds to the same domain.
+ _isRegistrationValidForTarget(registration) {
+ if (this.targetCommand.descriptorFront.isBrowserProcessDescriptor) {
+ // All registrations are valid for main process debugging.
+ return true;
+ }
+
+ if (!this.targetCommand.descriptorFront.isTabDescriptor) {
+ // No support for service worker targets outside of main process &
+ // tab debugging.
+ return false;
+ }
+
+ // For local tabs, we match ServiceWorkerRegistrations and the target
+ // if they share the same hostname for their "url" properties.
+ const targetDomain = this.#currentTargetURL.hostname;
+ try {
+ const registrationDomain = new URL(registration.url).hostname;
+ return registrationDomain === targetDomain;
+ } catch (e) {
+ // XXX: Some registrations have an empty URL.
+ return false;
+ }
+ }
+}
+
+module.exports = LegacyServiceWorkersWatcher;