legacy-workers-watcher.js (8465B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const LegacyProcessesWatcher = require("resource://devtools/shared/commands/target/legacy-target-watchers/legacy-processes-watcher.js"); 8 9 class LegacyWorkersWatcher { 10 constructor(targetCommand, onTargetAvailable, onTargetDestroyed) { 11 this.targetCommand = targetCommand; 12 this.rootFront = targetCommand.rootFront; 13 14 this.onTargetAvailable = onTargetAvailable; 15 this.onTargetDestroyed = onTargetDestroyed; 16 17 this.targetsByProcess = new WeakMap(); 18 this.targetsListeners = new WeakMap(); 19 20 this._onProcessAvailable = this._onProcessAvailable.bind(this); 21 this._onProcessDestroyed = this._onProcessDestroyed.bind(this); 22 } 23 24 async _onProcessAvailable({ targetFront }) { 25 this.targetsByProcess.set(targetFront, new Set()); 26 // Listen for worker which will be created later 27 const listener = this._workerListChanged.bind(this, targetFront); 28 this.targetsListeners.set(targetFront, listener); 29 30 // If this is the browser toolbox, we have to listen from the RootFront 31 // (see comment in _workerListChanged) 32 const front = targetFront.isParentProcess ? this.rootFront : targetFront; 33 front.on("workerListChanged", listener); 34 35 // We also need to process the already existing workers 36 await this._workerListChanged(targetFront); 37 } 38 39 async _onProcessDestroyed({ targetFront }) { 40 const existingTargets = this.targetsByProcess.get(targetFront); 41 42 // Process the new list to detect the ones being destroyed 43 // Force destroying the targets 44 for (const target of existingTargets) { 45 this.onTargetDestroyed(target); 46 47 target.destroy(); 48 existingTargets.delete(target); 49 } 50 this.targetsByProcess.delete(targetFront); 51 this.targetsListeners.delete(targetFront); 52 } 53 54 _supportWorkerTarget(workerTarget) { 55 // subprocess workers are ignored because they take several seconds to 56 // attach to when opening the browser toolbox. See bug 1594597. 57 // When attaching we get the following error: 58 // JavaScript error: resource://devtools/server/startup/worker.js, 59 // line 37: NetworkError: WorkerDebuggerGlobalScope.loadSubScript: Failed to load worker script at resource://devtools/shared/worker/loader.js (nsresult = 0x805e0006) 60 return ( 61 workerTarget.isDedicatedWorker && 62 !/resource:\/\/gre\/modules\/subprocess\/subprocess_.*\.worker\.js/.test( 63 workerTarget.url 64 ) 65 ); 66 } 67 68 async _workerListChanged(targetFront) { 69 // If we're in the Browser Toolbox, query workers from the Root Front instead of the 70 // ParentProcessTarget as the ParentProcess Target filters out the workers to only 71 // show the one from the top level window, whereas we expect the one from all the 72 // windows, and also the window-less ones. 73 // TODO: For Content Toolbox, expose SW of the page, maybe optionally? 74 const front = targetFront.isParentProcess ? this.rootFront : targetFront; 75 if (!front || front.isDestroyed() || this.targetCommand.isDestroyed()) { 76 return; 77 } 78 79 let workers; 80 try { 81 ({ workers } = await front.listWorkers()); 82 } catch (e) { 83 // Workers may be added/removed at anytime so that listWorkers request 84 // can be spawn during a toolbox destroy sequence and easily fail 85 if (front.isDestroyed()) { 86 return; 87 } 88 throw e; 89 } 90 91 // Fetch the list of already existing worker targets for this process target front. 92 const existingTargets = this.targetsByProcess.get(targetFront); 93 if (!existingTargets) { 94 // unlisten was called while processing the workerListChanged callback. 95 return; 96 } 97 98 // Process the new list to detect the ones being destroyed 99 // Force destroying the targets 100 for (const target of existingTargets) { 101 if (!workers.includes(target)) { 102 this.onTargetDestroyed(target); 103 104 target.destroy(); 105 existingTargets.delete(target); 106 } 107 } 108 109 const promises = workers.map(workerTarget => 110 this._processNewWorkerTarget(workerTarget, existingTargets) 111 ); 112 await Promise.all(promises); 113 } 114 115 // This is overloaded for Service Workers, which records all SW targets, 116 // but only notify about a subset of them. 117 _recordWorkerTarget(workerTarget) { 118 return this._supportWorkerTarget(workerTarget); 119 } 120 121 async _processNewWorkerTarget(workerTarget, existingTargets) { 122 if ( 123 !this._recordWorkerTarget(workerTarget) || 124 existingTargets.has(workerTarget) || 125 this.targetCommand.isDestroyed() 126 ) { 127 return; 128 } 129 130 // Add the new worker targets to the local list 131 existingTargets.add(workerTarget); 132 133 if (this._supportWorkerTarget(workerTarget)) { 134 await this.onTargetAvailable(workerTarget); 135 } 136 } 137 138 async listen() { 139 // Listen to the current target front. 140 this.target = this.targetCommand.targetFront; 141 142 if (this.target.isParentProcess) { 143 await this.targetCommand.watchTargets({ 144 types: [this.targetCommand.TYPES.PROCESS], 145 onAvailable: this._onProcessAvailable, 146 onDestroyed: this._onProcessDestroyed, 147 }); 148 149 // The ParentProcessTarget front is considered to be a FRAME instead of a PROCESS. 150 // So process it manually here. 151 await this._onProcessAvailable({ targetFront: this.target }); 152 return; 153 } 154 155 if (this._isSharedWorkerWatcher) { 156 // Here we're not in the browser toolbox, and SharedWorker targets are not supported 157 // in regular toolbox (See Bug 1607778) 158 return; 159 } 160 161 if (this._isServiceWorkerWatcher) { 162 this._legacyProcessesWatcher = new LegacyProcessesWatcher( 163 this.targetCommand, 164 async targetFront => { 165 // Service workers only live in content processes. 166 if (!targetFront.isParentProcess) { 167 await this._onProcessAvailable({ targetFront }); 168 } 169 }, 170 targetFront => { 171 if (!targetFront.isParentProcess) { 172 this._onProcessDestroyed({ targetFront }); 173 } 174 } 175 ); 176 await this._legacyProcessesWatcher.listen(); 177 return; 178 } 179 180 // Here, we're handling Dedicated Workers in content toolbox. 181 this.targetsByProcess.set( 182 this.target, 183 this.targetsByProcess.get(this.target) || new Set() 184 ); 185 this._workerListChangedListener = this._workerListChanged.bind( 186 this, 187 this.target 188 ); 189 this.target.on("workerListChanged", this._workerListChangedListener); 190 await this._workerListChanged(this.target); 191 } 192 193 _getProcessTargets() { 194 return this.targetCommand.getAllTargets([this.targetCommand.TYPES.PROCESS]); 195 } 196 197 unlisten({ isTargetSwitching } = {}) { 198 // Stop listening for new process targets. 199 if (this.target.isParentProcess) { 200 this.targetCommand.unwatchTargets({ 201 types: [this.targetCommand.TYPES.PROCESS], 202 onAvailable: this._onProcessAvailable, 203 onDestroyed: this._onProcessDestroyed, 204 }); 205 } else if (this._isServiceWorkerWatcher) { 206 this._legacyProcessesWatcher.unlisten(); 207 } 208 209 // Cleanup the targetsByProcess/targetsListeners maps, and unsubscribe from 210 // all targetFronts. Process target fronts are either stored locally when 211 // watching service workers for the content toolbox, or can be retrieved via 212 // the TargetCommand API otherwise (see _getProcessTargets implementations). 213 if (this.target.isParentProcess || this._isServiceWorkerWatcher) { 214 for (const targetFront of this._getProcessTargets()) { 215 const listener = this.targetsListeners.get(targetFront); 216 targetFront.off("workerListChanged", listener); 217 218 // When unlisten is called from a target switch or when we observe service workers targets 219 // we don't want to remove the targets from targetsByProcess 220 if (!isTargetSwitching || !this._isServiceWorkerWatcher) { 221 this.targetsByProcess.delete(targetFront); 222 } 223 this.targetsListeners.delete(targetFront); 224 } 225 } else { 226 this.target.off("workerListChanged", this._workerListChangedListener); 227 delete this._workerListChangedListener; 228 this.targetsByProcess.delete(this.target); 229 this.targetsListeners.delete(this.target); 230 } 231 } 232 } 233 234 module.exports = LegacyWorkersWatcher;