From def92d1b8e9d373e2f6f27c366d578d97d8960c6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:34:50 +0200 Subject: Merging upstream version 126.0. Signed-off-by: Daniel Baumann --- .../server/actors/accessibility/audit/contrast.js | 16 +- devtools/server/actors/blackboxing.js | 7 +- devtools/server/actors/breakpoint-list.js | 7 +- devtools/server/actors/inspector/walker.js | 4 +- devtools/server/actors/page-style.js | 1 + devtools/server/actors/resources/index.js | 16 + devtools/server/actors/resources/jstracer-state.js | 15 +- devtools/server/actors/resources/network-events.js | 8 +- devtools/server/actors/resources/sources.js | 2 + .../resources/utils/parent-process-storage.js | 11 +- devtools/server/actors/style-rule.js | 37 +- devtools/server/actors/target-configuration.js | 12 +- .../server/actors/targets/base-target-actor.js | 12 + .../targets/session-data-processors/breakpoints.js | 4 +- .../session-data-processors/event-breakpoints.js | 7 +- .../targets/session-data-processors/index.js | 7 +- .../thread-configuration.js | 7 +- .../session-data-processors/xhr-breakpoints.js | 5 +- .../actors/targets/target-actor-registry.sys.mjs | 13 +- devtools/server/actors/targets/webextension.js | 6 + devtools/server/actors/targets/window-global.js | 59 +- devtools/server/actors/targets/worker.js | 6 - devtools/server/actors/thread-configuration.js | 7 +- devtools/server/actors/thread.js | 17 +- devtools/server/actors/tracer.js | 30 +- devtools/server/actors/utils/event-breakpoints.js | 15 +- devtools/server/actors/utils/style-utils.js | 66 +- .../server/actors/utils/stylesheets-manager.js | 31 +- devtools/server/actors/watcher.js | 353 +++---- .../watcher/ParentProcessWatcherRegistry.sys.mjs | 437 ++++++++ .../server/actors/watcher/SessionDataHelpers.jsm | 244 ----- .../actors/watcher/SessionDataHelpers.sys.mjs | 218 ++++ .../server/actors/watcher/WatcherRegistry.sys.mjs | 461 --------- .../watcher/browsing-context-helpers.sys.mjs | 2 +- devtools/server/actors/watcher/moz.build | 8 +- .../content-process-jsprocessactor-startup.js | 26 - .../actors/watcher/target-helpers/frame-helper.js | 330 ------ .../server/actors/watcher/target-helpers/moz.build | 14 - .../watcher/target-helpers/process-helper.js | 115 --- .../target-helpers/service-worker-helper.js | 220 ---- .../service-worker-jsprocessactor-startup.js | 26 - .../actors/watcher/target-helpers/worker-helper.js | 137 --- .../server/actors/webconsole/commands/manager.js | 19 +- .../ContentProcessWatcherRegistry.sys.mjs | 430 ++++++++ .../js-process-actor/DevToolsProcessChild.sys.mjs | 614 ++++++----- .../js-process-actor/DevToolsProcessParent.sys.mjs | 272 +++-- .../content-process-jsprocessactor-startup.js | 33 + .../server/connectors/js-process-actor/moz.build | 6 + .../js-process-actor/target-watchers/moz.build | 12 + .../target-watchers/process.sys.mjs | 95 ++ .../target-watchers/service_worker.sys.mjs | 51 + .../target-watchers/window-global.sys.mjs | 574 +++++++++++ .../target-watchers/worker.sys.mjs | 457 ++++++++ .../js-window-actor/DevToolsFrameChild.sys.mjs | 710 ------------- .../js-window-actor/DevToolsFrameParent.sys.mjs | 277 ----- .../js-window-actor/DevToolsWorkerChild.sys.mjs | 571 ---------- .../js-window-actor/DevToolsWorkerParent.sys.mjs | 294 ------ .../js-window-actor/WindowGlobalLogger.sys.mjs | 76 -- .../server/connectors/js-window-actor/moz.build | 13 - devtools/server/connectors/moz.build | 2 - .../DevToolsServiceWorkerChild.sys.mjs | 741 ------------- .../DevToolsServiceWorkerParent.sys.mjs | 308 ------ devtools/server/connectors/process-actor/moz.build | 10 - devtools/server/connectors/worker-connector.js | 6 +- devtools/server/startup/content-process-script.js | 4 +- devtools/server/startup/worker.js | 24 +- .../tests/browser/browser_inspector-anonymous.js | 4 +- .../browser/doc_accessibility_keyboard_audit.html | 6 +- .../doc_accessibility_text_label_audit.html | 72 +- devtools/server/tests/browser/head.js | 2 + devtools/server/tests/xpcshell/head_dbg.js | 29 +- .../tests/xpcshell/test_addon_debugging_connect.js | 4 +- devtools/server/tests/xpcshell/test_getRuleText.js | 41 +- .../tests/xpcshell/test_sessionDataHelpers.js | 5 +- .../tests/xpcshell/test_xpcshell_debugging.js | 16 +- devtools/server/tests/xpcshell/testactors.js | 36 +- devtools/server/tracer/moz.build | 2 +- .../tracer/tests/browser/WorkerDebugger.tracer.js | 17 +- .../tests/browser/browser_document_tracer.js | 18 +- .../server/tracer/tests/xpcshell/test_tracer.js | 175 +++- devtools/server/tracer/tracer.jsm | 1050 ------------------- devtools/server/tracer/tracer.sys.mjs | 1088 ++++++++++++++++++++ 82 files changed, 4651 insertions(+), 6532 deletions(-) create mode 100644 devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs delete mode 100644 devtools/server/actors/watcher/SessionDataHelpers.jsm create mode 100644 devtools/server/actors/watcher/SessionDataHelpers.sys.mjs delete mode 100644 devtools/server/actors/watcher/WatcherRegistry.sys.mjs delete mode 100644 devtools/server/actors/watcher/target-helpers/content-process-jsprocessactor-startup.js delete mode 100644 devtools/server/actors/watcher/target-helpers/frame-helper.js delete mode 100644 devtools/server/actors/watcher/target-helpers/moz.build delete mode 100644 devtools/server/actors/watcher/target-helpers/process-helper.js delete mode 100644 devtools/server/actors/watcher/target-helpers/service-worker-helper.js delete mode 100644 devtools/server/actors/watcher/target-helpers/service-worker-jsprocessactor-startup.js delete mode 100644 devtools/server/actors/watcher/target-helpers/worker-helper.js create mode 100644 devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs create mode 100644 devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js create mode 100644 devtools/server/connectors/js-process-actor/target-watchers/moz.build create mode 100644 devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs create mode 100644 devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs create mode 100644 devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs create mode 100644 devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs delete mode 100644 devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs delete mode 100644 devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs delete mode 100644 devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs delete mode 100644 devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs delete mode 100644 devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs delete mode 100644 devtools/server/connectors/js-window-actor/moz.build delete mode 100644 devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs delete mode 100644 devtools/server/connectors/process-actor/DevToolsServiceWorkerParent.sys.mjs delete mode 100644 devtools/server/connectors/process-actor/moz.build delete mode 100644 devtools/server/tracer/tracer.jsm create mode 100644 devtools/server/tracer/tracer.sys.mjs (limited to 'devtools/server') diff --git a/devtools/server/actors/accessibility/audit/contrast.js b/devtools/server/actors/accessibility/audit/contrast.js index 68e7b497f8..95510be4fc 100644 --- a/devtools/server/actors/accessibility/audit/contrast.js +++ b/devtools/server/actors/accessibility/audit/contrast.js @@ -40,17 +40,19 @@ loader.lazyRequireGetter( "resource://devtools/shared/accessibility.js", true ); -loader.lazyRequireGetter( - this, - "DevToolsWorker", - "resource://devtools/shared/worker/worker.js", - true -); loader.lazyRequireGetter( this, "InspectorActorUtils", "resource://devtools/server/actors/inspector/utils.js" ); +const lazy = {}; +ChromeUtils.defineESModuleGetters( + lazy, + { + DevToolsWorker: "resource://devtools/shared/worker/worker.sys.mjs", + }, + { global: "contextual" } +); const WORKER_URL = "resource://devtools/server/actors/accessibility/worker.js"; const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted"; @@ -58,7 +60,7 @@ const { LARGE_TEXT: { BOLD_LARGE_TEXT_MIN_PIXELS, LARGE_TEXT_MIN_PIXELS }, } = require("resource://devtools/shared/accessibility.js"); -loader.lazyGetter(this, "worker", () => new DevToolsWorker(WORKER_URL)); +loader.lazyGetter(this, "worker", () => new lazy.DevToolsWorker(WORKER_URL)); /** * Get canvas rendering context for the current target window bound by the bounds of the diff --git a/devtools/server/actors/blackboxing.js b/devtools/server/actors/blackboxing.js index 49dfc8180d..8163327b46 100644 --- a/devtools/server/actors/blackboxing.js +++ b/devtools/server/actors/blackboxing.js @@ -9,9 +9,10 @@ const { blackboxingSpec, } = require("resource://devtools/shared/specs/blackboxing.js"); -const { - SessionDataHelpers, -} = require("resource://devtools/server/actors/watcher/SessionDataHelpers.jsm"); +const { SessionDataHelpers } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", + { global: "contextual" } +); const { SUPPORTED_DATA } = SessionDataHelpers; const { BLACKBOXING } = SUPPORTED_DATA; diff --git a/devtools/server/actors/breakpoint-list.js b/devtools/server/actors/breakpoint-list.js index 1f9d6c0bf9..a28ffc3f7a 100644 --- a/devtools/server/actors/breakpoint-list.js +++ b/devtools/server/actors/breakpoint-list.js @@ -9,9 +9,10 @@ const { breakpointListSpec, } = require("resource://devtools/shared/specs/breakpoint-list.js"); -const { - SessionDataHelpers, -} = require("resource://devtools/server/actors/watcher/SessionDataHelpers.jsm"); +const { SessionDataHelpers } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", + { global: "contextual" } +); const { SUPPORTED_DATA } = SessionDataHelpers; const { BREAKPOINTS, XHR_BREAKPOINTS, EVENT_BREAKPOINTS } = SUPPORTED_DATA; diff --git a/devtools/server/actors/inspector/walker.js b/devtools/server/actors/inspector/walker.js index 50df1720b7..fbf417565c 100644 --- a/devtools/server/actors/inspector/walker.js +++ b/devtools/server/actors/inspector/walker.js @@ -384,7 +384,6 @@ class WalkerActor extends Actor { this.layoutHelpers = null; this._orphaned = null; this._retainedOrphans = null; - this._nodeActorsMap = null; this.targetActor.off("will-navigate", this.onFrameUnload); this.targetActor.off("window-ready", this.onFrameLoad); @@ -433,6 +432,9 @@ class WalkerActor extends Actor { this._onEventListenerChange ); + // Only nullify some key attributes after having removed all the listeners + // as they may still be used in the related listeners. + this._nodeActorsMap = null; this.onMutations = null; this.layoutActor = null; diff --git a/devtools/server/actors/page-style.js b/devtools/server/actors/page-style.js index 1783b58a8f..b37816c85f 100644 --- a/devtools/server/actors/page-style.js +++ b/devtools/server/actors/page-style.js @@ -704,6 +704,7 @@ class PageStyleActor extends Actor { case "::first-line": case "::selection": case "::highlight": + case "::target-text": return true; case "::marker": return this._nodeIsListItem(node); diff --git a/devtools/server/actors/resources/index.js b/devtools/server/actors/resources/index.js index e2857502ad..cfc941a161 100644 --- a/devtools/server/actors/resources/index.js +++ b/devtools/server/actors/resources/index.js @@ -390,6 +390,14 @@ exports.hasResourceTypesForTargets = hasResourceTypesForTargets; * List of all type of resource to stop listening to. */ function unwatchResources(rootOrWatcherOrTargetActor, resourceTypes) { + // If we are given a target actor, filter out the resource types supported by the target. + // When using sharedData to pass types between processes, we are passing them for all target types. + const { targetType } = rootOrWatcherOrTargetActor; + // Only target actors usecase will have a target type. + // For Root and Watcher we process the `resourceTypes` list unfiltered. + if (targetType) { + resourceTypes = getResourceTypesForTargetType(resourceTypes, targetType); + } for (const resourceType of resourceTypes) { // Pull all info about this resource type from `Resources` global object const { watchers } = getResourceTypeEntry( @@ -415,6 +423,14 @@ exports.unwatchResources = unwatchResources; * List of all type of resource to clear. */ function clearResources(rootOrWatcherOrTargetActor, resourceTypes) { + // If we are given a target actor, filter out the resource types supported by the target. + // When using sharedData to pass types between processes, we are passing them for all target types. + const { targetType } = rootOrWatcherOrTargetActor; + // Only target actors usecase will have a target type. + // For Root and Watcher we process the `resourceTypes` list unfiltered. + if (targetType) { + resourceTypes = getResourceTypesForTargetType(resourceTypes, targetType); + } for (const resourceType of resourceTypes) { const { watchers } = getResourceTypeEntry( rootOrWatcherOrTargetActor, diff --git a/devtools/server/actors/resources/jstracer-state.js b/devtools/server/actors/resources/jstracer-state.js index 74491a6ced..1bb4723b55 100644 --- a/devtools/server/actors/resources/jstracer-state.js +++ b/devtools/server/actors/resources/jstracer-state.js @@ -8,13 +8,10 @@ const { TYPES: { JSTRACER_STATE }, } = require("resource://devtools/server/actors/resources/index.js"); -// Bug 1827382, as this module can be used from the worker thread, -// the following JSM may be loaded by the worker loader until -// we have proper support for ESM from workers. -const { - addTracingListener, - removeTracingListener, -} = require("resource://devtools/server/tracer/tracer.jsm"); +const { JSTracer } = ChromeUtils.importESModule( + "resource://devtools/server/tracer/tracer.sys.mjs", + { global: "contextual" } +); const { LOG_METHODS } = require("resource://devtools/server/actors/tracer.js"); const Targets = require("resource://devtools/server/actors/targets/index.js"); @@ -42,7 +39,7 @@ class TracingStateWatcher { this.tracingListener = { onTracingToggled: this.onTracingToggled.bind(this), }; - addTracingListener(this.tracingListener); + JSTracer.addTracingListener(this.tracingListener); } /** @@ -52,7 +49,7 @@ class TracingStateWatcher { if (!this.tracingListener) { return; } - removeTracingListener(this.tracingListener); + JSTracer.removeTracingListener(this.tracingListener); } /** diff --git a/devtools/server/actors/resources/network-events.js b/devtools/server/actors/resources/network-events.js index 909c16e052..9401d835ff 100644 --- a/devtools/server/actors/resources/network-events.js +++ b/devtools/server/actors/resources/network-events.js @@ -9,9 +9,9 @@ const { isWindowGlobalPartOfContext } = ChromeUtils.importESModule( "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", { global: "contextual" } ); -const { WatcherRegistry } = ChromeUtils.importESModule( - "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs", - // WatcherRegistry needs to be a true singleton and loads ActorManagerParent +const { ParentProcessWatcherRegistry } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs", + // ParentProcessWatcherRegistry needs to be a true singleton and loads ActorManagerParent // which also has to be a true singleton. { global: "shared" } ); @@ -253,7 +253,7 @@ class NetworkEventWatcher { // (i.e. the process where this Watcher runs) const isParentProcessOnlyBrowserToolbox = this.watcherActor.sessionContext.type == "all" && - !WatcherRegistry.isWatchingTargets( + !ParentProcessWatcherRegistry.isWatchingTargets( this.watcherActor, Targets.TYPES.FRAME ); diff --git a/devtools/server/actors/resources/sources.js b/devtools/server/actors/resources/sources.js index c4f0601106..63a5987e0e 100644 --- a/devtools/server/actors/resources/sources.js +++ b/devtools/server/actors/resources/sources.js @@ -44,6 +44,8 @@ class SourceWatcher { this.sourcesManager = targetActor.sourcesManager; this.onAvailable = onAvailable; + threadActor.attach({}); + // Disable `ThreadActor.newSource` RDP event in order to avoid unnecessary traffic threadActor.disableNewSourceEvents(); diff --git a/devtools/server/actors/resources/utils/parent-process-storage.js b/devtools/server/actors/resources/utils/parent-process-storage.js index 760e6e4d38..1d3a3dd341 100644 --- a/devtools/server/actors/resources/utils/parent-process-storage.js +++ b/devtools/server/actors/resources/utils/parent-process-storage.js @@ -79,11 +79,12 @@ class ParentProcessStorage { watcherActor.sessionContext; await this._spawnActor(addonBrowsingContextID, addonInnerWindowId); } else if (watcherActor.sessionContext.type == "all") { - const parentProcessTargetActor = - this.watcherActor.getTargetActorInParentProcess(); - const { browsingContextID, innerWindowId } = - parentProcessTargetActor.form(); - await this._spawnActor(browsingContextID, innerWindowId); + // Note that there should be only one such target in the browser toolbox. + // The Parent Process Target Actor. + for (const targetActor of this.watcherActor.getTargetActorsInParentProcess()) { + const { browsingContextID, innerWindowId } = targetActor.form(); + await this._spawnActor(browsingContextID, innerWindowId); + } } else { throw new Error( "Unsupported session context type=" + watcherActor.sessionContext.type diff --git a/devtools/server/actors/style-rule.js b/devtools/server/actors/style-rule.js index e9f39fa3d0..9ddd6a380c 100644 --- a/devtools/server/actors/style-rule.js +++ b/devtools/server/actors/style-rule.js @@ -724,7 +724,7 @@ class StyleRuleActor extends Actor { const cssText = await this.pageStyle.styleSheetsManager.getText( resourceId ); - const { text } = getRuleText(cssText, this.line, this.column); + const text = getRuleText(cssText, this.line, this.column); // Cache the result on the rule actor to avoid parsing again next time this._failedToGetRuleText = false; this.authoredText = text; @@ -823,19 +823,32 @@ class StyleRuleActor extends Actor { this.pageStyle.styleSheetsManager.getStyleSheetResourceId( this._parentSheet ); - let cssText = await this.pageStyle.styleSheetsManager.getText(resourceId); - const { offset, text } = getRuleText(cssText, this.line, this.column); - cssText = - cssText.substring(0, offset) + - newText + - cssText.substring(offset + text.length); - - await this.pageStyle.styleSheetsManager.setStyleSheetText( - resourceId, - cssText, - { kind: UPDATE_PRESERVING_RULES } + const sheetText = await this.pageStyle.styleSheetsManager.getText( + resourceId + ); + const cssText = InspectorUtils.replaceBlockRuleBodyTextInStylesheet( + sheetText, + this.line, + this.column, + newText ); + + if (typeof cssText !== "string") { + throw new Error( + "Error in InspectorUtils.replaceBlockRuleBodyTextInStylesheet" + ); + } + + // setStyleSheetText will parse the stylesheet which can be costly, so only do it + // if the text has actually changed. + if (sheetText !== newText) { + await this.pageStyle.styleSheetsManager.setStyleSheetText( + resourceId, + cssText, + { kind: UPDATE_PRESERVING_RULES } + ); + } } this.authoredText = newText; diff --git a/devtools/server/actors/target-configuration.js b/devtools/server/actors/target-configuration.js index b6db235143..e739c1cc3d 100644 --- a/devtools/server/actors/target-configuration.js +++ b/devtools/server/actors/target-configuration.js @@ -9,9 +9,10 @@ const { targetConfigurationSpec, } = require("resource://devtools/shared/specs/target-configuration.js"); -const { - SessionDataHelpers, -} = require("resource://devtools/server/actors/watcher/SessionDataHelpers.jsm"); +const { SessionDataHelpers } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", + { global: "contextual" } +); const { isBrowsingContextPartOfContext } = ChromeUtils.importESModule( "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", { global: "contextual" } @@ -486,7 +487,10 @@ class TargetConfigurationActor extends Actor { "bf-cache-navigation-pageshow", this._onBfCacheNavigation ); - this._restoreParentProcessConfiguration(); + // Avoid trying to restore if the related context is already being destroyed + if (this._browsingContext && !this._browsingContext.isDiscarded) { + this._restoreParentProcessConfiguration(); + } super.destroy(); } } diff --git a/devtools/server/actors/targets/base-target-actor.js b/devtools/server/actors/targets/base-target-actor.js index f3fc2a89e7..646874c4f1 100644 --- a/devtools/server/actors/targets/base-target-actor.js +++ b/devtools/server/actors/targets/base-target-actor.js @@ -203,6 +203,18 @@ class BaseTargetActor extends Actor { ) { return; } + // In the browser toolbox, when debugging the parent process, we should only toggle the tracer in the Parent Process Target Actor. + // We have to ignore any frame target which may run in the parent process. + // For example DevTools documents or a tab running in the parent process. + // (PROCESS_TYPE_DEFAULT refers to the parent process) + if ( + this.sessionContext.type == "all" && + this.targetType === Targets.TYPES.FRAME && + this.typeName != "parentProcessTarget" && + Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT + ) { + return; + } const tracerActor = this.getTargetScopedActor("tracer"); tracerActor.startTracing(options.tracerOptions); } else if (this.hasTargetScopedActor("tracer")) { diff --git a/devtools/server/actors/targets/session-data-processors/breakpoints.js b/devtools/server/actors/targets/session-data-processors/breakpoints.js index 67c270654d..8ecd80ad64 100644 --- a/devtools/server/actors/targets/session-data-processors/breakpoints.js +++ b/devtools/server/actors/targets/session-data-processors/breakpoints.js @@ -32,11 +32,9 @@ module.exports = { threadActor.removeAllBreakpoints(); } const isTargetCreation = threadActor.state == THREAD_STATES.DETACHED; - if (isTargetCreation && !targetActor.targetType.endsWith("worker")) { + if (isTargetCreation) { // If addOrSetSessionDataEntry is called during target creation, attach the // thread actor automatically and pass the initial breakpoints. - // However, do not attach the thread actor for Workers. They use a codepath - // which releases the worker on `attach`. For them, the client will call `attach`. (bug 1691986) await threadActor.attach({ breakpoints: entries }); } else { // If addOrSetSessionDataEntry is called for an existing target, set the new diff --git a/devtools/server/actors/targets/session-data-processors/event-breakpoints.js b/devtools/server/actors/targets/session-data-processors/event-breakpoints.js index 4eb9e4f3a8..1b2dbd847e 100644 --- a/devtools/server/actors/targets/session-data-processors/event-breakpoints.js +++ b/devtools/server/actors/targets/session-data-processors/event-breakpoints.js @@ -16,11 +16,8 @@ module.exports = { updateType ) { const { threadActor } = targetActor; - // Same as comments for XHR breakpoints. See lines 117-118 - if ( - threadActor.state == THREAD_STATES.DETACHED && - !targetActor.targetType.endsWith("worker") - ) { + // The thread actor has to be initialized in order to have functional breakpoints + if (threadActor.state == THREAD_STATES.DETACHED) { threadActor.attach(); } if (updateType == "set") { diff --git a/devtools/server/actors/targets/session-data-processors/index.js b/devtools/server/actors/targets/session-data-processors/index.js index 19b7d69302..72bc769dd1 100644 --- a/devtools/server/actors/targets/session-data-processors/index.js +++ b/devtools/server/actors/targets/session-data-processors/index.js @@ -4,9 +4,10 @@ "use strict"; -const { - SessionDataHelpers, -} = require("resource://devtools/server/actors/watcher/SessionDataHelpers.jsm"); +const { SessionDataHelpers } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", + { global: "contextual" } +); const { SUPPORTED_DATA } = SessionDataHelpers; const SessionDataProcessors = {}; diff --git a/devtools/server/actors/targets/session-data-processors/thread-configuration.js b/devtools/server/actors/targets/session-data-processors/thread-configuration.js index ad5c0fe024..381a62f640 100644 --- a/devtools/server/actors/targets/session-data-processors/thread-configuration.js +++ b/devtools/server/actors/targets/session-data-processors/thread-configuration.js @@ -28,12 +28,9 @@ module.exports = { threadOptions[key] = value; } - if ( - !targetActor.targetType.endsWith("worker") && - targetActor.threadActor.state == THREAD_STATES.DETACHED - ) { + if (targetActor.threadActor.state == THREAD_STATES.DETACHED) { await targetActor.threadActor.attach(threadOptions); - } else { + } else if (!targetActor.threadActor.isDestroyed()) { // Regarding `updateType`, `entries` is always a partial set of configurations. // We will acknowledge the passed attribute, but if we had set some other attributes // before this call, they will stay as-is. diff --git a/devtools/server/actors/targets/session-data-processors/xhr-breakpoints.js b/devtools/server/actors/targets/session-data-processors/xhr-breakpoints.js index 3bbcf54aaf..81ecb72fb2 100644 --- a/devtools/server/actors/targets/session-data-processors/xhr-breakpoints.js +++ b/devtools/server/actors/targets/session-data-processors/xhr-breakpoints.js @@ -22,10 +22,7 @@ module.exports = { // The thread actor has to be initialized in order to correctly // retrieve the stack trace when hitting an XHR - if ( - threadActor.state == THREAD_STATES.DETACHED && - !targetActor.targetType.endsWith("worker") - ) { + if (threadActor.state == THREAD_STATES.DETACHED) { await threadActor.attach(); } diff --git a/devtools/server/actors/targets/target-actor-registry.sys.mjs b/devtools/server/actors/targets/target-actor-registry.sys.mjs index 25c1ac1234..bc7adffec2 100644 --- a/devtools/server/actors/targets/target-actor-registry.sys.mjs +++ b/devtools/server/actors/targets/target-actor-registry.sys.mjs @@ -9,7 +9,8 @@ // are still using message manager in order to avoid being destroyed on navigation. // And because of this, these actors aren't using JS Window Actor. const windowGlobalTargetActors = new Set(); -let xpcShellTargetActor = null; + +const xpcShellTargetActors = new Set(); export var TargetActorRegistry = { registerTargetActor(targetActor) { @@ -21,15 +22,15 @@ export var TargetActorRegistry = { }, registerXpcShellTargetActor(targetActor) { - xpcShellTargetActor = targetActor; + xpcShellTargetActors.add(targetActor); }, - unregisterXpcShellTargetActor() { - xpcShellTargetActor = null; + unregisterXpcShellTargetActor(targetActor) { + xpcShellTargetActors.delete(targetActor); }, - get xpcShellTargetActor() { - return xpcShellTargetActor; + get xpcShellTargetActors() { + return xpcShellTargetActors; }, /** diff --git a/devtools/server/actors/targets/webextension.js b/devtools/server/actors/targets/webextension.js index c717b53011..47127dc65c 100644 --- a/devtools/server/actors/targets/webextension.js +++ b/devtools/server/actors/targets/webextension.js @@ -162,6 +162,12 @@ class WebExtensionTargetActor extends ParentProcessTargetActor { // URL shown in the window tittle when the addon debugger is opened). const extensionWindow = this._searchForExtensionWindow(); this.setDocShell(extensionWindow.docShell); + + // `setDocShell` will force the instantiation of the thread actor. + // We now have to initialize it in order to listen for new global + // which allows to properly detect addon reload via _shouldAddNewGlobalAsDebuggee + // which may call _onNewExtensionWindow. + this.threadActor.attach({}); } // Override the ParentProcessTargetActor's override in order to only iterate diff --git a/devtools/server/actors/targets/window-global.js b/devtools/server/actors/targets/window-global.js index 6719f0518d..f8f5e5f3c6 100644 --- a/devtools/server/actors/targets/window-global.js +++ b/devtools/server/actors/targets/window-global.js @@ -381,6 +381,15 @@ class WindowGlobalTargetActor extends BaseTargetActor { // (This is also probably meant to disappear once EFT is the only supported codepath) this._docShellsObserved = false; DevToolsUtils.executeSoon(() => this._watchDocshells()); + + // The `watchedByDevTools` enables gecko behavior tied to this flag, such as: + // - reporting the contents of HTML loaded in the docshells, + // - or capturing stacks for the network monitor. + // + // This flag can only be set on top level BrowsingContexts. + if (!this.browsingContext.parent) { + this.browsingContext.watchedByDevTools = true; + } } get docShell() { @@ -480,6 +489,10 @@ class WindowGlobalTargetActor extends BaseTargetActor { return this.browsingContext?.id; } + get innerWindowId() { + return this.window?.windowGlobalChild.innerWindowId; + } + get browserId() { return this.browsingContext?.browserId; } @@ -687,6 +700,11 @@ class WindowGlobalTargetActor extends BaseTargetActor { response.outerWindowID = this.outerWindowID; } + // If the actor is already being destroyed, avoid re-registering the target scoped actors + if (this.destroying) { + return response; + } + const actors = this._createExtraActors(); Object.assign(response, actors); @@ -731,6 +749,17 @@ class WindowGlobalTargetActor extends BaseTargetActor { this._touchSimulator = null; } + // The watchedByDevTools flag is only set on top level BrowsingContext + // (as it then cascades to all its children), + // and when destroying the target, we should tell the platform we no longer + // observe this BrowsingContext and set this attribute to false. + if ( + this.browsingContext?.watchedByDevTools && + !this.browsingContext.parent + ) { + this.browsingContext.watchedByDevTools = false; + } + // Check for `docShell` availability, as it can be already gone during // Firefox shutdown. if (this.docShell) { @@ -1314,10 +1343,6 @@ class WindowGlobalTargetActor extends BaseTargetActor { if (typeof options.touchEventsOverride !== "undefined") { const enableTouchSimulator = options.touchEventsOverride === "enabled"; - this.docShell.metaViewportOverride = enableTouchSimulator - ? Ci.nsIDocShell.META_VIEWPORT_OVERRIDE_ENABLED - : Ci.nsIDocShell.META_VIEWPORT_OVERRIDE_NONE; - // We want to reload the document if it's an "existing" top level target on which // the touch simulator will be toggled and the user has turned the // "reload on touch simulation" setting on. @@ -1384,7 +1409,14 @@ class WindowGlobalTargetActor extends BaseTargetActor { */ _restoreTargetConfiguration() { if (this._restoreFocus && this.browsingContext?.isActive) { - this.window.focus(); + try { + this.window.focus(); + } catch (e) { + // When closing devtools while navigating, focus() may throw NS_ERROR_XPC_SECURITY_MANAGER_VETO + if (e.result != Cr.NS_ERROR_XPC_SECURITY_MANAGER_VETO) { + throw e; + } + } } } @@ -1688,17 +1720,6 @@ class DebuggerProgressListener { this._knownWindowIDs.set(getWindowID(win), win); } - // The `watchedByDevTools` enables gecko behavior tied to this flag, such as: - // - reporting the contents of HTML loaded in the docshells, - // - or capturing stacks for the network monitor. - // - // This flag is also set in frame-helper but in the case of the browser toolbox, we - // don't have the watcher enabled by default yet, and as a result we need to set it - // here for the parent process window global. - // This should be removed as part of Bug 1709529. - if (this._targetActor.typeName === "parentProcessTarget") { - docShell.browsingContext.watchedByDevTools = true; - } // Immediately enable CSS error reports on new top level docshells, if this was already enabled. // This is specific to MBT and WebExtension targets (so the isRootActor check). if ( @@ -1741,12 +1762,6 @@ class DebuggerProgressListener { for (const win of windows) { this._knownWindowIDs.delete(getWindowID(win)); } - - // We only reset it for parent process target actor as the flag should be set in parent - // process, and thus is set elsewhere for other type of BrowsingContextActor. - if (this._targetActor.typeName === "parentProcessTarget") { - docShell.browsingContext.watchedByDevTools = false; - } } _getWindowsInDocShell(docShell) { diff --git a/devtools/server/actors/targets/worker.js b/devtools/server/actors/targets/worker.js index 20b60cfa24..7604b5be6e 100644 --- a/devtools/server/actors/targets/worker.js +++ b/devtools/server/actors/targets/worker.js @@ -126,12 +126,6 @@ class WorkerTargetActor extends BaseTargetActor { return this._sourcesManager; } - // This is called from the ThreadActor#onAttach method - onThreadAttached() { - // This isn't an RDP event and is only listened to from startup/worker.js. - this.emit("worker-thread-attached"); - } - destroy() { super.destroy(); diff --git a/devtools/server/actors/thread-configuration.js b/devtools/server/actors/thread-configuration.js index f0c697bb51..d3b7e229bf 100644 --- a/devtools/server/actors/thread-configuration.js +++ b/devtools/server/actors/thread-configuration.js @@ -9,9 +9,10 @@ const { threadConfigurationSpec, } = require("resource://devtools/shared/specs/thread-configuration.js"); -const { - SessionDataHelpers, -} = require("resource://devtools/server/actors/watcher/SessionDataHelpers.jsm"); +const { SessionDataHelpers } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", + { global: "contextual" } +); const { SUPPORTED_DATA: { THREAD_CONFIGURATION }, } = SessionDataHelpers; diff --git a/devtools/server/actors/thread.js b/devtools/server/actors/thread.js index 07dcc27a6a..0042e76a2a 100644 --- a/devtools/server/actors/thread.js +++ b/devtools/server/actors/thread.js @@ -418,12 +418,6 @@ class ThreadActor extends Actor { this.alreadyAttached = true; this.dbg.enable(); - // Notify the target actor that we've finished attaching. If this is a worker - // thread which was paused until attaching, this will allow content to - // begin executing. - if (this.targetActor.onThreadAttached) { - this.targetActor.onThreadAttached(); - } if (Services.obs) { // Set a wrappedJSObject property so |this| can be sent via the observer service // for the xpcshell harness. @@ -535,6 +529,13 @@ class ThreadActor extends Actor { } async setBreakpoint(location, options) { + // Automatically initialize the thread actor if it wasn't yet done. + // Note that ideally, it should rather be done via reconfigure/thread configuration. + if (this._state === STATES.DETACHED) { + this.attach({}); + this.addAllSources(); + } + let actor = this.breakpointActorMap.get(location); // Avoid resetting the exact same breakpoint twice if (actor && JSON.stringify(actor.options) == JSON.stringify(options)) { @@ -597,7 +598,9 @@ class ThreadActor extends Actor { } getAvailableEventBreakpoints() { - return getAvailableEventBreakpoints(this.targetActor.window); + return getAvailableEventBreakpoints( + this.targetActor.window || this.targetActor.workerGlobal + ); } getActiveEventBreakpoints() { return Array.from(this._activeEventBreakpoints); diff --git a/devtools/server/actors/tracer.js b/devtools/server/actors/tracer.js index bf759cee5f..d98749ceb1 100644 --- a/devtools/server/actors/tracer.js +++ b/devtools/server/actors/tracer.js @@ -4,16 +4,14 @@ "use strict"; -// Bug 1827382, as this module can be used from the worker thread, -// the following JSM may be loaded by the worker loader until -// we have proper support for ESM from workers. -const { - startTracing, - stopTracing, - addTracingListener, - removeTracingListener, - NEXT_INTERACTION_MESSAGE, -} = require("resource://devtools/server/tracer/tracer.jsm"); +const lazy = {}; +ChromeUtils.defineESModuleGetters( + lazy, + { + JSTracer: "resource://devtools/server/tracer/tracer.sys.mjs", + }, + { global: "contextual" } +); const { Actor } = require("resource://devtools/shared/protocol.js"); const { tracerSpec } = require("resource://devtools/shared/specs/tracer.js"); @@ -136,10 +134,10 @@ class TracerActor extends Actor { onTracingPending: this.onTracingPending.bind(this), onTracingDOMMutation: this.onTracingDOMMutation.bind(this), }; - addTracingListener(this.tracingListener); + lazy.JSTracer.addTracingListener(this.tracingListener); this.traceValues = !!options.traceValues; try { - startTracing({ + lazy.JSTracer.startTracing({ global: this.targetActor.window || this.targetActor.workerGlobal, prefix: options.prefix || "", // Enable receiving the `currentDOMEvent` being passed to `onTracingFrame` @@ -170,10 +168,10 @@ class TracerActor extends Actor { return; } // Remove before stopping to prevent receiving the stop notification - removeTracingListener(this.tracingListener); + lazy.JSTracer.removeTracingListener(this.tracingListener); this.tracingListener = null; - stopTracing(); + lazy.JSTracer.stopTracing(); this.logMethod = null; } @@ -230,7 +228,7 @@ class TracerActor extends Actor { if (consoleMessageWatcher) { consoleMessageWatcher.emitMessages([ { - arguments: [NEXT_INTERACTION_MESSAGE], + arguments: [lazy.JSTracer.NEXT_INTERACTION_MESSAGE], styles: [], level: "jstracer", chromeContext: false, @@ -510,7 +508,7 @@ class TracerActor extends Actor { * A string to be displayed as a prefix of any logged frame. * @param {String} options.why * A string to explain why the function stopped. - * See tracer.jsm's FRAME_EXIT_REASONS. + * See tracer.sys.mjs's FRAME_EXIT_REASONS. * @param {Debugger.Object|primitive} options.rv * The returned value. It can be the returned value, or the thrown exception. * It is either a primitive object, otherwise it is a Debugger.Object for any other JS Object type. diff --git a/devtools/server/actors/utils/event-breakpoints.js b/devtools/server/actors/utils/event-breakpoints.js index a7752b8201..8fbefec804 100644 --- a/devtools/server/actors/utils/event-breakpoints.js +++ b/devtools/server/actors/utils/event-breakpoints.js @@ -131,7 +131,8 @@ const AVAILABLE_BREAKPOINTS = [ items: [ // The condition should be removed when "dom.element.popover.enabled" is removed generalEvent("control", "beforetoggle", () => - Services.prefs.getBoolPref("dom.element.popover.enabled") + // Services.prefs isn't available on worker targets + Services.prefs?.getBoolPref("dom.element.popover.enabled") ), generalEvent("control", "blur"), generalEvent("control", "change"), @@ -139,7 +140,11 @@ const AVAILABLE_BREAKPOINTS = [ generalEvent("control", "focusin"), generalEvent("control", "focusout"), // The condition should be removed when "dom.element.invokers.enabled" is removed - generalEvent("control", "invoke", win => "InvokeEvent" in win), + generalEvent( + "control", + "invoke", + global => global && "InvokeEvent" in global + ), generalEvent("control", "reset"), generalEvent("control", "resize"), generalEvent("control", "scroll"), @@ -483,17 +488,17 @@ exports.getAvailableEventBreakpoints = getAvailableEventBreakpoints; /** * Get all available event breakpoints * - * @param {Window} window + * @param {Window|WorkerGlobalScope} global * @returns {Array} An array containing object with 2 properties, an id and a name, * representing the event. */ -function getAvailableEventBreakpoints(window) { +function getAvailableEventBreakpoints(global) { const available = []; for (const { name, items } of AVAILABLE_BREAKPOINTS) { available.push({ name, events: items - .filter(item => !item.condition || item.condition(window)) + .filter(item => !item.condition || item.condition(global)) .map(item => ({ id: item.id, name: item.name, diff --git a/devtools/server/actors/utils/style-utils.js b/devtools/server/actors/utils/style-utils.js index 5f2e912002..1d52448fb6 100644 --- a/devtools/server/actors/utils/style-utils.js +++ b/devtools/server/actors/utils/style-utils.js @@ -4,8 +4,6 @@ "use strict"; -const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js"); - const XHTML_NS = "http://www.w3.org/1999/xhtml"; const FONT_PREVIEW_TEXT = "Abc"; const FONT_PREVIEW_FONT_SIZE = 40; @@ -120,66 +118,12 @@ function getRuleText(initialText, line, column) { throw new Error("Location information is missing"); } - const { offset: textOffset, text } = getTextAtLineColumn( - initialText, - line, - column - ); - const lexer = getCSSLexer(text); - - // Search forward for the opening brace. - while (true) { - const token = lexer.nextToken(); - if (!token) { - throw new Error("couldn't find start of the rule"); - } - if (token.tokenType === "symbol" && token.text === "{") { - break; - } - } - - // Now collect text until we see the matching close brace. - let braceDepth = 1; - let startOffset, endOffset; - while (true) { - const token = lexer.nextToken(); - if (!token) { - break; - } - if (startOffset === undefined) { - startOffset = token.startOffset; - } - if (token.tokenType === "symbol") { - if (token.text === "{") { - ++braceDepth; - } else if (token.text === "}") { - --braceDepth; - if (braceDepth == 0) { - break; - } - } - } - endOffset = token.endOffset; - } - - // If the rule was of the form "selector {" with no closing brace - // and no properties, just return an empty string. - if (startOffset === undefined) { - return { offset: 0, text: "" }; - } - // If the input didn't have any tokens between the braces (e.g., - // "div {}"), then the endOffset won't have been set yet; so account - // for that here. - if (endOffset === undefined) { - endOffset = startOffset; + const { text } = getTextAtLineColumn(initialText, line, column); + const res = InspectorUtils.getRuleBodyText(text); + if (res === null || typeof res === "undefined") { + throw new Error("Couldn't find rule"); } - - // Note that this approach will preserve comments, despite the fact - // that cssTokenizer skips them. - return { - offset: textOffset + startOffset, - text: text.substring(startOffset, endOffset), - }; + return res; } exports.getRuleText = getRuleText; diff --git a/devtools/server/actors/utils/stylesheets-manager.js b/devtools/server/actors/utils/stylesheets-manager.js index a9c0705e8d..1c065afd4e 100644 --- a/devtools/server/actors/utils/stylesheets-manager.js +++ b/devtools/server/actors/utils/stylesheets-manager.js @@ -446,10 +446,12 @@ class StyleSheetsManager extends EventEmitter { InspectorUtils.parseStyleSheet(styleSheet, text); modifiedStyleSheets.set(styleSheet, text); - const { atRules, ruleCount } = - this.getStyleSheetRuleCountAndAtRules(styleSheet); - + // getStyleSheetRuleCountAndAtRules can be costly, so only call it when needed, + // i.e. when the whole stylesheet is modified, not when a rule body is. + let atRules, ruleCount; if (kind !== UPDATE_PRESERVING_RULES) { + ({ atRules, ruleCount } = + this.getStyleSheetRuleCountAndAtRules(styleSheet)); this.#notifyPropertyChanged(resourceId, "ruleCount", ruleCount); } @@ -465,13 +467,15 @@ class StyleSheetsManager extends EventEmitter { }); } - this.#onStyleSheetUpdated({ - resourceId, - updateKind: "at-rules-changed", - updates: { - resourceUpdates: { atRules }, - }, - }); + if (kind !== UPDATE_PRESERVING_RULES) { + this.#onStyleSheetUpdated({ + resourceId, + updateKind: "at-rules-changed", + updates: { + resourceUpdates: { atRules }, + }, + }); + } } /** @@ -705,6 +709,13 @@ class StyleSheetsManager extends EventEmitter { line: InspectorUtils.getRelativeRuleLine(rule), column: InspectorUtils.getRuleColumn(rule), }); + } else if (className === "CSSPropertyRule") { + atRules.push({ + type: "property", + propertyName: rule.name, + line: InspectorUtils.getRelativeRuleLine(rule), + column: InspectorUtils.getRuleColumn(rule), + }); } } return { diff --git a/devtools/server/actors/watcher.js b/devtools/server/actors/watcher.js index 935d33faa8..10de102229 100644 --- a/devtools/server/actors/watcher.js +++ b/devtools/server/actors/watcher.js @@ -11,13 +11,12 @@ const { TargetActorRegistry } = ChromeUtils.importESModule( "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs", { global: "shared" } ); -const { WatcherRegistry } = ChromeUtils.importESModule( - "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs", - // WatcherRegistry needs to be a true singleton and loads ActorManagerParent +const { ParentProcessWatcherRegistry } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs", + // ParentProcessWatcherRegistry needs to be a true singleton and loads ActorManagerParent // which also has to be a true singleton. { global: "shared" } ); -const Targets = require("resource://devtools/server/actors/targets/index.js"); const { getAllBrowsingContextsForContext } = ChromeUtils.importESModule( "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", { global: "contextual" } @@ -26,28 +25,6 @@ const { SESSION_TYPES, } = require("resource://devtools/server/actors/watcher/session-context.js"); -const TARGET_HELPERS = {}; -loader.lazyRequireGetter( - TARGET_HELPERS, - Targets.TYPES.FRAME, - "resource://devtools/server/actors/watcher/target-helpers/frame-helper.js" -); -loader.lazyRequireGetter( - TARGET_HELPERS, - Targets.TYPES.PROCESS, - "resource://devtools/server/actors/watcher/target-helpers/process-helper.js" -); -loader.lazyRequireGetter( - TARGET_HELPERS, - Targets.TYPES.SERVICE_WORKER, - "devtools/server/actors/watcher/target-helpers/service-worker-helper" -); -loader.lazyRequireGetter( - TARGET_HELPERS, - Targets.TYPES.WORKER, - "resource://devtools/server/actors/watcher/target-helpers/worker-helper.js" -); - loader.lazyRequireGetter( this, "NetworkParentActor", @@ -137,6 +114,14 @@ exports.WatcherActor = class WatcherActor extends Actor { // but there are certain cases when a new target is available before the // old target is destroyed. this._currentWindowGlobalTargets = new Map(); + + // The Browser Toolbox requires to load modules in a distinct compartment in order + // to be able to debug system compartments modules (most of Firefox internal codebase). + // This is a requirement coming from SpiderMonkey Debugger API and relates to the thread actor. + this._jsActorName = + sessionContext.type == SESSION_TYPES.ALL + ? "BrowserToolboxDevToolsProcess" + : "DevToolsProcess"; } get sessionContext() { @@ -176,16 +161,33 @@ exports.WatcherActor = class WatcherActor extends Actor { } destroy() { - // Force unwatching for all types, even if we weren't watching. - // This is fine as unwatchTarget is NOOP if we weren't already watching for this target type. - for (const targetType of Object.values(Targets.TYPES)) { - this.unwatchTargets(targetType); + // Only try to notify content processes if the watcher was in the registry. + // Otherwise it means that it wasn't connected to any process and the JS Process Actor + // wouldn't be registered. + if (ParentProcessWatcherRegistry.getWatcher(this.actorID)) { + // Emit one IPC message on destroy to all the processes + const domProcesses = ChromeUtils.getAllDOMProcesses(); + for (const domProcess of domProcesses) { + domProcess.getActor(this._jsActorName).destroyWatcher({ + watcherActorID: this.actorID, + }); + } } - this.unwatchResources(Object.values(Resources.TYPES)); - WatcherRegistry.unregisterWatcher(this); + // Ensure destroying all Resource Watcher instantiated in the parent process + Resources.unwatchResources( + this, + Resources.getParentProcessResourceTypes(Object.values(Resources.TYPES)) + ); + + ParentProcessWatcherRegistry.unregisterWatcher(this.actorID); - // Destroy the actor at the end so that its actorID keeps being defined. + // In case the watcher actor is leaked, prevent leaking the browser window + this._browserElement = null; + + // Destroy the actor in order to ensure destroying all its children actors. + // As this actor is a pool with children actors, when the transport/connection closes + // we expect all actors and its children to be destroyed. super.destroy(); } @@ -196,7 +198,7 @@ exports.WatcherActor = class WatcherActor extends Actor { * Returns the list of currently watched resource types. */ get sessionData() { - return WatcherRegistry.getSessionData(this); + return ParentProcessWatcherRegistry.getSessionData(this); } form() { @@ -225,11 +227,44 @@ exports.WatcherActor = class WatcherActor extends Actor { * Type of context to observe. See Targets.TYPES object. */ async watchTargets(targetType) { - WatcherRegistry.watchTargets(this, targetType); + ParentProcessWatcherRegistry.watchTargets(this, targetType); + + // When debugging a tab, ensure processing the top level target first + // (for now, other session context types are instantiating the top level target + // from the descriptor's getTarget method instead of the Watcher) + let topLevelTargetProcess; + if (this.sessionContext.type == SESSION_TYPES.BROWSER_ELEMENT) { + topLevelTargetProcess = + this.browserElement.browsingContext.currentWindowGlobal?.domProcess; + if (topLevelTargetProcess) { + await topLevelTargetProcess.getActor(this._jsActorName).watchTargets({ + watcherActorID: this.actorID, + targetType, + }); + // Stop execution if we were destroyed in the meantime + if (this.isDestroyed()) { + return; + } + } + } - const targetHelperModule = TARGET_HELPERS[targetType]; - // Await the registration in order to ensure receiving the already existing targets - await targetHelperModule.createTargets(this); + // We have to reach out all the content processes as the page may navigate + // to any other content process when navigating to another origin. + // It may even run in the parent process when loading about:robots. + const domProcesses = ChromeUtils.getAllDOMProcesses(); + const promises = []; + for (const domProcess of domProcesses) { + if (domProcess == topLevelTargetProcess) { + continue; + } + promises.push( + domProcess.getActor(this._jsActorName).watchTargets({ + watcherActorID: this.actorID, + targetType, + }) + ); + } + await Promise.all(promises); } /** @@ -242,7 +277,7 @@ exports.WatcherActor = class WatcherActor extends Actor { * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref */ unwatchTargets(targetType, options = {}) { - const isWatchingTargets = WatcherRegistry.unwatchTargets( + const isWatchingTargets = ParentProcessWatcherRegistry.unwatchTargets( this, targetType, options @@ -251,14 +286,20 @@ exports.WatcherActor = class WatcherActor extends Actor { return; } - const targetHelperModule = TARGET_HELPERS[targetType]; - targetHelperModule.destroyTargets(this, options); + const domProcesses = ChromeUtils.getAllDOMProcesses(); + for (const domProcess of domProcesses) { + domProcess.getActor(this._jsActorName).unwatchTargets({ + watcherActorID: this.actorID, + targetType, + options, + }); + } // Unregister the JS Actors if there is no more DevTools code observing any target/resource, // unless we're switching mode (having both condition at the same time should only // happen in tests). if (!options.isModeSwitching) { - WatcherRegistry.maybeUnregisterJSActors(); + ParentProcessWatcherRegistry.maybeUnregisterJSActors(); } } @@ -301,7 +342,12 @@ exports.WatcherActor = class WatcherActor extends Actor { this._flushIframeTargets(actor.innerWindowId); if (this.sessionContext.type == SESSION_TYPES.BROWSER_ELEMENT) { - this.updateDomainSessionDataForServiceWorkers(actor.url); + // Ignore any pending exception as this request may be pending + // while the toolbox closes. And we don't want to delay target emission + // on this as this is a implementation detail. + this.updateDomainSessionDataForServiceWorkers(actor.url).catch( + () => {} + ); } } else if (this._currentWindowGlobalTargets.has(actor.topInnerWindowId)) { // Emit the event immediately if the top-level target is already available @@ -444,18 +490,18 @@ exports.WatcherActor = class WatcherActor extends Actor { } /** - * Try to retrieve a parent process TargetActor which is ignored by the - * TARGET_HELPERS. Examples: - * - top level target for the browser toolbox - * - xpcshell target for xpcshell debugging + * Try to retrieve Target Actors instantiated in the parent process which aren't + * instantiated via the Watcher actor (and its dependencies): + * - top level target for the browser toolboxes + * - xpcshell targets for xpcshell debugging * * See comment in `watchResources`. * - * @return {TargetActor|null} Matching target actor if any, null otherwise. + * @return {Set} Matching target actors. */ - getTargetActorInParentProcess() { - if (TargetActorRegistry.xpcShellTargetActor) { - return TargetActorRegistry.xpcShellTargetActor; + getTargetActorsInParentProcess() { + if (TargetActorRegistry.xpcShellTargetActors.size) { + return TargetActorRegistry.xpcShellTargetActors; } // Note: For browser-element debugging, the WindowGlobalTargetActor returned here is created @@ -467,12 +513,18 @@ exports.WatcherActor = class WatcherActor extends Actor { switch (this.sessionContext.type) { case "all": - return actors.find(actor => actor.typeName === "parentProcessTarget"); + const parentProcessTargetActor = actors.find( + actor => actor.typeName === "parentProcessTarget" + ); + if (parentProcessTargetActor) { + return new Set([parentProcessTargetActor]); + } + return new Set(); case "browser-element": case "webextension": // All target actors for browser-element and webextension sessions // should be created using the JS Window actors. - return null; + return new Set(); default: throw new Error( "Unsupported session context type: " + this.sessionContext.type @@ -497,41 +549,32 @@ exports.WatcherActor = class WatcherActor extends Actor { ); // Bail out early if all resources were watched from parent process. - // In this scenario, we do not need to update these resource types in the WatcherRegistry + // In this scenario, we do not need to update these resource types in the ParentProcessWatcherRegistry // as targets do not care about them. if (!Resources.hasResourceTypesForTargets(resourceTypes)) { return; } - WatcherRegistry.watchResources(this, resourceTypes); + ParentProcessWatcherRegistry.watchResources(this, resourceTypes); - // Fetch resources from all existing targets - for (const targetType in TARGET_HELPERS) { - // We process frame targets even if we aren't watching them, - // because frame target helper codepath handles the top level target, if it runs in the *content* process. - // It will do another check to `isWatchingTargets(FRAME)` internally. - // Note that the workaround at the end of this method, using TargetActorRegistry - // is specific to top level target running in the *parent* process. - if ( - !WatcherRegistry.isWatchingTargets(this, targetType) && - targetType != Targets.TYPES.FRAME - ) { - continue; - } - const targetResourceTypes = Resources.getResourceTypesForTargetType( - resourceTypes, - targetType + const promises = []; + const domProcesses = ChromeUtils.getAllDOMProcesses(); + for (const domProcess of domProcesses) { + promises.push( + domProcess.getActor(this._jsActorName).addOrSetSessionDataEntry({ + watcherActorID: this.actorID, + sessionContext: this.sessionContext, + type: "resources", + entries: resourceTypes, + updateType: "add", + }) ); - if (!targetResourceTypes.length) { - continue; - } - const targetHelperModule = TARGET_HELPERS[targetType]; - await targetHelperModule.addOrSetSessionDataEntry({ - watcher: this, - type: "resources", - entries: targetResourceTypes, - updateType: "add", - }); + } + await Promise.all(promises); + + // Stop execution if we were destroyed in the meantime + if (this.isDestroyed()) { + return; } /* @@ -551,8 +594,8 @@ exports.WatcherActor = class WatcherActor extends Actor { * We will eventually get rid of this code once all targets are properly supported by * the Watcher Actor and we have target helpers for all of them. */ - const targetActor = this.getTargetActorInParentProcess(); - if (targetActor) { + const targetActors = this.getTargetActorsInParentProcess(); + for (const targetActor of targetActors) { const targetActorResourceTypes = Resources.getResourceTypesForTargetType( resourceTypes, targetActor.targetType @@ -581,13 +624,13 @@ exports.WatcherActor = class WatcherActor extends Actor { ); // Bail out early if all resources were all watched from parent process. - // In this scenario, we do not need to update these resource types in the WatcherRegistry + // In this scenario, we do not need to update these resource types in the ParentProcessWatcherRegistry // as targets do not care about them. if (!Resources.hasResourceTypesForTargets(resourceTypes)) { return; } - const isWatchingResources = WatcherRegistry.unwatchResources( + const isWatchingResources = ParentProcessWatcherRegistry.unwatchResources( this, resourceTypes ); @@ -598,34 +641,20 @@ exports.WatcherActor = class WatcherActor extends Actor { // Prevent trying to unwatch when the related BrowsingContext has already // been destroyed if (!this.isContextDestroyed()) { - for (const targetType in TARGET_HELPERS) { - // Frame target helper handles the top level target, if it runs in the content process - // so we should always process it. It does a second check to isWatchingTargets. - if ( - !WatcherRegistry.isWatchingTargets(this, targetType) && - targetType != Targets.TYPES.FRAME - ) { - continue; - } - const targetResourceTypes = Resources.getResourceTypesForTargetType( - resourceTypes, - targetType - ); - if (!targetResourceTypes.length) { - continue; - } - const targetHelperModule = TARGET_HELPERS[targetType]; - targetHelperModule.removeSessionDataEntry({ - watcher: this, + const domProcesses = ChromeUtils.getAllDOMProcesses(); + for (const domProcess of domProcesses) { + domProcess.getActor(this._jsActorName).removeSessionDataEntry({ + watcherActorID: this.actorID, + sessionContext: this.sessionContext, type: "resources", - entries: targetResourceTypes, + entries: resourceTypes, }); } } // See comment in watchResources. - const targetActor = this.getTargetActorInParentProcess(); - if (targetActor) { + const targetActors = this.getTargetActorsInParentProcess(); + for (const targetActor of targetActors) { const targetActorResourceTypes = Resources.getResourceTypesForTargetType( resourceTypes, targetActor.targetType @@ -634,7 +663,7 @@ exports.WatcherActor = class WatcherActor extends Actor { } // Unregister the JS Window Actor if there is no more DevTools code observing any target/resource - WatcherRegistry.maybeUnregisterJSActors(); + ParentProcessWatcherRegistry.maybeUnregisterJSActors(); } clearResources(resourceTypes) { @@ -729,34 +758,36 @@ exports.WatcherActor = class WatcherActor extends Actor { * "set" will update the data set with the new entries. */ async addOrSetDataEntry(type, entries, updateType) { - WatcherRegistry.addOrSetSessionDataEntry(this, type, entries, updateType); - - await Promise.all( - Object.values(Targets.TYPES) - .filter( - targetType => - // We process frame targets even if we aren't watching them, - // because frame target helper codepath handles the top level target, if it runs in the *content* process. - // It will do another check to `isWatchingTargets(FRAME)` internally. - // Note that the workaround at the end of this method, using TargetActorRegistry - // is specific to top level target running in the *parent* process. - WatcherRegistry.isWatchingTargets(this, targetType) || - targetType === Targets.TYPES.FRAME - ) - .map(async targetType => { - const targetHelperModule = TARGET_HELPERS[targetType]; - await targetHelperModule.addOrSetSessionDataEntry({ - watcher: this, - type, - entries, - updateType, - }); - }) + ParentProcessWatcherRegistry.addOrSetSessionDataEntry( + this, + type, + entries, + updateType ); + const promises = []; + const domProcesses = ChromeUtils.getAllDOMProcesses(); + for (const domProcess of domProcesses) { + promises.push( + domProcess.getActor(this._jsActorName).addOrSetSessionDataEntry({ + watcherActorID: this.actorID, + sessionContext: this.sessionContext, + type, + entries, + updateType, + }) + ); + } + await Promise.all(promises); + + // Stop execution if we were destroyed in the meantime + if (this.isDestroyed()) { + return; + } + // See comment in watchResources - const targetActor = this.getTargetActorInParentProcess(); - if (targetActor) { + const targetActors = this.getTargetActorsInParentProcess(); + for (const targetActor of targetActors) { await targetActor.addOrSetSessionDataEntry( type, entries, @@ -777,27 +808,21 @@ exports.WatcherActor = class WatcherActor extends Actor { * List of values to remove from this data type. */ removeDataEntry(type, entries) { - WatcherRegistry.removeSessionDataEntry(this, type, entries); - - Object.values(Targets.TYPES) - .filter( - targetType => - // See comment in addOrSetDataEntry - WatcherRegistry.isWatchingTargets(this, targetType) || - targetType === Targets.TYPES.FRAME - ) - .forEach(targetType => { - const targetHelperModule = TARGET_HELPERS[targetType]; - targetHelperModule.removeSessionDataEntry({ - watcher: this, - type, - entries, - }); + ParentProcessWatcherRegistry.removeSessionDataEntry(this, type, entries); + + const domProcesses = ChromeUtils.getAllDOMProcesses(); + for (const domProcess of domProcesses) { + domProcess.getActor(this._jsActorName).removeSessionDataEntry({ + watcherActorID: this.actorID, + sessionContext: this.sessionContext, + type, + entries, }); + } // See comment in addOrSetDataEntry - const targetActor = this.getTargetActorInParentProcess(); - if (targetActor) { + const targetActors = this.getTargetActorsInParentProcess(); + for (const targetActor of targetActors) { targetActor.removeSessionDataEntry(type, entries); } } @@ -827,35 +852,13 @@ exports.WatcherActor = class WatcherActor extends Actor { host = new URL(newTargetUrl).host; } catch (e) {} - WatcherRegistry.addOrSetSessionDataEntry( + ParentProcessWatcherRegistry.addOrSetSessionDataEntry( this, "browser-element-host", [host], "set" ); - // This SessionData attribute is only used when debugging service workers. - // Avoid instantiating the JS Process Actors if we aren't watching for SW, - // or if we aren't watching for them just yet. - // But still update the WatcherRegistry, so that when we start watching - // and instantiate the target, the host will be set to the right value. - // - // Note that it is very important to avoid calling Service worker target helper's - // addOrSetSessionDataEntry. Otherwise, when we aren't watching for SW at all, - // we won't call destroyTargets on watcher actor destruction, - // and as a consequence never unregister the js process actor. - if ( - !WatcherRegistry.isWatchingTargets(this, Targets.TYPES.SERVICE_WORKER) - ) { - return; - } - - const targetHelperModule = TARGET_HELPERS[Targets.TYPES.SERVICE_WORKER]; - await targetHelperModule.addOrSetSessionDataEntry({ - watcher: this, - type: "browser-element-host", - entries: [host], - updateType: "set", - }); + return this.addOrSetDataEntry("browser-element-host", [host], "set"); } }; diff --git a/devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs b/devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs new file mode 100644 index 0000000000..e9b3a9d50d --- /dev/null +++ b/devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs @@ -0,0 +1,437 @@ +/* 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/. */ + +/** + * Helper module around `sharedData` object that helps storing the state + * of all observed Targets and Resources, that, for all DevTools connections. + * Here is a few words about the C++ implementation of sharedData: + * https://searchfox.org/mozilla-central/rev/bc3600def806859c31b2c7ac06e3d69271052a89/dom/ipc/SharedMap.h#30-55 + * + * We may have more than one DevToolsServer and one server may have more than one + * client. This module will be the single source of truth in the parent process, + * in order to know which targets/resources are currently observed. It will also + * be used to declare when something starts/stops being observed. + * + * `sharedData` is a platform API that helps sharing JS Objects across processes. + * We use it in order to communicate to the content process which targets and resources + * should be observed. Content processes read this data only once, as soon as they are created. + * It isn't used beyond this point. Content processes are not going to update it. + * We will notify about changes in observed targets and resources for already running + * processes by some other means. (Via JS Window Actor queries "DevTools:(un)watch(Resources|Target)") + * This means that only this module will update the "DevTools:watchedPerWatcher" value. + * From the parent process, we should be going through this module to fetch the data, + * while from the content process, we will read `sharedData` directly. + */ + +const { SessionDataHelpers } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", + { global: "contextual" } +); + +const { SUPPORTED_DATA } = SessionDataHelpers; +const SUPPORTED_DATA_TYPES = Object.values(SUPPORTED_DATA); + +// Define the Map that will be saved in `sharedData`. +// It is keyed by WatcherActor ID and values contains following attributes: +// - targets: Set of strings, refering to target types to be listened to +// - resources: Set of strings, refering to resource types to be observed +// - sessionContext Object, The Session Context to help know what is debugged. +// See devtools/server/actors/watcher/session-context.js +// - connectionPrefix: The DevToolsConnection prefix of the watcher actor. Used to compute new actor ID in the content processes. +// +// Unfortunately, `sharedData` is subject to race condition and may have side effect +// when read/written from multiple places in the same process, +// which is why this map should be considered as the single source of truth. +const sessionDataByWatcherActor = new Map(); + +// In parallel to the previous map, keep all the WatcherActor keyed by the same WatcherActor ID, +// the WatcherActor ID. We don't (and can't) propagate the WatcherActor instances to the content +// processes, but still would like to match them by their ID. +const watcherActors = new Map(); + +// Name of the attribute into which we save this Map in `sharedData` object. +const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; + +/** + * Use `sharedData` to allow processes, early during their creation, + * to know which resources should be listened to. This will be read + * from the Target actor, when it gets created early during process start, + * in order to start listening to the expected resource types. + */ +function persistMapToSharedData() { + Services.ppmm.sharedData.set(SHARED_DATA_KEY_NAME, sessionDataByWatcherActor); + // Request to immediately flush the data to the content processes in order to prevent + // races (bug 1644649). Otherwise content process may have outdated sharedData + // and try to create targets for Watcher actor that already stopped watching for targets. + Services.ppmm.sharedData.flush(); +} + +export const ParentProcessWatcherRegistry = { + /** + * Tells if a given watcher currently watches for a given target type. + * + * @param WatcherActor watcher + * The WatcherActor which should be listening. + * @param string targetType + * The new target type to query. + * @return boolean + * Returns true if already watching. + */ + isWatchingTargets(watcher, targetType) { + const sessionData = this.getSessionData(watcher); + return !!sessionData?.targets?.includes(targetType); + }, + + /** + * Retrieve the data saved into `sharedData` that is used to know + * about which type of targets and resources we care listening about. + * `sessionDataByWatcherActor` is saved into `sharedData` after each mutation, + * but `sessionDataByWatcherActor` is the source of truth. + * + * @param WatcherActor watcher + * The related WatcherActor which starts/stops observing. + * @param object options (optional) + * A dictionary object with `createData` boolean attribute. + * If this attribute is set to true, we create the data structure in the Map + * if none exists for this prefix. + */ + getSessionData(watcher, { createData = false } = {}) { + // Use WatcherActor ID as a key as we may have multiple clients willing to watch for targets. + // For example, a Browser Toolbox debugging everything and a Content Toolbox debugging + // just one tab. We might also have multiple watchers, on the same connection when using about:debugging. + const watcherActorID = watcher.actorID; + let sessionData = sessionDataByWatcherActor.get(watcherActorID); + if (!sessionData && createData) { + sessionData = { + // The "session context" object help understand what should be debugged and which target should be created. + // See WatcherActor constructor for more info. + sessionContext: watcher.sessionContext, + // The DevToolsServerConnection prefix will be used to compute actor IDs created in the content process + connectionPrefix: watcher.conn.prefix, + }; + sessionDataByWatcherActor.set(watcherActorID, sessionData); + watcherActors.set(watcherActorID, watcher); + } + return sessionData; + }, + + /** + * Given a Watcher Actor ID, return the related Watcher Actor instance. + * + * @param String actorID + * The Watcher Actor ID to search for. + * @return WatcherActor + * The Watcher Actor instance. + */ + getWatcher(actorID) { + return watcherActors.get(actorID); + }, + + /** + * Return an array of the watcher actors that match the passed browserId + * + * @param {Number} browserId + * @returns {Array} An array of the matching watcher actors + */ + getWatchersForBrowserId(browserId) { + const watchers = []; + for (const watcherActor of watcherActors.values()) { + if ( + watcherActor.sessionContext.type == "browser-element" && + watcherActor.sessionContext.browserId === browserId + ) { + watchers.push(watcherActor); + } + } + + return watchers; + }, + + /** + * Notify that a given watcher added or set some entries for given data type. + * + * @param WatcherActor watcher + * The WatcherActor which starts observing. + * @param string type + * The type of data to be added + * @param Array entries + * The values to be added to this type of data + * @param String updateType + * "add" will only add the new entries in the existing data set. + * "set" will update the data set with the new entries. + */ + addOrSetSessionDataEntry(watcher, type, entries, updateType) { + const sessionData = this.getSessionData(watcher, { + createData: true, + }); + + if (!SUPPORTED_DATA_TYPES.includes(type)) { + throw new Error(`Unsupported session data type: ${type}`); + } + + SessionDataHelpers.addOrSetSessionDataEntry( + sessionData, + type, + entries, + updateType + ); + + // Flush sharedData before registering the JS Actors as it is used + // during their instantiation. + persistMapToSharedData(); + + // Register the JS Window Actor the first time we start watching for something (e.g. resource, target, …). + if (watcher.sessionContext.type == "all") { + registerBrowserToolboxJSProcessActor(); + } else { + registerJSProcessActor(); + } + }, + + /** + * Notify that a given watcher removed an entry in a given data type. + * + * @param WatcherActor watcher + * The WatcherActor which stops observing. + * @param string type + * The type of data to be removed + * @param Array entries + * The values to be removed to this type of data + * @params {Object} options + * @params {Boolean} options.isModeSwitching: Set to true true when this is called as the + * result of a change to the devtools.browsertoolbox.scope pref. + * + * @return boolean + * True if we such entry was already registered, for this watcher actor. + */ + removeSessionDataEntry(watcher, type, entries, options) { + const sessionData = this.getSessionData(watcher); + if (!sessionData) { + return false; + } + + if (!SUPPORTED_DATA_TYPES.includes(type)) { + throw new Error(`Unsupported session data type: ${type}`); + } + + if ( + !SessionDataHelpers.removeSessionDataEntry(sessionData, type, entries) + ) { + return false; + } + + const isWatchingSomething = SUPPORTED_DATA_TYPES.some( + dataType => sessionData[dataType] && !!sessionData[dataType].length + ); + + // Remove the watcher reference if it's not watching for anything anymore, unless we're + // doing a mode switch; in such case we don't mean to end the DevTools session, so we + // still want to have access to the underlying data (furthermore, such case should only + // happen in tests, in a regular workflow we'd still be watching for resources). + if (!isWatchingSomething && !options?.isModeSwitching) { + sessionDataByWatcherActor.delete(watcher.actorID); + watcherActors.delete(watcher.actorID); + } + + persistMapToSharedData(); + + return true; + }, + + /** + * Cleanup everything about a given watcher actor. + * Remove it from any registry so that we stop interacting with it. + * + * The watcher would be automatically unregistered from removeWatcherEntry, + * if we remove all entries. But we aren't removing all breakpoints. + * So here, we force clearing any reference to the watcher actor when it destroys. + */ + unregisterWatcher(watcherActorID) { + sessionDataByWatcherActor.delete(watcherActorID); + watcherActors.delete(watcherActorID); + this.maybeUnregisterJSActors(); + }, + + /** + * Unregister the JS Actors if there is no more DevTools code observing any target/resource. + */ + maybeUnregisterJSActors() { + if (sessionDataByWatcherActor.size == 0) { + unregisterBrowserToolboxJSProcessActor(); + unregisterJSProcessActor(); + } + }, + + /** + * Notify that a given watcher starts observing a new target type. + * + * @param WatcherActor watcher + * The WatcherActor which starts observing. + * @param string targetType + * The new target type to start listening to. + */ + watchTargets(watcher, targetType) { + this.addOrSetSessionDataEntry( + watcher, + SUPPORTED_DATA.TARGETS, + [targetType], + "add" + ); + }, + + /** + * Notify that a given watcher stops observing a given target type. + * + * @param WatcherActor watcher + * The WatcherActor which stops observing. + * @param string targetType + * The new target type to stop listening to. + * @params {Object} options + * @params {Boolean} options.isModeSwitching: Set to true true when this is called as the + * result of a change to the devtools.browsertoolbox.scope pref. + * @return boolean + * True if we were watching for this target type, for this watcher actor. + */ + unwatchTargets(watcher, targetType, options) { + return this.removeSessionDataEntry( + watcher, + SUPPORTED_DATA.TARGETS, + [targetType], + options + ); + }, + + /** + * Notify that a given watcher starts observing new resource types. + * + * @param WatcherActor watcher + * The WatcherActor which starts observing. + * @param Array resourceTypes + * The new resource types to start listening to. + */ + watchResources(watcher, resourceTypes) { + this.addOrSetSessionDataEntry( + watcher, + SUPPORTED_DATA.RESOURCES, + resourceTypes, + "add" + ); + }, + + /** + * Notify that a given watcher stops observing given resource types. + * + * See `watchResources` for argument definition. + * + * @return boolean + * True if we were watching for this resource type, for this watcher actor. + */ + unwatchResources(watcher, resourceTypes) { + return this.removeSessionDataEntry( + watcher, + SUPPORTED_DATA.RESOURCES, + resourceTypes + ); + }, +}; + +// Boolean flag to know if the DevToolsProcess JS Process Actor is currently registered +let isJSProcessActorRegistered = false; +let isBrowserToolboxJSProcessActorRegistered = false; + +const JSProcessActorConfig = { + parent: { + esModuleURI: + "resource://devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs", + }, + child: { + esModuleURI: + "resource://devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs", + // There is no good observer service notification we can listen to to instantiate the JSProcess Actor + // reliably as soon as the process start. + // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification... + observers: ["init-devtools-content-process-actor"], + }, + // The parent process is handled very differently from content processes + // This uses the ParentProcessTarget which inherits from BrowsingContextTarget + // and is, for now, manually created by the descriptor as the top level target. + includeParent: true, +}; + +const BrowserToolboxJSProcessActorConfig = { + ...JSProcessActorConfig, + + // This JS Process Actor is used to bootstrap DevTools code debugging the privileged code + // in content processes. The privileged code runs in the "shared JSM global" (See mozJSModuleLoader). + // DevTools modules should be loaded in a distinct global in order to be able to debug this privileged code. + // There is a strong requirement in spidermonkey for the debuggee and debugger to be using distinct compartments. + // This flag will force both parent and child modules to be loaded via a dedicated loader (See mozJSModuleLoader::GetOrCreateDevToolsLoader) + // + // Note that as a side effect, it makes these modules and all their dependencies to be invisible to the debugger. + loadInDevToolsLoader: true, +}; + +const PROCESS_SCRIPT_URL = + "resource://devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js"; + +function registerJSProcessActor() { + if (isJSProcessActorRegistered) { + return; + } + isJSProcessActorRegistered = true; + ChromeUtils.registerProcessActor("DevToolsProcess", JSProcessActorConfig); + + // There is no good observer service notification we can listen to to instantiate the JSProcess Actor + // as soon as the process start. + // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification... + Services.ppmm.loadProcessScript(PROCESS_SCRIPT_URL, true); +} + +function registerBrowserToolboxJSProcessActor() { + if (isBrowserToolboxJSProcessActorRegistered) { + return; + } + isBrowserToolboxJSProcessActorRegistered = true; + ChromeUtils.registerProcessActor( + "BrowserToolboxDevToolsProcess", + BrowserToolboxJSProcessActorConfig + ); + + // There is no good observer service notification we can listen to to instantiate the JSProcess Actor + // as soon as the process start. + // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification... + Services.ppmm.loadProcessScript(PROCESS_SCRIPT_URL, true); +} + +function unregisterJSProcessActor() { + if (!isJSProcessActorRegistered) { + return; + } + isJSProcessActorRegistered = false; + try { + ChromeUtils.unregisterProcessActor("DevToolsProcess"); + } catch (e) { + // If any pending query was still ongoing, this would throw + } + if (isBrowserToolboxJSProcessActorRegistered) { + return; + } + Services.ppmm.removeDelayedProcessScript(PROCESS_SCRIPT_URL); +} + +function unregisterBrowserToolboxJSProcessActor() { + if (!isBrowserToolboxJSProcessActorRegistered) { + return; + } + isBrowserToolboxJSProcessActorRegistered = false; + try { + ChromeUtils.unregisterProcessActor("BrowserToolboxDevToolsProcess"); + } catch (e) { + // If any pending query was still ongoing, this would throw + } + if (isJSProcessActorRegistered) { + return; + } + Services.ppmm.removeDelayedProcessScript(PROCESS_SCRIPT_URL); +} diff --git a/devtools/server/actors/watcher/SessionDataHelpers.jsm b/devtools/server/actors/watcher/SessionDataHelpers.jsm deleted file mode 100644 index c70df1744f..0000000000 --- a/devtools/server/actors/watcher/SessionDataHelpers.jsm +++ /dev/null @@ -1,244 +0,0 @@ -/* 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"; - -/** - * Helper module alongside WatcherRegistry, which focus on updating the "sessionData" object. - * This object is shared across processes and threads and have to be maintained in all these runtimes. - */ - -var EXPORTED_SYMBOLS = ["SessionDataHelpers"]; - -const lazy = {}; - -if (typeof module == "object") { - // Allow this JSM to also be loaded as a CommonJS module - // Because this module is used from the worker thread, - // (via target-actor-mixin), and workers can't load JSMs via ChromeUtils.import. - loader.lazyRequireGetter( - lazy, - "validateBreakpointLocation", - "resource://devtools/shared/validate-breakpoint.jsm", - true - ); - - loader.lazyRequireGetter( - lazy, - "validateEventBreakpoint", - "resource://devtools/server/actors/utils/event-breakpoints.js", - true - ); -} else { - ChromeUtils.defineLazyGetter(lazy, "validateBreakpointLocation", () => { - return ChromeUtils.import( - "resource://devtools/shared/validate-breakpoint.jsm" - ).validateBreakpointLocation; - }); - ChromeUtils.defineLazyGetter(lazy, "validateEventBreakpoint", () => { - const { loader } = ChromeUtils.importESModule( - "resource://devtools/shared/loader/Loader.sys.mjs" - ); - return loader.require( - "resource://devtools/server/actors/utils/event-breakpoints.js" - ).validateEventBreakpoint; - }); -} - -// List of all arrays stored in `sessionData`, which are replicated across processes and threads -const SUPPORTED_DATA = { - BLACKBOXING: "blackboxing", - BREAKPOINTS: "breakpoints", - BROWSER_ELEMENT_HOST: "browser-element-host", - XHR_BREAKPOINTS: "xhr-breakpoints", - EVENT_BREAKPOINTS: "event-breakpoints", - RESOURCES: "resources", - TARGET_CONFIGURATION: "target-configuration", - THREAD_CONFIGURATION: "thread-configuration", - TARGETS: "targets", -}; - -// Optional function, if data isn't a primitive data type in order to produce a key -// for the given data entry -const DATA_KEY_FUNCTION = { - [SUPPORTED_DATA.BLACKBOXING]({ url, range }) { - return ( - url + - (range - ? `:${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}` - : "") - ); - }, - [SUPPORTED_DATA.BREAKPOINTS]({ location }) { - lazy.validateBreakpointLocation(location); - const { sourceUrl, sourceId, line, column } = location; - return `${sourceUrl}:${sourceId}:${line}:${column}`; - }, - [SUPPORTED_DATA.TARGET_CONFIGURATION]({ key }) { - // Configuration data entries are { key, value } objects, `key` can be used - // as the unique identifier for the entry. - return key; - }, - [SUPPORTED_DATA.THREAD_CONFIGURATION]({ key }) { - // See target configuration comment - return key; - }, - [SUPPORTED_DATA.XHR_BREAKPOINTS]({ path, method }) { - if (typeof path != "string") { - throw new Error( - `XHR Breakpoints expect to have path string, got ${typeof path} instead.` - ); - } - if (typeof method != "string") { - throw new Error( - `XHR Breakpoints expect to have method string, got ${typeof method} instead.` - ); - } - return `${path}:${method}`; - }, - [SUPPORTED_DATA.EVENT_BREAKPOINTS](id) { - if (typeof id != "string") { - throw new Error( - `Event Breakpoints expect the id to be a string , got ${typeof id} instead.` - ); - } - if (!lazy.validateEventBreakpoint(id)) { - throw new Error( - `The id string should be a valid event breakpoint id, ${id} is not.` - ); - } - return id; - }, -}; -// Optional validation method to assert the shape of each session data entry -const DATA_VALIDATION_FUNCTION = { - [SUPPORTED_DATA.BREAKPOINTS]({ location }) { - lazy.validateBreakpointLocation(location); - }, - [SUPPORTED_DATA.XHR_BREAKPOINTS]({ path, method }) { - if (typeof path != "string") { - throw new Error( - `XHR Breakpoints expect to have path string, got ${typeof path} instead.` - ); - } - if (typeof method != "string") { - throw new Error( - `XHR Breakpoints expect to have method string, got ${typeof method} instead.` - ); - } - }, - [SUPPORTED_DATA.EVENT_BREAKPOINTS](id) { - if (typeof id != "string") { - throw new Error( - `Event Breakpoints expect the id to be a string , got ${typeof id} instead.` - ); - } - if (!lazy.validateEventBreakpoint(id)) { - throw new Error( - `The id string should be a valid event breakpoint id, ${id} is not.` - ); - } - }, -}; - -function idFunction(v) { - if (typeof v != "string") { - throw new Error( - `Expect data entry values to be string, or be using custom data key functions. Got ${typeof v} type instead.` - ); - } - return v; -} - -const SessionDataHelpers = { - SUPPORTED_DATA, - - /** - * Add new values to the shared "sessionData" object. - * - * @param Object sessionData - * The data object to update. - * @param string type - * The type of data to be added - * @param Array entries - * The values to be added to this type of data - * @param String updateType - * "add" will only add the new entries in the existing data set. - * "set" will update the data set with the new entries. - */ - addOrSetSessionDataEntry(sessionData, type, entries, updateType) { - const validationFunction = DATA_VALIDATION_FUNCTION[type]; - if (validationFunction) { - entries.forEach(validationFunction); - } - - // When we are replacing the whole entries, things are significantly simplier - if (updateType == "set") { - sessionData[type] = entries; - return; - } - - if (!sessionData[type]) { - sessionData[type] = []; - } - const toBeAdded = []; - const keyFunction = DATA_KEY_FUNCTION[type] || idFunction; - for (const entry of entries) { - const existingIndex = sessionData[type].findIndex(existingEntry => { - return keyFunction(existingEntry) === keyFunction(entry); - }); - if (existingIndex === -1) { - // New entry. - toBeAdded.push(entry); - } else { - // Existing entry, update the value. This is relevant if the data-entry - // is not a primitive data-type, and the value can change for the same - // key. - sessionData[type][existingIndex] = entry; - } - } - sessionData[type].push(...toBeAdded); - }, - - /** - * Remove values from the shared "sessionData" object. - * - * @param Object sessionData - * The data object to update. - * @param string type - * The type of data to be remove - * @param Array entries - * The values to be removed from this type of data - * @return Boolean - * True, if at least one entries existed and has been removed. - * False, if none of the entries existed and none has been removed. - */ - removeSessionDataEntry(sessionData, type, entries) { - let includesAtLeastOne = false; - const keyFunction = DATA_KEY_FUNCTION[type] || idFunction; - for (const entry of entries) { - const idx = sessionData[type] - ? sessionData[type].findIndex(existingEntry => { - return keyFunction(existingEntry) === keyFunction(entry); - }) - : -1; - if (idx !== -1) { - sessionData[type].splice(idx, 1); - includesAtLeastOne = true; - } - } - if (!includesAtLeastOne) { - return false; - } - - return true; - }, -}; - -// Allow this JSM to also be loaded as a CommonJS module -// Because this module is used from the worker thread, -// (via target-actor-mixin), and workers can't load JSMs. -if (typeof module == "object") { - module.exports.SessionDataHelpers = SessionDataHelpers; -} diff --git a/devtools/server/actors/watcher/SessionDataHelpers.sys.mjs b/devtools/server/actors/watcher/SessionDataHelpers.sys.mjs new file mode 100644 index 0000000000..def31b77a8 --- /dev/null +++ b/devtools/server/actors/watcher/SessionDataHelpers.sys.mjs @@ -0,0 +1,218 @@ +/* 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/. */ + +/** + * Helper module alongside ParentProcessWatcherRegistry, which focus on updating the "sessionData" object. + * This object is shared across processes and threads and have to be maintained in all these runtimes. + */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters( + lazy, + { + validateBreakpointLocation: + "resource://devtools/shared/validate-breakpoint.sys.mjs", + }, + { global: "contextual" } +); + +ChromeUtils.defineLazyGetter(lazy, "validateEventBreakpoint", () => { + const { loader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs", + { global: "contextual" } + ); + return loader.require( + "resource://devtools/server/actors/utils/event-breakpoints.js" + ).validateEventBreakpoint; +}); + +// List of all arrays stored in `sessionData`, which are replicated across processes and threads +const SUPPORTED_DATA = { + BLACKBOXING: "blackboxing", + BREAKPOINTS: "breakpoints", + BROWSER_ELEMENT_HOST: "browser-element-host", + XHR_BREAKPOINTS: "xhr-breakpoints", + EVENT_BREAKPOINTS: "event-breakpoints", + RESOURCES: "resources", + TARGET_CONFIGURATION: "target-configuration", + THREAD_CONFIGURATION: "thread-configuration", + TARGETS: "targets", +}; + +// Optional function, if data isn't a primitive data type in order to produce a key +// for the given data entry +const DATA_KEY_FUNCTION = { + [SUPPORTED_DATA.BLACKBOXING]({ url, range }) { + return ( + url + + (range + ? `:${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}` + : "") + ); + }, + [SUPPORTED_DATA.BREAKPOINTS]({ location }) { + lazy.validateBreakpointLocation(location); + const { sourceUrl, sourceId, line, column } = location; + return `${sourceUrl}:${sourceId}:${line}:${column}`; + }, + [SUPPORTED_DATA.TARGET_CONFIGURATION]({ key }) { + // Configuration data entries are { key, value } objects, `key` can be used + // as the unique identifier for the entry. + return key; + }, + [SUPPORTED_DATA.THREAD_CONFIGURATION]({ key }) { + // See target configuration comment + return key; + }, + [SUPPORTED_DATA.XHR_BREAKPOINTS]({ path, method }) { + if (typeof path != "string") { + throw new Error( + `XHR Breakpoints expect to have path string, got ${typeof path} instead.` + ); + } + if (typeof method != "string") { + throw new Error( + `XHR Breakpoints expect to have method string, got ${typeof method} instead.` + ); + } + return `${path}:${method}`; + }, + [SUPPORTED_DATA.EVENT_BREAKPOINTS](id) { + if (typeof id != "string") { + throw new Error( + `Event Breakpoints expect the id to be a string , got ${typeof id} instead.` + ); + } + if (!lazy.validateEventBreakpoint(id)) { + throw new Error( + `The id string should be a valid event breakpoint id, ${id} is not.` + ); + } + return id; + }, +}; +// Optional validation method to assert the shape of each session data entry +const DATA_VALIDATION_FUNCTION = { + [SUPPORTED_DATA.BREAKPOINTS]({ location }) { + lazy.validateBreakpointLocation(location); + }, + [SUPPORTED_DATA.XHR_BREAKPOINTS]({ path, method }) { + if (typeof path != "string") { + throw new Error( + `XHR Breakpoints expect to have path string, got ${typeof path} instead.` + ); + } + if (typeof method != "string") { + throw new Error( + `XHR Breakpoints expect to have method string, got ${typeof method} instead.` + ); + } + }, + [SUPPORTED_DATA.EVENT_BREAKPOINTS](id) { + if (typeof id != "string") { + throw new Error( + `Event Breakpoints expect the id to be a string , got ${typeof id} instead.` + ); + } + if (!lazy.validateEventBreakpoint(id)) { + throw new Error( + `The id string should be a valid event breakpoint id, ${id} is not.` + ); + } + }, +}; + +function idFunction(v) { + if (typeof v != "string") { + throw new Error( + `Expect data entry values to be string, or be using custom data key functions. Got ${typeof v} type instead.` + ); + } + return v; +} + +export const SessionDataHelpers = { + SUPPORTED_DATA, + + /** + * Add new values to the shared "sessionData" object. + * + * @param Object sessionData + * The data object to update. + * @param string type + * The type of data to be added + * @param Array entries + * The values to be added to this type of data + * @param String updateType + * "add" will only add the new entries in the existing data set. + * "set" will update the data set with the new entries. + */ + addOrSetSessionDataEntry(sessionData, type, entries, updateType) { + const validationFunction = DATA_VALIDATION_FUNCTION[type]; + if (validationFunction) { + entries.forEach(validationFunction); + } + + // When we are replacing the whole entries, things are significantly simplier + if (updateType == "set") { + sessionData[type] = entries; + return; + } + + if (!sessionData[type]) { + sessionData[type] = []; + } + const toBeAdded = []; + const keyFunction = DATA_KEY_FUNCTION[type] || idFunction; + for (const entry of entries) { + const existingIndex = sessionData[type].findIndex(existingEntry => { + return keyFunction(existingEntry) === keyFunction(entry); + }); + if (existingIndex === -1) { + // New entry. + toBeAdded.push(entry); + } else { + // Existing entry, update the value. This is relevant if the data-entry + // is not a primitive data-type, and the value can change for the same + // key. + sessionData[type][existingIndex] = entry; + } + } + sessionData[type].push(...toBeAdded); + }, + + /** + * Remove values from the shared "sessionData" object. + * + * @param Object sessionData + * The data object to update. + * @param string type + * The type of data to be remove + * @param Array entries + * The values to be removed from this type of data + * @return Boolean + * True, if at least one entries existed and has been removed. + * False, if none of the entries existed and none has been removed. + */ + removeSessionDataEntry(sessionData, type, entries) { + let includesAtLeastOne = false; + const keyFunction = DATA_KEY_FUNCTION[type] || idFunction; + for (const entry of entries) { + const idx = sessionData[type] + ? sessionData[type].findIndex(existingEntry => { + return keyFunction(existingEntry) === keyFunction(entry); + }) + : -1; + if (idx !== -1) { + sessionData[type].splice(idx, 1); + includesAtLeastOne = true; + } + } + if (!includesAtLeastOne) { + return false; + } + + return true; + }, +}; diff --git a/devtools/server/actors/watcher/WatcherRegistry.sys.mjs b/devtools/server/actors/watcher/WatcherRegistry.sys.mjs deleted file mode 100644 index ac8bc7f0c8..0000000000 --- a/devtools/server/actors/watcher/WatcherRegistry.sys.mjs +++ /dev/null @@ -1,461 +0,0 @@ -/* 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/. */ - -/** - * Helper module around `sharedData` object that helps storing the state - * of all observed Targets and Resources, that, for all DevTools connections. - * Here is a few words about the C++ implementation of sharedData: - * https://searchfox.org/mozilla-central/rev/bc3600def806859c31b2c7ac06e3d69271052a89/dom/ipc/SharedMap.h#30-55 - * - * We may have more than one DevToolsServer and one server may have more than one - * client. This module will be the single source of truth in the parent process, - * in order to know which targets/resources are currently observed. It will also - * be used to declare when something starts/stops being observed. - * - * `sharedData` is a platform API that helps sharing JS Objects across processes. - * We use it in order to communicate to the content process which targets and resources - * should be observed. Content processes read this data only once, as soon as they are created. - * It isn't used beyond this point. Content processes are not going to update it. - * We will notify about changes in observed targets and resources for already running - * processes by some other means. (Via JS Window Actor queries "DevTools:(un)watch(Resources|Target)") - * This means that only this module will update the "DevTools:watchedPerWatcher" value. - * From the parent process, we should be going through this module to fetch the data, - * while from the content process, we will read `sharedData` directly. - */ - -import { ActorManagerParent } from "resource://gre/modules/ActorManagerParent.sys.mjs"; - -const { SessionDataHelpers } = ChromeUtils.import( - "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm" -); - -const { SUPPORTED_DATA } = SessionDataHelpers; -const SUPPORTED_DATA_TYPES = Object.values(SUPPORTED_DATA); - -// Define the Map that will be saved in `sharedData`. -// It is keyed by WatcherActor ID and values contains following attributes: -// - targets: Set of strings, refering to target types to be listened to -// - resources: Set of strings, refering to resource types to be observed -// - sessionContext Object, The Session Context to help know what is debugged. -// See devtools/server/actors/watcher/session-context.js -// - connectionPrefix: The DevToolsConnection prefix of the watcher actor. Used to compute new actor ID in the content processes. -// -// Unfortunately, `sharedData` is subject to race condition and may have side effect -// when read/written from multiple places in the same process, -// which is why this map should be considered as the single source of truth. -const sessionDataByWatcherActor = new Map(); - -// In parallel to the previous map, keep all the WatcherActor keyed by the same WatcherActor ID, -// the WatcherActor ID. We don't (and can't) propagate the WatcherActor instances to the content -// processes, but still would like to match them by their ID. -const watcherActors = new Map(); - -// Name of the attribute into which we save this Map in `sharedData` object. -const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; - -/** - * Use `sharedData` to allow processes, early during their creation, - * to know which resources should be listened to. This will be read - * from the Target actor, when it gets created early during process start, - * in order to start listening to the expected resource types. - */ -function persistMapToSharedData() { - Services.ppmm.sharedData.set(SHARED_DATA_KEY_NAME, sessionDataByWatcherActor); - // Request to immediately flush the data to the content processes in order to prevent - // races (bug 1644649). Otherwise content process may have outdated sharedData - // and try to create targets for Watcher actor that already stopped watching for targets. - Services.ppmm.sharedData.flush(); -} - -export const WatcherRegistry = { - /** - * Tells if a given watcher currently watches for a given target type. - * - * @param WatcherActor watcher - * The WatcherActor which should be listening. - * @param string targetType - * The new target type to query. - * @return boolean - * Returns true if already watching. - */ - isWatchingTargets(watcher, targetType) { - const sessionData = this.getSessionData(watcher); - return !!sessionData?.targets?.includes(targetType); - }, - - /** - * Retrieve the data saved into `sharedData` that is used to know - * about which type of targets and resources we care listening about. - * `sessionDataByWatcherActor` is saved into `sharedData` after each mutation, - * but `sessionDataByWatcherActor` is the source of truth. - * - * @param WatcherActor watcher - * The related WatcherActor which starts/stops observing. - * @param object options (optional) - * A dictionary object with `createData` boolean attribute. - * If this attribute is set to true, we create the data structure in the Map - * if none exists for this prefix. - */ - getSessionData(watcher, { createData = false } = {}) { - // Use WatcherActor ID as a key as we may have multiple clients willing to watch for targets. - // For example, a Browser Toolbox debugging everything and a Content Toolbox debugging - // just one tab. We might also have multiple watchers, on the same connection when using about:debugging. - const watcherActorID = watcher.actorID; - let sessionData = sessionDataByWatcherActor.get(watcherActorID); - if (!sessionData && createData) { - sessionData = { - // The "session context" object help understand what should be debugged and which target should be created. - // See WatcherActor constructor for more info. - sessionContext: watcher.sessionContext, - // The DevToolsServerConnection prefix will be used to compute actor IDs created in the content process - connectionPrefix: watcher.conn.prefix, - }; - sessionDataByWatcherActor.set(watcherActorID, sessionData); - watcherActors.set(watcherActorID, watcher); - } - return sessionData; - }, - - /** - * Given a Watcher Actor ID, return the related Watcher Actor instance. - * - * @param String actorID - * The Watcher Actor ID to search for. - * @return WatcherActor - * The Watcher Actor instance. - */ - getWatcher(actorID) { - return watcherActors.get(actorID); - }, - - /** - * Return an array of the watcher actors that match the passed browserId - * - * @param {Number} browserId - * @returns {Array} An array of the matching watcher actors - */ - getWatchersForBrowserId(browserId) { - const watchers = []; - for (const watcherActor of watcherActors.values()) { - if ( - watcherActor.sessionContext.type == "browser-element" && - watcherActor.sessionContext.browserId === browserId - ) { - watchers.push(watcherActor); - } - } - - return watchers; - }, - - /** - * Notify that a given watcher added or set some entries for given data type. - * - * @param WatcherActor watcher - * The WatcherActor which starts observing. - * @param string type - * The type of data to be added - * @param Array entries - * The values to be added to this type of data - * @param String updateType - * "add" will only add the new entries in the existing data set. - * "set" will update the data set with the new entries. - */ - addOrSetSessionDataEntry(watcher, type, entries, updateType) { - const sessionData = this.getSessionData(watcher, { - createData: true, - }); - - if (!SUPPORTED_DATA_TYPES.includes(type)) { - throw new Error(`Unsupported session data type: ${type}`); - } - - SessionDataHelpers.addOrSetSessionDataEntry( - sessionData, - type, - entries, - updateType - ); - - // Register the JS Window Actor the first time we start watching for something (e.g. resource, target, …). - registerJSWindowActor(); - if (sessionData?.targets?.includes("process")) { - registerJSProcessActor(); - } - - persistMapToSharedData(); - }, - - /** - * Notify that a given watcher removed an entry in a given data type. - * - * @param WatcherActor watcher - * The WatcherActor which stops observing. - * @param string type - * The type of data to be removed - * @param Array entries - * The values to be removed to this type of data - * @params {Object} options - * @params {Boolean} options.isModeSwitching: Set to true true when this is called as the - * result of a change to the devtools.browsertoolbox.scope pref. - * - * @return boolean - * True if we such entry was already registered, for this watcher actor. - */ - removeSessionDataEntry(watcher, type, entries, options) { - const sessionData = this.getSessionData(watcher); - if (!sessionData) { - return false; - } - - if (!SUPPORTED_DATA_TYPES.includes(type)) { - throw new Error(`Unsupported session data type: ${type}`); - } - - if ( - !SessionDataHelpers.removeSessionDataEntry(sessionData, type, entries) - ) { - return false; - } - - const isWatchingSomething = SUPPORTED_DATA_TYPES.some( - dataType => sessionData[dataType] && !!sessionData[dataType].length - ); - - // Remove the watcher reference if it's not watching for anything anymore, unless we're - // doing a mode switch; in such case we don't mean to end the DevTools session, so we - // still want to have access to the underlying data (furthermore, such case should only - // happen in tests, in a regular workflow we'd still be watching for resources). - if (!isWatchingSomething && !options?.isModeSwitching) { - sessionDataByWatcherActor.delete(watcher.actorID); - watcherActors.delete(watcher.actorID); - } - - persistMapToSharedData(); - - return true; - }, - - /** - * Cleanup everything about a given watcher actor. - * Remove it from any registry so that we stop interacting with it. - * - * The watcher would be automatically unregistered from removeWatcherEntry, - * if we remove all entries. But we aren't removing all breakpoints. - * So here, we force clearing any reference to the watcher actor when it destroys. - */ - unregisterWatcher(watcher) { - sessionDataByWatcherActor.delete(watcher.actorID); - watcherActors.delete(watcher.actorID); - this.maybeUnregisterJSActors(); - }, - - /** - * Unregister the JS Actors if there is no more DevTools code observing any target/resource. - */ - maybeUnregisterJSActors() { - if (sessionDataByWatcherActor.size == 0) { - unregisterJSWindowActor(); - unregisterJSProcessActor(); - } - }, - - /** - * Notify that a given watcher starts observing a new target type. - * - * @param WatcherActor watcher - * The WatcherActor which starts observing. - * @param string targetType - * The new target type to start listening to. - */ - watchTargets(watcher, targetType) { - this.addOrSetSessionDataEntry( - watcher, - SUPPORTED_DATA.TARGETS, - [targetType], - "add" - ); - }, - - /** - * Notify that a given watcher stops observing a given target type. - * - * @param WatcherActor watcher - * The WatcherActor which stops observing. - * @param string targetType - * The new target type to stop listening to. - * @params {Object} options - * @params {Boolean} options.isModeSwitching: Set to true true when this is called as the - * result of a change to the devtools.browsertoolbox.scope pref. - * @return boolean - * True if we were watching for this target type, for this watcher actor. - */ - unwatchTargets(watcher, targetType, options) { - return this.removeSessionDataEntry( - watcher, - SUPPORTED_DATA.TARGETS, - [targetType], - options - ); - }, - - /** - * Notify that a given watcher starts observing new resource types. - * - * @param WatcherActor watcher - * The WatcherActor which starts observing. - * @param Array resourceTypes - * The new resource types to start listening to. - */ - watchResources(watcher, resourceTypes) { - this.addOrSetSessionDataEntry( - watcher, - SUPPORTED_DATA.RESOURCES, - resourceTypes, - "add" - ); - }, - - /** - * Notify that a given watcher stops observing given resource types. - * - * See `watchResources` for argument definition. - * - * @return boolean - * True if we were watching for this resource type, for this watcher actor. - */ - unwatchResources(watcher, resourceTypes) { - return this.removeSessionDataEntry( - watcher, - SUPPORTED_DATA.RESOURCES, - resourceTypes - ); - }, -}; - -// Boolean flag to know if the DevToolsFrame JS Window Actor is currently registered -let isJSWindowActorRegistered = false; - -/** - * Register the JSWindowActor pair "DevToolsFrame". - * - * We should call this method before we try to use this JS Window Actor from the parent process - * (via `WindowGlobal.getActor("DevToolsFrame")` or `WindowGlobal.getActor("DevToolsWorker")`). - * Also, registering it will automatically force spawing the content process JSWindow Actor - * anytime a new document is opened (via DOMWindowCreated event). - */ - -const JSWindowActorsConfig = { - DevToolsFrame: { - parent: { - esModuleURI: - "resource://devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs", - }, - child: { - esModuleURI: - "resource://devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs", - events: { - DOMWindowCreated: {}, - DOMDocElementInserted: {}, - pageshow: {}, - pagehide: {}, - }, - }, - allFrames: true, - }, - DevToolsWorker: { - parent: { - esModuleURI: - "resource://devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs", - }, - child: { - esModuleURI: - "resource://devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs", - events: { - DOMWindowCreated: {}, - }, - }, - allFrames: true, - }, -}; - -function registerJSWindowActor() { - if (isJSWindowActorRegistered) { - return; - } - isJSWindowActorRegistered = true; - ActorManagerParent.addJSWindowActors(JSWindowActorsConfig); -} - -function unregisterJSWindowActor() { - if (!isJSWindowActorRegistered) { - return; - } - isJSWindowActorRegistered = false; - - for (const JSWindowActorName of Object.keys(JSWindowActorsConfig)) { - // ActorManagerParent doesn't expose a "removeActors" method, but it would be equivalent to that: - ChromeUtils.unregisterWindowActor(JSWindowActorName); - } -} - -// Boolean flag to know if the DevToolsProcess JS Process Actor is currently registered -let isJSProcessActorRegistered = false; - -const JSProcessActorConfig = { - parent: { - esModuleURI: - "resource://devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs", - }, - child: { - esModuleURI: - "resource://devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs", - // There is no good observer service notification we can listen to to instantiate the JSProcess Actor - // reliably as soon as the process start. - // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification... - observers: ["init-devtools-content-process-actor"], - }, - // The parent process is handled very differently from content processes - // This uses the ParentProcessTarget which inherits from BrowsingContextTarget - // and is, for now, manually created by the descriptor as the top level target. - includeParent: false, - - // This JS Process Actor is used to bootstrap DevTools code debugging the privileged code - // in content processes. The privileged code runs in the "shared JSM global" (See mozJSModuleLoader). - // DevTools modules should be loaded in a distinct global in order to be able to debug this privileged code. - // There is a strong requirement in spidermonkey for the debuggee and debugger to be using distinct compartments. - // This flag will force both parent and child modules to be loaded via a dedicated loader (See mozJSModuleLoader::GetOrCreateDevToolsLoader) - // - // Note that as a side effect, it makes these modules and all their dependencies to be invisible to the debugger. - loadInDevToolsLoader: true, -}; - -const PROCESS_SCRIPT_URL = - "resource://devtools/server/actors/watcher/target-helpers/content-process-jsprocessactor-startup.js"; - -function registerJSProcessActor() { - if (isJSProcessActorRegistered) { - return; - } - isJSProcessActorRegistered = true; - ChromeUtils.registerProcessActor("DevToolsProcess", JSProcessActorConfig); - - // There is no good observer service notification we can listen to to instantiate the JSProcess Actor - // as soon as the process start. - // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification... - Services.ppmm.loadProcessScript(PROCESS_SCRIPT_URL, true); -} - -function unregisterJSProcessActor() { - if (!isJSProcessActorRegistered) { - return; - } - isJSProcessActorRegistered = false; - try { - ChromeUtils.unregisterProcessActor("DevToolsProcess"); - } catch (e) { - // If any pending query was still ongoing, this would throw - } - Services.ppmm.removeDelayedProcessScript(PROCESS_SCRIPT_URL); -} diff --git a/devtools/server/actors/watcher/browsing-context-helpers.sys.mjs b/devtools/server/actors/watcher/browsing-context-helpers.sys.mjs index d52cbc5708..cd34c75760 100644 --- a/devtools/server/actors/watcher/browsing-context-helpers.sys.mjs +++ b/devtools/server/actors/watcher/browsing-context-helpers.sys.mjs @@ -382,7 +382,7 @@ export function getAllBrowsingContextsForContext( sessionContext.browserId ); // topBrowsingContext can be null if getCurrentTopByBrowserId is called for a tab that is unloaded. - if (topBrowsingContext) { + if (topBrowsingContext?.embedderElement) { // Unfortunately, getCurrentTopByBrowserId is subject to race conditions and may refer to a BrowsingContext // that already navigated away. // Query the current "live" BrowsingContext by going through the embedder element (i.e. the /