diff options
Diffstat (limited to 'devtools/shared/commands/target/legacy-target-watchers')
5 files changed, 655 insertions, 0 deletions
diff --git a/devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js b/devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js new file mode 100644 index 0000000000..e0c5b18d51 --- /dev/null +++ b/devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js @@ -0,0 +1,72 @@ +/* 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"; + +class LegacyProcessesWatcher { + constructor(targetCommand, onTargetAvailable, onTargetDestroyed) { + this.targetCommand = targetCommand; + this.rootFront = targetCommand.rootFront; + + this.onTargetAvailable = onTargetAvailable; + this.onTargetDestroyed = onTargetDestroyed; + + this.descriptors = new Set(); + this._processListChanged = this._processListChanged.bind(this); + } + + async _processListChanged() { + if (this.targetCommand.isDestroyed()) { + return; + } + + const processes = await this.rootFront.listProcesses(); + // Process the new list to detect the ones being destroyed + // Force destroyed the descriptor as well as the target + for (const descriptor of this.descriptors) { + if (!processes.includes(descriptor)) { + // Manually call onTargetDestroyed listeners in order to + // ensure calling them *before* destroying the descriptor. + // Otherwise the descriptor will automatically destroy the target + // and may not fire the contentProcessTarget's destroy event. + const target = descriptor.getCachedTarget(); + if (target) { + this.onTargetDestroyed(target); + } + + descriptor.destroy(); + this.descriptors.delete(descriptor); + } + } + + const promises = processes + .filter(descriptor => !this.descriptors.has(descriptor)) + .map(async descriptor => { + // Add the new process descriptors to the local list + this.descriptors.add(descriptor); + const target = await descriptor.getTarget(); + if (!target) { + console.error( + "Wasn't able to retrieve the target for", + descriptor.actorID + ); + return; + } + await this.onTargetAvailable(target); + }); + + await Promise.all(promises); + } + + async listen() { + this.rootFront.on("processListChanged", this._processListChanged); + await this._processListChanged(); + } + + unlisten() { + this.rootFront.off("processListChanged", this._processListChanged); + } +} + +module.exports = LegacyProcessesWatcher; 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..adaeb9def4 --- /dev/null +++ b/devtools/shared/commands/target/legacy-target-watchers/legacy-serviceworkers-watcher.js @@ -0,0 +1,316 @@ +/* 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; diff --git a/devtools/shared/commands/target/legacy-target-watchers/legacy-sharedworkers-watcher.js b/devtools/shared/commands/target/legacy-target-watchers/legacy-sharedworkers-watcher.js new file mode 100644 index 0000000000..b248e6aef7 --- /dev/null +++ b/devtools/shared/commands/target/legacy-target-watchers/legacy-sharedworkers-watcher.js @@ -0,0 +1,19 @@ +/* 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 LegacyWorkersWatcher = require("resource://devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js"); + +class LegacySharedWorkersWatcher extends LegacyWorkersWatcher { + // Flag used from the parent class to listen to process targets. + // Decision tree is complicated, keep all logic in the parent methods. + _isSharedWorkerWatcher = true; + + _supportWorkerTarget(workerTarget) { + return workerTarget.isSharedWorker; + } +} + +module.exports = LegacySharedWorkersWatcher; diff --git a/devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js b/devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js new file mode 100644 index 0000000000..0baa14757b --- /dev/null +++ b/devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js @@ -0,0 +1,238 @@ +/* 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 LegacyProcessesWatcher = require("resource://devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js"); + +class LegacyWorkersWatcher { + constructor(targetCommand, onTargetAvailable, onTargetDestroyed) { + this.targetCommand = targetCommand; + this.rootFront = targetCommand.rootFront; + + this.onTargetAvailable = onTargetAvailable; + this.onTargetDestroyed = onTargetDestroyed; + + this.targetsByProcess = new WeakMap(); + this.targetsListeners = new WeakMap(); + + this._onProcessAvailable = this._onProcessAvailable.bind(this); + this._onProcessDestroyed = this._onProcessDestroyed.bind(this); + } + + async _onProcessAvailable({ targetFront }) { + this.targetsByProcess.set(targetFront, new Set()); + // Listen for worker which will be created later + const listener = this._workerListChanged.bind(this, targetFront); + this.targetsListeners.set(targetFront, listener); + + // If this is the browser toolbox, we have to listen from the RootFront + // (see comment in _workerListChanged) + const front = targetFront.isParentProcess ? this.rootFront : targetFront; + front.on("workerListChanged", listener); + + // We also need to process the already existing workers + await this._workerListChanged(targetFront); + } + + async _onProcessDestroyed({ targetFront }) { + const existingTargets = this.targetsByProcess.get(targetFront); + + // Process the new list to detect the ones being destroyed + // Force destroying the targets + for (const target of existingTargets) { + this.onTargetDestroyed(target); + + target.destroy(); + existingTargets.delete(target); + } + this.targetsByProcess.delete(targetFront); + this.targetsListeners.delete(targetFront); + } + + _supportWorkerTarget(workerTarget) { + // subprocess workers are ignored because they take several seconds to + // attach to when opening the browser toolbox. See bug 1594597. + // When attaching we get the following error: + // JavaScript error: resource://devtools/server/startup/worker.js, + // line 37: NetworkError: WorkerDebuggerGlobalScope.loadSubScript: Failed to load worker script at resource://devtools/shared/worker/loader.js (nsresult = 0x805e0006) + return ( + workerTarget.isDedicatedWorker && + !workerTarget.url.startsWith( + "resource://gre/modules/subprocess/subprocess_worker" + ) + ); + } + + async _workerListChanged(targetFront) { + // If we're in the Browser Toolbox, query workers from the Root Front instead of the + // ParentProcessTarget as the ParentProcess Target filters out the workers to only + // show the one from the top level window, whereas we expect the one from all the + // windows, and also the window-less ones. + // TODO: For Content Toolbox, expose SW of the page, maybe optionally? + const front = targetFront.isParentProcess ? this.rootFront : targetFront; + if (!front || front.isDestroyed() || this.targetCommand.isDestroyed()) { + return; + } + + let workers; + try { + ({ workers } = await front.listWorkers()); + } catch (e) { + // Workers may be added/removed at anytime so that listWorkers request + // can be spawn during a toolbox destroy sequence and easily fail + if (front.isDestroyed()) { + return; + } + throw e; + } + + // Fetch the list of already existing worker targets for this process target front. + const existingTargets = this.targetsByProcess.get(targetFront); + if (!existingTargets) { + // unlisten was called while processing the workerListChanged callback. + return; + } + + // Process the new list to detect the ones being destroyed + // Force destroying the targets + for (const target of existingTargets) { + if (!workers.includes(target)) { + this.onTargetDestroyed(target); + + target.destroy(); + existingTargets.delete(target); + } + } + + const promises = workers.map(workerTarget => + this._processNewWorkerTarget(workerTarget, existingTargets) + ); + await Promise.all(promises); + } + + // This is overloaded for Service Workers, which records all SW targets, + // but only notify about a subset of them. + _recordWorkerTarget(workerTarget) { + return this._supportWorkerTarget(workerTarget); + } + + async _processNewWorkerTarget(workerTarget, existingTargets) { + if ( + !this._recordWorkerTarget(workerTarget) || + existingTargets.has(workerTarget) || + this.targetCommand.isDestroyed() + ) { + return; + } + + // Add the new worker targets to the local list + existingTargets.add(workerTarget); + + if (this._supportWorkerTarget(workerTarget)) { + await this.onTargetAvailable(workerTarget); + } + } + + async listen() { + // Listen to the current target front. + this.target = this.targetCommand.targetFront; + + if (this.target.isParentProcess) { + await this.targetCommand.watchTargets({ + types: [this.targetCommand.TYPES.PROCESS], + onAvailable: this._onProcessAvailable, + onDestroyed: this._onProcessDestroyed, + }); + + // The ParentProcessTarget front is considered to be a FRAME instead of a PROCESS. + // So process it manually here. + await this._onProcessAvailable({ targetFront: this.target }); + return; + } + + if (this._isSharedWorkerWatcher) { + // Here we're not in the browser toolbox, and SharedWorker targets are not supported + // in regular toolbox (See Bug 1607778) + return; + } + + if (this._isServiceWorkerWatcher) { + this._legacyProcessesWatcher = new LegacyProcessesWatcher( + this.targetCommand, + async targetFront => { + // Service workers only live in content processes. + if (!targetFront.isParentProcess) { + await this._onProcessAvailable({ targetFront }); + } + }, + targetFront => { + if (!targetFront.isParentProcess) { + this._onProcessDestroyed({ targetFront }); + } + } + ); + await this._legacyProcessesWatcher.listen(); + return; + } + + // Here, we're handling Dedicated Workers in content toolbox. + this.targetsByProcess.set( + this.target, + this.targetsByProcess.get(this.target) || new Set() + ); + this._workerListChangedListener = this._workerListChanged.bind( + this, + this.target + ); + this.target.on("workerListChanged", this._workerListChangedListener); + await this._workerListChanged(this.target); + } + + _getProcessTargets() { + return this.targetCommand.getAllTargets([this.targetCommand.TYPES.PROCESS]); + } + + unlisten({ isTargetSwitching } = {}) { + // Stop listening for new process targets. + if (this.target.isParentProcess) { + this.targetCommand.unwatchTargets({ + types: [this.targetCommand.TYPES.PROCESS], + onAvailable: this._onProcessAvailable, + onDestroyed: this._onProcessDestroyed, + }); + } else if (this._isServiceWorkerWatcher) { + this._legacyProcessesWatcher.unlisten(); + } + + // Cleanup the targetsByProcess/targetsListeners maps, and unsubscribe from + // all targetFronts. Process target fronts are either stored locally when + // watching service workers for the content toolbox, or can be retrieved via + // the TargetCommand API otherwise (see _getProcessTargets implementations). + if (this.target.isParentProcess || this._isServiceWorkerWatcher) { + for (const targetFront of this._getProcessTargets()) { + const listener = this.targetsListeners.get(targetFront); + targetFront.off("workerListChanged", listener); + + // When unlisten is called from a target switch and service workers targets are not + // destroyed on navigation, we don't want to remove the targets from targetsByProcess + if ( + !isTargetSwitching || + !this._isServiceWorkerWatcher || + this.targetCommand.destroyServiceWorkersOnNavigation + ) { + this.targetsByProcess.delete(targetFront); + } + this.targetsListeners.delete(targetFront); + } + } else { + this.target.off("workerListChanged", this._workerListChangedListener); + delete this._workerListChangedListener; + this.targetsByProcess.delete(this.target); + this.targetsListeners.delete(this.target); + } + } +} + +module.exports = LegacyWorkersWatcher; diff --git a/devtools/shared/commands/target/legacy-target-watchers/moz.build b/devtools/shared/commands/target/legacy-target-watchers/moz.build new file mode 100644 index 0000000000..60fdd7ec22 --- /dev/null +++ b/devtools/shared/commands/target/legacy-target-watchers/moz.build @@ -0,0 +1,10 @@ +# 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( + "legacy-processes-watcher.js", + "legacy-serviceworkers-watcher.js", + "legacy-sharedworkers-watcher.js", + "legacy-workers-watcher.js", +) |