legacy-serviceworkers-watcher.js (11362B)
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 { 8 WorkersListener, 9 // eslint-disable-next-line mozilla/reject-some-requires 10 } = require("resource://devtools/client/shared/workers-listener.js"); 11 12 const LegacyWorkersWatcher = require("resource://devtools/shared/commands/target/legacy-target-watchers/legacy-workers-watcher.js"); 13 14 class LegacyServiceWorkersWatcher extends LegacyWorkersWatcher { 15 // Holds the current target URL object 16 #currentTargetURL; 17 18 constructor(targetCommand, onTargetAvailable, onTargetDestroyed, commands) { 19 super(targetCommand, onTargetAvailable, onTargetDestroyed); 20 this._registrations = []; 21 this._processTargets = new Set(); 22 this.commands = commands; 23 24 // We need to listen for registration changes at least in order to properly 25 // filter service workers by domain when debugging a local tab. 26 // 27 // A WorkerTarget instance has a url property, but it points to the url of 28 // the script, whereas the url property of the ServiceWorkerRegistration 29 // points to the URL controlled by the service worker. 30 // 31 // Historically we have been matching the service worker registration URL 32 // to match service workers for local tab tools (app panel & debugger). 33 // Maybe here we could have some more info on the actual worker. 34 this._workersListener = new WorkersListener(this.rootFront, { 35 registrationsOnly: true, 36 }); 37 38 // Note that this is called much more often than when a registration 39 // is created or destroyed. WorkersListener notifies of anything that 40 // potentially impacted workers. 41 // I use it as a shortcut in this first patch. Listening to rootFront's 42 // "serviceWorkerRegistrationListChanged" should be enough to be notified 43 // about registrations. And if we need to also update the 44 // "debuggerServiceWorkerStatus" from here, then we would have to 45 // also listen to "registration-changed" one each registration. 46 this._onRegistrationListChanged = 47 this._onRegistrationListChanged.bind(this); 48 this._onDocumentEvent = this._onDocumentEvent.bind(this); 49 50 // Flag used from the parent class to listen to process targets. 51 // Decision tree is complicated, keep all logic in the parent methods. 52 this._isServiceWorkerWatcher = true; 53 } 54 55 /** 56 * Override from LegacyWorkersWatcher. 57 * 58 * We record all valid service worker targets (ie workers that match a service 59 * worker registration), but we will only notify about the ones which match 60 * the current domain. 61 */ 62 _recordWorkerTarget(workerTarget) { 63 return !!this._getRegistrationForWorkerTarget(workerTarget); 64 } 65 66 // Override from LegacyWorkersWatcher. 67 _supportWorkerTarget(workerTarget) { 68 if (!workerTarget.isServiceWorker) { 69 return false; 70 } 71 72 const registration = this._getRegistrationForWorkerTarget(workerTarget); 73 return registration && this._isRegistrationValidForTarget(registration); 74 } 75 76 // Override from LegacyWorkersWatcher. 77 async listen() { 78 // Listen to the current target front. 79 this.target = this.targetCommand.targetFront; 80 81 if (this.targetCommand.descriptorFront.isTabDescriptor) { 82 this.#currentTargetURL = new URL(this.targetCommand.targetFront.url); 83 } 84 85 this._workersListener.addListener(this._onRegistrationListChanged); 86 87 // Fetch the registrations before calling listen, since service workers 88 // might already be available and will need to be compared with the existing 89 // registrations. 90 await this._onRegistrationListChanged(); 91 92 if (this.targetCommand.descriptorFront.isTabDescriptor) { 93 await this.commands.resourceCommand.watchResources( 94 [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT], 95 { 96 onAvailable: this._onDocumentEvent, 97 ignoreExistingResources: true, 98 } 99 ); 100 } 101 102 await super.listen(); 103 } 104 105 // Override from LegacyWorkersWatcher. 106 unlisten(...args) { 107 this._workersListener.removeListener(this._onRegistrationListChanged); 108 109 if (this.targetCommand.descriptorFront.isTabDescriptor) { 110 this.commands.resourceCommand.unwatchResources( 111 [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT], 112 { 113 onAvailable: this._onDocumentEvent, 114 } 115 ); 116 } 117 118 super.unlisten(...args); 119 } 120 121 // Override from LegacyWorkersWatcher. 122 async _onProcessAvailable({ targetFront }) { 123 if (this.targetCommand.descriptorFront.isTabDescriptor) { 124 // XXX: This has been ported straight from the current debugger 125 // implementation. Since pauseMatchingServiceWorkers expects an origin 126 // to filter matching workers, it only makes sense when we are debugging 127 // a tab. However in theory, parent process debugging could pause all 128 // service workers without matching anything. 129 try { 130 // To support early breakpoint we need to setup the 131 // `pauseMatchingServiceWorkers` mechanism in each process. 132 await targetFront.pauseMatchingServiceWorkers({ 133 origin: this.#currentTargetURL.origin, 134 }); 135 } catch (e) { 136 if (targetFront.actorID) { 137 throw e; 138 } else { 139 console.warn( 140 "Process target destroyed while calling pauseMatchingServiceWorkers" 141 ); 142 } 143 } 144 } 145 146 this._processTargets.add(targetFront); 147 return super._onProcessAvailable({ targetFront }); 148 } 149 150 _onProcessDestroyed({ targetFront }) { 151 this._processTargets.delete(targetFront); 152 return super._onProcessDestroyed({ targetFront }); 153 } 154 155 _onDocumentEvent(resources) { 156 for (const resource of resources) { 157 if ( 158 resource.resourceType !== 159 this.commands.resourceCommand.TYPES.DOCUMENT_EVENT 160 ) { 161 continue; 162 } 163 164 if (resource.name === "will-navigate") { 165 // We rely on will-navigate as the onTargetAvailable for the top-level frame can 166 // happen after the onTargetAvailable for processes (handled in _onProcessAvailable), 167 // where we need the origin we navigate to. 168 this.#currentTargetURL = new URL(resource.newURI); 169 continue; 170 } 171 172 // Note that we rely on "dom-loading" rather than "will-navigate" because the 173 // destroyed/available callbacks should be triggered after the Debugger 174 // has cleaned up its reducers, which happens on "will-navigate". 175 // On the other end, "dom-complete", which is a better mapping of "navigate", is 176 // happening too late (because of resources being throttled), and would cause failures 177 // in test (like browser_target_command_service_workers_navigation.js), as the new worker 178 // target would already be registered at this point, and seen as something that would 179 // need to be destroyed. 180 if (resource.name === "dom-loading") { 181 const allServiceWorkerTargets = this._getAllServiceWorkerTargets(); 182 183 for (const target of allServiceWorkerTargets) { 184 // Note: we call isTargetRegistered again because calls to 185 // onTargetDestroyed might have modified the list of registered targets. 186 const isRegisteredAfter = 187 this.targetCommand.isTargetRegistered(target); 188 const isValidTarget = this._supportWorkerTarget(target); 189 if (isValidTarget && !isRegisteredAfter) { 190 // If the target is still valid for the current top target, call 191 // onTargetAvailable as well. 192 this.onTargetAvailable(target); 193 } 194 } 195 } 196 } 197 } 198 199 async _onRegistrationListChanged() { 200 if (this.targetCommand.isDestroyed()) { 201 return; 202 } 203 204 await this._updateRegistrations(); 205 206 // Everything after this point is not strictly necessary for sw support 207 // in the target list, but it makes the behavior closer to the previous 208 // listAllWorkers/WorkersListener pair. 209 const allServiceWorkerTargets = this._getAllServiceWorkerTargets(); 210 for (const target of allServiceWorkerTargets) { 211 const hasRegistration = this._getRegistrationForWorkerTarget(target); 212 if (!hasRegistration) { 213 // XXX: At this point the worker target is not really destroyed, but 214 // historically, listAllWorkers* APIs stopped returning worker targets 215 // if worker registrations are no longer available. 216 if (this.targetCommand.isTargetRegistered(target)) { 217 // Only emit onTargetDestroyed if it wasn't already done by 218 // onNavigate (ie the target is still tracked by TargetCommand) 219 this.onTargetDestroyed(target); 220 } 221 // Here we only care about service workers which no longer match *any* 222 // registration. The worker will be completely destroyed soon, remove 223 // it from the legacy worker watcher internal targetsByProcess Maps. 224 this._removeTargetReferences(target); 225 } 226 } 227 } 228 229 // Delete the provided worker target from the internal targetsByProcess Maps. 230 _removeTargetReferences(target) { 231 const allProcessTargets = this._getProcessTargets().filter(t => 232 this.targetsByProcess.get(t) 233 ); 234 235 for (const processTarget of allProcessTargets) { 236 this.targetsByProcess.get(processTarget).delete(target); 237 } 238 } 239 240 async _updateRegistrations() { 241 const { registrations } = 242 await this.rootFront.listServiceWorkerRegistrations(); 243 244 this._registrations = registrations; 245 } 246 247 _getRegistrationForWorkerTarget(workerTarget) { 248 return this._registrations.find(r => { 249 return ( 250 r.evaluatingWorker?.id === workerTarget.id || 251 r.activeWorker?.id === workerTarget.id || 252 r.installingWorker?.id === workerTarget.id || 253 r.waitingWorker?.id === workerTarget.id 254 ); 255 }); 256 } 257 258 _getProcessTargets() { 259 return [...this._processTargets]; 260 } 261 262 // Flatten all service worker targets in all processes. 263 _getAllServiceWorkerTargets() { 264 const allProcessTargets = this._getProcessTargets().filter(target => 265 this.targetsByProcess.get(target) 266 ); 267 268 const serviceWorkerTargets = []; 269 for (const target of allProcessTargets) { 270 serviceWorkerTargets.push(...this.targetsByProcess.get(target)); 271 } 272 return serviceWorkerTargets; 273 } 274 275 // Check if the registration is relevant for the current target, ie 276 // corresponds to the same domain. 277 _isRegistrationValidForTarget(registration) { 278 if (this.targetCommand.descriptorFront.isBrowserProcessDescriptor) { 279 // All registrations are valid for main process debugging. 280 return true; 281 } 282 283 if (!this.targetCommand.descriptorFront.isTabDescriptor) { 284 // No support for service worker targets outside of main process & 285 // tab debugging. 286 return false; 287 } 288 289 // For local tabs, we match ServiceWorkerRegistrations and the target 290 // if they share the same hostname for their "url" properties. 291 const targetDomain = this.#currentTargetURL.hostname; 292 const registrationDomain = URL.parse(registration.url)?.hostname; 293 if (registrationDomain) { 294 return registrationDomain === targetDomain; 295 } 296 // XXX: Some registrations have an empty URL. 297 return false; 298 } 299 } 300 301 module.exports = LegacyServiceWorkersWatcher;