diff options
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.js | 320 |
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; |