/* 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 && !/resource:\/\/gre\/modules\/subprocess\/subprocess_.*\.worker\.js/.test( workerTarget.url ) ); } 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 or when we observe service workers targets // we don't want to remove the targets from targetsByProcess if (!isTargetSwitching || !this._isServiceWorkerWatcher) { 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;