root.js (21015B)
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 // protocol.js uses objects as exceptions in order to define 8 // error packets. 9 /* eslint-disable no-throw-literal */ 10 11 const { Actor, Pool } = require("resource://devtools/shared/protocol.js"); 12 const { rootSpec } = require("resource://devtools/shared/specs/root.js"); 13 14 const { 15 LazyPool, 16 createExtraActors, 17 } = require("resource://devtools/shared/protocol/lazy-pool.js"); 18 const { 19 DevToolsServer, 20 } = require("resource://devtools/server/devtools-server.js"); 21 const Resources = require("resource://devtools/server/actors/resources/index.js"); 22 23 loader.lazyRequireGetter( 24 this, 25 "ProcessDescriptorActor", 26 "resource://devtools/server/actors/descriptors/process.js", 27 true 28 ); 29 30 /* Root actor for the remote debugging protocol. */ 31 32 /** 33 * Create a remote debugging protocol root actor. 34 * 35 * @param conn 36 * The DevToolsServerConnection whose root actor we are constructing. 37 * 38 * @param parameters 39 * The properties of |parameters| provide backing objects for the root 40 * actor's requests; if a given property is omitted from |parameters|, the 41 * root actor won't implement the corresponding requests or notifications. 42 * Supported properties: 43 * 44 * - tabList: a live list (see below) of target actors for tabs. If present, 45 * the new root actor supports the 'listTabs' request, providing the live 46 * list's elements as its target actors, and sending 'tabListChanged' 47 * notifications when the live list's contents change. One actor in 48 * this list must have a true '.selected' property. 49 * 50 * - addonList: a live list (see below) of addon actors. If present, the 51 * new root actor supports the 'listAddons' request, providing the live 52 * list's elements as its addon actors, and sending 'addonListchanged' 53 * notifications when the live list's contents change. 54 * 55 * - globalActorFactories: an object |A| describing further actors to 56 * attach to the 'listTabs' reply. This is the type accumulated by 57 * ActorRegistry.addGlobalActor. For each own property |P| of |A|, 58 * the root actor adds a property named |P| to the 'listTabs' 59 * reply whose value is the name of an actor constructed by 60 * |A[P]|. 61 * 62 * - onShutdown: a function to call when the root actor is destroyed. 63 * 64 * Instance properties: 65 * 66 * - applicationType: the string the root actor will include as the 67 * "applicationType" property in the greeting packet. By default, this 68 * is "browser". 69 * 70 * Live lists: 71 * 72 * A "live list", as used for the |tabList|, is an object that presents a 73 * list of actors, and also notifies its clients of changes to the list. A 74 * live list's interface is two properties: 75 * 76 * - getList: a method that returns a promise to the contents of the list. 77 * 78 * - onListChanged: a handler called, with no arguments, when the set of 79 * values the iterator would produce has changed since the last 80 * time 'iterator' was called. This may only be set to null or a 81 * callable value (one for which the typeof operator returns 82 * 'function'). (Note that the live list will not call the 83 * onListChanged handler until the list has been iterated over 84 * once; if nobody's seen the list in the first place, nobody 85 * should care if its contents have changed!) 86 * 87 * When the list changes, the list implementation should ensure that any 88 * actors yielded in previous iterations whose referents (tabs) still exist 89 * get yielded again in subsequent iterations. If the underlying referent 90 * is the same, the same actor should be presented for it. 91 * 92 * The root actor registers an 'onListChanged' handler on the appropriate 93 * list when it may need to send the client 'tabListChanged' notifications, 94 * and is careful to remove the handler whenever it does not need to send 95 * such notifications (including when it is destroyed). This means that 96 * live list implementations can use the state of the handler property (set 97 * or null) to install and remove observers and event listeners. 98 * 99 * Note that, as the only way for the root actor to see the members of the 100 * live list is to begin an iteration over the list, the live list need not 101 * actually produce any actors until they are reached in the course of 102 * iteration: alliterative lazy live lists. 103 */ 104 class RootActor extends Actor { 105 constructor(conn, parameters) { 106 super(conn, rootSpec); 107 108 this._parameters = parameters; 109 this._onTabListChanged = this.onTabListChanged.bind(this); 110 this._onAddonListChanged = this.onAddonListChanged.bind(this); 111 this._onWorkerListChanged = this.onWorkerListChanged.bind(this); 112 this._onServiceWorkerRegistrationListChanged = 113 this.onServiceWorkerRegistrationListChanged.bind(this); 114 this._onProcessListChanged = this.onProcessListChanged.bind(this); 115 116 this._extraActors = {}; 117 118 this._globalActorPool = new LazyPool(this.conn); 119 120 this.applicationType = "browser"; 121 122 // Compute the list of all supported Root Resources 123 const supportedResources = {}; 124 for (const resourceType in Resources.RootResources) { 125 supportedResources[resourceType] = true; 126 } 127 128 this.traits = { 129 networkMonitor: true, 130 resources: supportedResources, 131 // @backward-compat { version 84 } Expose the pref value to the client. 132 // Services.prefs is undefined in xpcshell tests. 133 workerConsoleApiMessagesDispatchedToMainThread: Services.prefs 134 ? Services.prefs.getBoolPref( 135 "dom.worker.console.dispatch_events_to_main_thread" 136 ) 137 : true, 138 // @backward-compat { version 151 } Process Descriptor's `getWatcher()` 139 // supports a new 'enableWindowGlobalThreadActors' flag to enable 140 // the WindowGlobal's thread actors when debugging the whole browser. 141 // This was actually changed in 137, but we support it for VSCode until 142 // ESR 140 is the only ESR available. 143 // 144 // ESR 115 EOL is currently planned for March 24 2026. Do not remove 145 // this trait before that date AND make sure the extension has been 146 // updated (https://github.com/firefox-devtools/vscode-firefox-debug/issues/391). 147 // Contact Holger Benl (hbenl) for topics related to the extension. 148 supportsEnableWindowGlobalThreadActors: true, 149 }; 150 } 151 152 /** 153 * Return a 'hello' packet as specified by the Remote Debugging Protocol. 154 */ 155 sayHello() { 156 return { 157 from: this.actorID, 158 applicationType: this.applicationType, 159 /* This is not in the spec, but it's used by tests. */ 160 testConnectionPrefix: this.conn.prefix, 161 traits: this.traits, 162 }; 163 } 164 165 forwardingCancelled(prefix) { 166 return { 167 from: this.actorID, 168 type: "forwardingCancelled", 169 prefix, 170 }; 171 } 172 173 /** 174 * Destroys the actor from the browser window. 175 */ 176 destroy() { 177 Resources.unwatchAllResources(this); 178 179 super.destroy(); 180 181 /* Tell the live lists we aren't watching any more. */ 182 if (this._parameters.tabList) { 183 this._parameters.tabList.destroy(); 184 } 185 if (this._parameters.addonList) { 186 this._parameters.addonList.onListChanged = null; 187 } 188 if (this._parameters.workerList) { 189 this._parameters.workerList.destroy(); 190 } 191 if (this._parameters.serviceWorkerRegistrationList) { 192 this._parameters.serviceWorkerRegistrationList.onListChanged = null; 193 } 194 if (this._parameters.processList) { 195 this._parameters.processList.onListChanged = null; 196 } 197 if (typeof this._parameters.onShutdown === "function") { 198 this._parameters.onShutdown(); 199 } 200 // Cleanup Actors on destroy 201 if (this._tabDescriptorActorPool) { 202 this._tabDescriptorActorPool.destroy(); 203 } 204 if (this._processDescriptorActorPool) { 205 this._processDescriptorActorPool.destroy(); 206 } 207 if (this._globalActorPool) { 208 this._globalActorPool.destroy(); 209 } 210 if (this._addonTargetActorPool) { 211 this._addonTargetActorPool.destroy(); 212 } 213 if (this._workerDescriptorActorPool) { 214 this._workerDescriptorActorPool.destroy(); 215 } 216 if (this._frameDescriptorActorPool) { 217 this._frameDescriptorActorPool.destroy(); 218 } 219 220 if (this._serviceWorkerRegistrationActorPool) { 221 this._serviceWorkerRegistrationActorPool.destroy(); 222 } 223 this._extraActors = null; 224 this._tabDescriptorActorPool = null; 225 this._globalActorPool = null; 226 this._parameters = null; 227 } 228 229 /** 230 * Method called by the client right after the root actor is communicated to it, 231 * with information about the frontend. 232 * 233 * For now this is used by Servo which implements different backend APIs, 234 * based on the frontend version. (backward compat to support many frontend versions 235 * on the same backend revision) 236 */ 237 // eslint-disable-next-line no-unused-vars 238 connect({ frontendVersion }) {} 239 240 /** 241 * Gets the "root" form, which lists all the global actors that affect the entire 242 * browser. 243 */ 244 getRoot() { 245 // Create global actors 246 if (!this._globalActorPool) { 247 this._globalActorPool = new LazyPool(this.conn); 248 } 249 const actors = createExtraActors( 250 this._parameters.globalActorFactories, 251 this._globalActorPool, 252 this 253 ); 254 255 return actors; 256 } 257 258 /* The 'listTabs' request and the 'tabListChanged' notification. */ 259 260 /** 261 * Handles the listTabs request. The actors will survive until at least 262 * the next listTabs request. 263 */ 264 async listTabs() { 265 const tabList = this._parameters.tabList; 266 if (!tabList) { 267 throw { 268 error: "noTabs", 269 message: "This root actor has no browser tabs.", 270 }; 271 } 272 273 // Now that a client has requested the list of tabs, we reattach the onListChanged 274 // listener in order to be notified if the list of tabs changes again in the future. 275 tabList.onListChanged = this._onTabListChanged; 276 277 // Walk the tab list, accumulating the array of target actors for the reply, and 278 // moving all the actors to a new Pool. We'll replace the old tab target actor 279 // pool with the one we build here, thus retiring any actors that didn't get listed 280 // again, and preparing any new actors to receive packets. 281 const newActorPool = new Pool(this.conn, "listTabs-tab-descriptors"); 282 283 const tabDescriptorActors = await tabList.getList(); 284 for (const tabDescriptorActor of tabDescriptorActors) { 285 newActorPool.manage(tabDescriptorActor); 286 } 287 288 // Drop the old actorID -> actor map. Actors that still mattered were added to the 289 // new map; others will go away. 290 if (this._tabDescriptorActorPool) { 291 this._tabDescriptorActorPool.destroy(); 292 } 293 this._tabDescriptorActorPool = newActorPool; 294 295 return tabDescriptorActors; 296 } 297 298 /** 299 * Return the tab descriptor actor for the tab identified by one of the IDs 300 * passed as argument. 301 * 302 * See BrowserTabList.prototype.getTab for the definition of these IDs. 303 */ 304 async getTab({ browserId }) { 305 const tabList = this._parameters.tabList; 306 if (!tabList) { 307 throw { 308 error: "noTabs", 309 message: "This root actor has no browser tabs.", 310 }; 311 } 312 if (!this._tabDescriptorActorPool) { 313 this._tabDescriptorActorPool = new Pool( 314 this.conn, 315 "getTab-tab-descriptors" 316 ); 317 } 318 319 let descriptorActor; 320 try { 321 descriptorActor = await tabList.getTab({ 322 browserId, 323 }); 324 } catch (error) { 325 if (error.error) { 326 // Pipe expected errors as-is to the client 327 throw error; 328 } 329 throw { 330 error: "noTab", 331 message: "Unexpected error while calling getTab(): " + error, 332 }; 333 } 334 335 descriptorActor.parentID = this.actorID; 336 this._tabDescriptorActorPool.manage(descriptorActor); 337 338 return descriptorActor; 339 } 340 341 onTabListChanged() { 342 this.conn.send({ from: this.actorID, type: "tabListChanged" }); 343 /* It's a one-shot notification; no need to watch any more. */ 344 this._parameters.tabList.onListChanged = null; 345 } 346 347 /** 348 * This function can receive the following option from devtools client. 349 * 350 * @param {object} option 351 * - iconDataURL: {boolean} 352 * When true, make data url from the icon of addon, then make possible to 353 * access by iconDataURL in the actor. The iconDataURL is useful when 354 * retrieving addons from a remote device, because the raw iconURL might not 355 * be accessible on the client. 356 */ 357 async listAddons(option) { 358 const addonList = this._parameters.addonList; 359 if (!addonList) { 360 throw { 361 error: "noAddons", 362 message: "This root actor has no browser addons.", 363 }; 364 } 365 366 // Reattach the onListChanged listener now that a client requested the list. 367 addonList.onListChanged = this._onAddonListChanged; 368 369 const addonTargetActors = await addonList.getList(); 370 const addonTargetActorPool = new Pool(this.conn, "addon-descriptors"); 371 for (const addonTargetActor of addonTargetActors) { 372 if (option.iconDataURL) { 373 await addonTargetActor.loadIconDataURL(); 374 } 375 376 addonTargetActorPool.manage(addonTargetActor); 377 } 378 379 if (this._addonTargetActorPool) { 380 this._addonTargetActorPool.destroy(); 381 } 382 this._addonTargetActorPool = addonTargetActorPool; 383 384 return addonTargetActors; 385 } 386 387 onAddonListChanged() { 388 this.conn.send({ from: this.actorID, type: "addonListChanged" }); 389 this._parameters.addonList.onListChanged = null; 390 } 391 392 listWorkers() { 393 const workerList = this._parameters.workerList; 394 if (!workerList) { 395 throw { 396 error: "noWorkers", 397 message: "This root actor has no workers.", 398 }; 399 } 400 401 // Reattach the onListChanged listener now that a client requested the list. 402 workerList.onListChanged = this._onWorkerListChanged; 403 404 return workerList.getList().then(actors => { 405 const pool = new Pool(this.conn, "worker-targets"); 406 for (const actor of actors) { 407 pool.manage(actor); 408 } 409 410 // Do not destroy the pool before transfering ownership to the newly created 411 // pool, so that we do not accidently destroy actors that are still in use. 412 if (this._workerDescriptorActorPool) { 413 this._workerDescriptorActorPool.destroy(); 414 } 415 416 this._workerDescriptorActorPool = pool; 417 418 return { 419 workers: actors, 420 }; 421 }); 422 } 423 424 onWorkerListChanged() { 425 this.conn.send({ from: this.actorID, type: "workerListChanged" }); 426 this._parameters.workerList.onListChanged = null; 427 } 428 429 listServiceWorkerRegistrations() { 430 const registrationList = this._parameters.serviceWorkerRegistrationList; 431 if (!registrationList) { 432 throw { 433 error: "noServiceWorkerRegistrations", 434 message: "This root actor has no service worker registrations.", 435 }; 436 } 437 438 // Reattach the onListChanged listener now that a client requested the list. 439 registrationList.onListChanged = 440 this._onServiceWorkerRegistrationListChanged; 441 442 return registrationList.getList().then(actors => { 443 const pool = new Pool(this.conn, "service-workers-registrations"); 444 for (const actor of actors) { 445 pool.manage(actor); 446 } 447 448 if (this._serviceWorkerRegistrationActorPool) { 449 this._serviceWorkerRegistrationActorPool.destroy(); 450 } 451 this._serviceWorkerRegistrationActorPool = pool; 452 453 return { 454 registrations: actors, 455 }; 456 }); 457 } 458 459 onServiceWorkerRegistrationListChanged() { 460 this.conn.send({ 461 from: this.actorID, 462 type: "serviceWorkerRegistrationListChanged", 463 }); 464 this._parameters.serviceWorkerRegistrationList.onListChanged = null; 465 } 466 467 listProcesses() { 468 const { processList } = this._parameters; 469 if (!processList) { 470 throw { 471 error: "noProcesses", 472 message: "This root actor has no processes.", 473 }; 474 } 475 processList.onListChanged = this._onProcessListChanged; 476 const processes = processList.getList(); 477 const pool = new Pool(this.conn, "process-descriptors"); 478 for (const metadata of processes) { 479 let processDescriptor = this._getKnownDescriptor( 480 metadata.id, 481 this._processDescriptorActorPool 482 ); 483 if (!processDescriptor) { 484 processDescriptor = new ProcessDescriptorActor(this.conn, metadata); 485 } 486 pool.manage(processDescriptor); 487 } 488 // Do not destroy the pool before transfering ownership to the newly created 489 // pool, so that we do not accidently destroy actors that are still in use. 490 if (this._processDescriptorActorPool) { 491 this._processDescriptorActorPool.destroy(); 492 } 493 this._processDescriptorActorPool = pool; 494 return [...this._processDescriptorActorPool.poolChildren()]; 495 } 496 497 onProcessListChanged() { 498 this.conn.send({ from: this.actorID, type: "processListChanged" }); 499 this._parameters.processList.onListChanged = null; 500 } 501 502 async getProcess(id) { 503 if (!DevToolsServer.allowChromeProcess) { 504 throw { 505 error: "forbidden", 506 message: "You are not allowed to debug chrome.", 507 }; 508 } 509 if (typeof id != "number") { 510 throw { 511 error: "wrongParameter", 512 message: "getProcess requires a valid `id` attribute.", 513 }; 514 } 515 this._processDescriptorActorPool = 516 this._processDescriptorActorPool || 517 new Pool(this.conn, "process-descriptors"); 518 519 let processDescriptor = this._getKnownDescriptor( 520 id, 521 this._processDescriptorActorPool 522 ); 523 if (!processDescriptor) { 524 // The parent process has id == 0, based on ProcessActorList::getList implementation 525 const options = { id, parent: id === 0 }; 526 processDescriptor = new ProcessDescriptorActor(this.conn, options); 527 this._processDescriptorActorPool.manage(processDescriptor); 528 } 529 return processDescriptor; 530 } 531 532 _getKnownDescriptor(id, pool) { 533 // if there is no pool, then we do not have any descriptors 534 if (!pool) { 535 return null; 536 } 537 for (const descriptor of pool.poolChildren()) { 538 if (descriptor.id === id) { 539 return descriptor; 540 } 541 } 542 return null; 543 } 544 545 /** 546 * Remove the extra actor (added by ActorRegistry.addGlobalActor or 547 * ActorRegistry.addTargetScopedActor) name |name|. 548 */ 549 removeActorByName(name) { 550 if (name in this._extraActors) { 551 const actor = this._extraActors[name]; 552 if (this._globalActorPool.has(actor.actorID)) { 553 actor.destroy(); 554 } 555 if (this._tabDescriptorActorPool) { 556 // Iterate over WindowGlobalTargetActor instances to also remove target-scoped 557 // actors created during listTabs for each document. 558 for (const tab in this._tabDescriptorActorPool.poolChildren()) { 559 tab.removeActorByName(name); 560 } 561 } 562 delete this._extraActors[name]; 563 } 564 } 565 566 /** 567 * Start watching for a list of resource types. 568 * 569 * See WatcherActor.watchResources. 570 */ 571 async watchResources(resourceTypes) { 572 await Resources.watchResources(this, resourceTypes); 573 } 574 575 /** 576 * Stop watching for a list of resource types. 577 * 578 * See WatcherActor.unwatchResources. 579 */ 580 unwatchResources(resourceTypes) { 581 Resources.unwatchResources(this, resourceTypes); 582 } 583 584 /** 585 * Clear resources of a list of resource types. 586 * 587 * See WatcherActor.clearResources. 588 */ 589 clearResources(resourceTypes) { 590 Resources.clearResources(this, resourceTypes); 591 } 592 593 /** 594 * Called by Resource Watchers, when new resources are available, updated or destroyed. 595 * 596 * @param String updateType 597 * Can be "available", "updated" or "destroyed" 598 * @param String resourceType 599 * The type of resources to be notified about. 600 * @param Array<json> resources 601 * List of all resources. A resource is a JSON object piped over to the client. 602 * It can contain actor IDs. 603 * It can also be or contain an actor form, to be manually marshalled by the client. 604 * (i.e. the frontend would have to manually instantiate a Front for the given actor form) 605 */ 606 notifyResources(updateType, resourceType, resources) { 607 if (resources.length === 0) { 608 // Don't try to emit if the resources array is empty. 609 return; 610 } 611 612 switch (updateType) { 613 case "available": 614 this.emit(`resources-available-array`, [[resourceType, resources]]); 615 break; 616 case "updated": 617 this.emit(`resources-updated-array`, [[resourceType, resources]]); 618 break; 619 case "destroyed": 620 this.emit(`resources-destroyed-array`, [[resourceType, resources]]); 621 break; 622 default: 623 throw new Error("Unsupported update type: " + updateType); 624 } 625 } 626 } 627 628 exports.RootActor = RootActor;