diff options
Diffstat (limited to 'devtools/server')
78 files changed, 3043 insertions, 4924 deletions
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 @@ -42,15 +42,17 @@ loader.lazyRequireGetter( ); 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<Object>} 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<TargetActor>} 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/WatcherRegistry.sys.mjs b/devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs index ac8bc7f0c8..e9b3a9d50d 100644 --- a/devtools/server/actors/watcher/WatcherRegistry.sys.mjs +++ b/devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs @@ -24,10 +24,9 @@ * 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 { SessionDataHelpers } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", + { global: "contextual" } ); const { SUPPORTED_DATA } = SessionDataHelpers; @@ -68,7 +67,7 @@ function persistMapToSharedData() { Services.ppmm.sharedData.flush(); } -export const WatcherRegistry = { +export const ParentProcessWatcherRegistry = { /** * Tells if a given watcher currently watches for a given target type. * @@ -178,13 +177,16 @@ export const WatcherRegistry = { 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, …). - registerJSWindowActor(); - if (sessionData?.targets?.includes("process")) { + if (watcher.sessionContext.type == "all") { + registerBrowserToolboxJSProcessActor(); + } else { registerJSProcessActor(); } - - persistMapToSharedData(); }, /** @@ -245,9 +247,9 @@ export const WatcherRegistry = { * 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); + unregisterWatcher(watcherActorID) { + sessionDataByWatcherActor.delete(watcherActorID); + watcherActors.delete(watcherActorID); this.maybeUnregisterJSActors(); }, @@ -256,7 +258,7 @@ export const WatcherRegistry = { */ maybeUnregisterJSActors() { if (sessionDataByWatcherActor.size == 0) { - unregisterJSWindowActor(); + unregisterBrowserToolboxJSProcessActor(); unregisterJSProcessActor(); } }, @@ -334,74 +336,9 @@ export const WatcherRegistry = { }, }; -// 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; +let isBrowserToolboxJSProcessActorRegistered = false; const JSProcessActorConfig = { parent: { @@ -419,7 +356,11 @@ const JSProcessActorConfig = { // 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, + 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). @@ -432,7 +373,7 @@ const JSProcessActorConfig = { }; const PROCESS_SCRIPT_URL = - "resource://devtools/server/actors/watcher/target-helpers/content-process-jsprocessactor-startup.js"; + "resource://devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js"; function registerJSProcessActor() { if (isJSProcessActorRegistered) { @@ -447,6 +388,22 @@ function registerJSProcessActor() { 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; @@ -457,5 +414,24 @@ function unregisterJSProcessActor() { } 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.sys.mjs index c70df1744f..def31b77a8 100644 --- a/devtools/server/actors/watcher/SessionDataHelpers.jsm +++ b/devtools/server/actors/watcher/SessionDataHelpers.sys.mjs @@ -2,49 +2,30 @@ * 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. + * 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. */ -var EXPORTED_SYMBOLS = ["SessionDataHelpers"]; - const lazy = {}; +ChromeUtils.defineESModuleGetters( + lazy, + { + validateBreakpointLocation: + "resource://devtools/shared/validate-breakpoint.sys.mjs", + }, + { global: "contextual" } +); -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 +ChromeUtils.defineLazyGetter(lazy, "validateEventBreakpoint", () => { + const { loader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs", + { global: "contextual" } ); - - 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; - }); -} + 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 = { @@ -151,7 +132,7 @@ function idFunction(v) { return v; } -const SessionDataHelpers = { +export const SessionDataHelpers = { SUPPORTED_DATA, /** @@ -235,10 +216,3 @@ const SessionDataHelpers = { 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/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 <browser>/<iframe> element) diff --git a/devtools/server/actors/watcher/moz.build b/devtools/server/actors/watcher/moz.build index 46a9d89718..47d08e8780 100644 --- a/devtools/server/actors/watcher/moz.build +++ b/devtools/server/actors/watcher/moz.build @@ -4,13 +4,9 @@ # 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/. -DIRS += [ - "target-helpers", -] - DevToolsModules( "browsing-context-helpers.sys.mjs", + "ParentProcessWatcherRegistry.sys.mjs", "session-context.js", - "SessionDataHelpers.jsm", - "WatcherRegistry.sys.mjs", + "SessionDataHelpers.sys.mjs", ) diff --git a/devtools/server/actors/watcher/target-helpers/content-process-jsprocessactor-startup.js b/devtools/server/actors/watcher/target-helpers/content-process-jsprocessactor-startup.js deleted file mode 100644 index 1765bcc66c..0000000000 --- a/devtools/server/actors/watcher/target-helpers/content-process-jsprocessactor-startup.js +++ /dev/null @@ -1,26 +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"; - -const { setTimeout } = ChromeUtils.importESModule( - "resource://gre/modules/Timer.sys.mjs" -); - -/* - We can't spawn the JSProcessActor right away and have to spin the event loop. - Otherwise it isn't registered yet and isn't listening to observer service. - Could it be the reason why JSProcessActor aren't spawn via process actor option's child.observers notifications ?? -*/ -setTimeout(function () { - /* - This notification is registered in DevToolsServiceWorker JS process actor's options's `observers` attribute - and will force the JS Process actor to be instantiated in all processes. - */ - Services.obs.notifyObservers(null, "init-devtools-content-process-actor"); - /* - Instead of using observer service, we could also manually call some method of the actor: - ChromeUtils.domProcessChild.getActor("DevToolsProcess").observe(null, "foo"); - */ -}, 0); diff --git a/devtools/server/actors/watcher/target-helpers/frame-helper.js b/devtools/server/actors/watcher/target-helpers/frame-helper.js deleted file mode 100644 index 18d4d8f92e..0000000000 --- a/devtools/server/actors/watcher/target-helpers/frame-helper.js +++ /dev/null @@ -1,330 +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"; - -const { WatcherRegistry } = ChromeUtils.importESModule( - "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs", - // WatcherRegistry needs to be a true singleton and loads ActorManagerParent - // which also has to be a true singleton. - { global: "shared" } -); -const { WindowGlobalLogger } = ChromeUtils.importESModule( - "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs", - { global: "contextual" } -); -const Targets = require("resource://devtools/server/actors/targets/index.js"); - -const browsingContextAttachedObserverByWatcher = new Map(); - -/** - * Force creating targets for all existing BrowsingContext, that, for a given Watcher Actor. - * - * @param WatcherActor watcher - * The Watcher Actor requesting to watch for new targets. - */ -async function createTargets(watcher) { - // Go over all existing BrowsingContext in order to: - // - Force the instantiation of a DevToolsFrameChild - // - Have the DevToolsFrameChild to spawn the WindowGlobalTargetActor - - // If we have a browserElement, set the watchedByDevTools flag on its related browsing context - // TODO: We should also set the flag for the "parent process" browsing context when we're - // in the browser toolbox. This is blocked by Bug 1675763, and should be handled as part - // of Bug 1709529. - if (watcher.sessionContext.type == "browser-element") { - // The `watchedByDevTools` enables gecko behavior tied to this flag, such as: - // - reporting the contents of HTML loaded in the docshells - // - capturing stacks for the network monitor. - watcher.browserElement.browsingContext.watchedByDevTools = true; - } - - if (!browsingContextAttachedObserverByWatcher.has(watcher)) { - // We store the browserId here as watcher.browserElement.browserId can momentary be - // set to 0 when there's a navigation to a new browsing context. - const browserId = watcher.sessionContext.browserId; - const onBrowsingContextAttached = browsingContext => { - // We want to set watchedByDevTools on new top-level browsing contexts: - // - in the case of the BrowserToolbox/BrowserConsole, that would be the browsing - // contexts of all the tabs we want to handle. - // - for the regular toolbox, browsing context that are being created when navigating - // to a page that forces a new browsing context. - // Then BrowsingContext will propagate to all the tree of children BrowsingContext's. - if ( - !browsingContext.parent && - (watcher.sessionContext.type != "browser-element" || - browserId === browsingContext.browserId) - ) { - browsingContext.watchedByDevTools = true; - } - }; - Services.obs.addObserver( - onBrowsingContextAttached, - "browsing-context-attached" - ); - // We store the observer so we can retrieve it elsewhere (e.g. for removal in destroyTargets). - browsingContextAttachedObserverByWatcher.set( - watcher, - onBrowsingContextAttached - ); - } - - if ( - watcher.sessionContext.isServerTargetSwitchingEnabled && - watcher.sessionContext.type == "browser-element" - ) { - // If server side target switching is enabled, process the top level browsing context first, - // so that we guarantee it is notified to the client first. - // If it is disabled, the top level target will be created from the client instead. - await createTargetForBrowsingContext({ - watcher, - browsingContext: watcher.browserElement.browsingContext, - retryOnAbortError: true, - }); - } - - const browsingContexts = watcher.getAllBrowsingContexts().filter( - // Filter out the top browsing context we just processed. - browsingContext => - browsingContext != watcher.browserElement?.browsingContext - ); - // Await for the all the queries in order to resolve only *after* we received all - // already available targets. - // i.e. each call to `createTargetForBrowsingContext` should end up emitting - // a target-available-form event via the WatcherActor. - await Promise.allSettled( - browsingContexts.map(browsingContext => - createTargetForBrowsingContext({ watcher, browsingContext }) - ) - ); -} - -/** - * (internal helper method) Force creating the target actor for a given BrowsingContext. - * - * @param WatcherActor watcher - * The Watcher Actor requesting to watch for new targets. - * @param BrowsingContext browsingContext - * The context for which a target should be created. - * @param Boolean retryOnAbortError - * Set to true to retry creating existing targets when receiving an AbortError. - * An AbortError is sent when the JSWindowActor pair was destroyed before the query - * was complete, which can happen if the document navigates while the query is pending. - */ -async function createTargetForBrowsingContext({ - watcher, - browsingContext, - retryOnAbortError = false, -}) { - logWindowGlobal(browsingContext.currentWindowGlobal, "Existing WindowGlobal"); - - // We need to set the watchedByDevTools flag on all top-level browsing context. In the - // case of a content toolbox, this is done in the tab descriptor, but when we're in the - // browser toolbox, such descriptor is not created. - // Then BrowsingContext will propagate to all the tree of children BbrowsingContext's. - if (!browsingContext.parent) { - browsingContext.watchedByDevTools = true; - } - - try { - await browsingContext.currentWindowGlobal - .getActor("DevToolsFrame") - .instantiateTarget({ - watcherActorID: watcher.actorID, - connectionPrefix: watcher.conn.prefix, - sessionContext: watcher.sessionContext, - sessionData: watcher.sessionData, - }); - } catch (e) { - console.warn( - "Failed to create DevTools Frame target for browsingContext", - browsingContext.id, - ": ", - e, - retryOnAbortError ? "retrying" : "" - ); - if (retryOnAbortError && e.name === "AbortError") { - await createTargetForBrowsingContext({ - watcher, - browsingContext, - retryOnAbortError, - }); - } else { - throw e; - } - } -} - -/** - * Force destroying all BrowsingContext targets which were related to a given watcher. - * - * @param WatcherActor watcher - * The Watcher Actor requesting to stop watching for new targets. - * @param {object} options - * @param {boolean} options.isModeSwitching - * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref - */ -function destroyTargets(watcher, options) { - // Go over all existing BrowsingContext in order to destroy all targets - const browsingContexts = watcher.getAllBrowsingContexts(); - - for (const browsingContext of browsingContexts) { - logWindowGlobal( - browsingContext.currentWindowGlobal, - "Existing WindowGlobal" - ); - - if (!browsingContext.parent) { - browsingContext.watchedByDevTools = false; - } - - browsingContext.currentWindowGlobal - .getActor("DevToolsFrame") - .destroyTarget({ - watcherActorID: watcher.actorID, - sessionContext: watcher.sessionContext, - options, - }); - } - - if (watcher.sessionContext.type == "browser-element") { - watcher.browserElement.browsingContext.watchedByDevTools = false; - } - - if (browsingContextAttachedObserverByWatcher.has(watcher)) { - Services.obs.removeObserver( - browsingContextAttachedObserverByWatcher.get(watcher), - "browsing-context-attached" - ); - browsingContextAttachedObserverByWatcher.delete(watcher); - } -} - -/** - * Go over all existing BrowsingContext in order to communicate about new data entries - * - * @param WatcherActor watcher - * The Watcher Actor requesting to stop watching for new targets. - * @param string type - * The type of data to be added - * @param Array<Object> 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. - */ -async function addOrSetSessionDataEntry({ - watcher, - type, - entries, - updateType, -}) { - const browsingContexts = getWatchingBrowsingContexts(watcher); - const promises = []; - for (const browsingContext of browsingContexts) { - logWindowGlobal( - browsingContext.currentWindowGlobal, - "Existing WindowGlobal" - ); - - const promise = browsingContext.currentWindowGlobal - .getActor("DevToolsFrame") - .addOrSetSessionDataEntry({ - watcherActorID: watcher.actorID, - sessionContext: watcher.sessionContext, - type, - entries, - updateType, - }); - promises.push(promise); - } - // Await for the queries in order to try to resolve only *after* the remote code processed the new data - return Promise.all(promises); -} - -/** - * Notify all existing frame targets that some data entries have been removed - * - * See addOrSetSessionDataEntry for argument documentation. - */ -function removeSessionDataEntry({ watcher, type, entries }) { - const browsingContexts = getWatchingBrowsingContexts(watcher); - for (const browsingContext of browsingContexts) { - logWindowGlobal( - browsingContext.currentWindowGlobal, - "Existing WindowGlobal" - ); - - browsingContext.currentWindowGlobal - .getActor("DevToolsFrame") - .removeSessionDataEntry({ - watcherActorID: watcher.actorID, - sessionContext: watcher.sessionContext, - type, - entries, - }); - } -} - -module.exports = { - createTargets, - destroyTargets, - addOrSetSessionDataEntry, - removeSessionDataEntry, -}; - -/** - * Return the list of BrowsingContexts which should be targeted in order to communicate - * updated session data. - * - * @param WatcherActor watcher - * The watcher actor will be used to know which target we debug - * and what BrowsingContext should be considered. - */ -function getWatchingBrowsingContexts(watcher) { - // If we are watching for additional frame targets, it means that the multiprocess or fission mode is enabled, - // either for a content toolbox or a BrowserToolbox via scope set to everything. - const watchingAdditionalTargets = WatcherRegistry.isWatchingTargets( - watcher, - Targets.TYPES.FRAME - ); - if (watchingAdditionalTargets) { - return watcher.getAllBrowsingContexts(); - } - // By default, when we are no longer watching for frame targets, we should no longer try to - // communicate with any browsing-context. But. - // - // For "browser-element" debugging, all targets are provided by watching by watching for frame targets. - // So, when we are no longer watching for frame, we don't expect to have any frame target to talk to. - // => we should no longer reach any browsing context. - // - // For "all" (=browser toolbox), there is only the special ParentProcessTargetActor we might want to return here. - // But this is actually handled by the WatcherActor which uses `WatcherActor.getTargetActorInParentProcess` to convey session data. - // => we should no longer reach any browsing context. - // - // For "webextension" debugging, there is the special WebExtensionTargetActor, which doesn't run in the parent process, - // so that we can't rely on the same code as the browser toolbox. - // => we should always reach out this particular browsing context. - if (watcher.sessionContext.type == "webextension") { - const browsingContext = BrowsingContext.get( - watcher.sessionContext.addonBrowsingContextID - ); - // The add-on browsing context may be destroying, in which case we shouldn't try to communicate with it - if (browsingContext.currentWindowGlobal) { - return [browsingContext]; - } - } - return []; -} - -// Set to true to log info about about WindowGlobal's being watched. -const DEBUG = false; - -function logWindowGlobal(windowGlobal, message) { - if (!DEBUG) { - return; - } - - WindowGlobalLogger.logWindowGlobal(windowGlobal, message); -} diff --git a/devtools/server/actors/watcher/target-helpers/moz.build b/devtools/server/actors/watcher/target-helpers/moz.build deleted file mode 100644 index 3b00f0ef47..0000000000 --- a/devtools/server/actors/watcher/target-helpers/moz.build +++ /dev/null @@ -1,14 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# 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/. - -DevToolsModules( - "content-process-jsprocessactor-startup.js", - "frame-helper.js", - "process-helper.js", - "service-worker-helper.js", - "service-worker-jsprocessactor-startup.js", - "worker-helper.js", -) diff --git a/devtools/server/actors/watcher/target-helpers/process-helper.js b/devtools/server/actors/watcher/target-helpers/process-helper.js deleted file mode 100644 index e36f0a204c..0000000000 --- a/devtools/server/actors/watcher/target-helpers/process-helper.js +++ /dev/null @@ -1,115 +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"; - -/** - * Return the list of all DOM Processes except the one for the parent process - * - * @return Array<nsIDOMProcessParent> - */ -function getAllContentProcesses() { - return ChromeUtils.getAllDOMProcesses().filter( - process => process.childID !== 0 - ); -} - -/** - * Instantiate all Content Process targets in all the DOM Processes. - * - * @param {WatcherActor} watcher - */ -async function createTargets(watcher) { - const promises = []; - for (const domProcess of getAllContentProcesses()) { - const processActor = domProcess.getActor("DevToolsProcess"); - promises.push( - processActor.instantiateTarget({ - watcherActorID: watcher.actorID, - connectionPrefix: watcher.conn.prefix, - sessionContext: watcher.sessionContext, - sessionData: watcher.sessionData, - }) - ); - } - await Promise.all(promises); -} - -/** - * @param {WatcherActor} watcher - * @param {object} options - * @param {boolean} options.isModeSwitching - * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref - */ -function destroyTargets(watcher, options) { - for (const domProcess of getAllContentProcesses()) { - const processActor = domProcess.getActor("DevToolsProcess"); - processActor.destroyTarget({ - watcherActorID: watcher.actorID, - isModeSwitching: options.isModeSwitching, - }); - } -} - -/** - * Go over all existing content processes in order to communicate about new data entries - * - * @param {Object} options - * @param {WatcherActor} options.watcher - * The Watcher Actor providing new data entries - * @param {string} options.type - * The type of data to be added - * @param {Array<Object>} options.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. - */ -async function addOrSetSessionDataEntry({ - watcher, - type, - entries, - updateType, -}) { - const promises = []; - for (const domProcess of getAllContentProcesses()) { - const processActor = domProcess.getActor("DevToolsProcess"); - promises.push( - processActor.addOrSetSessionDataEntry({ - watcherActorID: watcher.actorID, - type, - entries, - updateType, - }) - ); - } - await Promise.all(promises); -} - -/** - * Notify all existing content processes that some data entries have been removed - * - * See addOrSetSessionDataEntry for argument documentation. - */ -async function removeSessionDataEntry({ watcher, type, entries }) { - const promises = []; - for (const domProcess of getAllContentProcesses()) { - const processActor = domProcess.getActor("DevToolsProcess"); - promises.push( - processActor.removeSessionDataEntry({ - watcherActorID: watcher.actorID, - type, - entries, - }) - ); - } - await Promise.all(promises); -} - -module.exports = { - createTargets, - destroyTargets, - addOrSetSessionDataEntry, - removeSessionDataEntry, -}; diff --git a/devtools/server/actors/watcher/target-helpers/service-worker-helper.js b/devtools/server/actors/watcher/target-helpers/service-worker-helper.js deleted file mode 100644 index 53fceead17..0000000000 --- a/devtools/server/actors/watcher/target-helpers/service-worker-helper.js +++ /dev/null @@ -1,220 +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"; - -const { waitForTick } = require("resource://devtools/shared/DevToolsUtils.js"); - -const PROCESS_SCRIPT_URL = - "resource://devtools/server/actors/watcher/target-helpers/service-worker-jsprocessactor-startup.js"; - -const PROCESS_ACTOR_NAME = "DevToolsServiceWorker"; -const PROCESS_ACTOR_OPTIONS = { - // Ignore the parent process. - includeParent: false, - - parent: { - esModuleURI: - "resource://devtools/server/connectors/process-actor/DevToolsServiceWorkerParent.sys.mjs", - }, - - child: { - esModuleURI: - "resource://devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs", - - observers: [ - // Tried various notification to ensure starting the actor - // from webServiceWorker processes... but none of them worked. - /* - "chrome-event-target-created", - "webnavigation-create", - "chrome-webnavigation-create", - "webnavigation-destroy", - "chrome-webnavigation-destroy", - "browsing-context-did-set-embedder", - "browsing-context-discarded", - "ipc:content-initializing", - "ipc:content-created", - */ - - // Fallback on firing a very custom notification from a "process script" (loadProcessScript) - "init-devtools-service-worker-actor", - ], - }, -}; - -// List of all active watchers -const gWatchers = new Set(); - -/** - * Register the DevToolsServiceWorker JS Process Actor, - * if we are registering the first watcher actor. - * - * @param {Watcher Actor} watcher - */ -function maybeRegisterProcessActor(watcher) { - const sizeBefore = gWatchers.size; - gWatchers.add(watcher); - - if (sizeBefore == 0 && gWatchers.size == 1) { - ChromeUtils.registerProcessActor(PROCESS_ACTOR_NAME, PROCESS_ACTOR_OPTIONS); - - // For some reason JSProcessActor doesn't work out of the box for `webServiceWorker` content processes. - // So manually spawn our JSProcessActor from a process script emitting an observer service notification... - // The Process script are correctly executed on all process types during their early startup. - Services.ppmm.loadProcessScript(PROCESS_SCRIPT_URL, true); - } -} - -/** - * Unregister the DevToolsServiceWorker JS Process Actor, - * if we are unregistering the last watcher actor. - * - * @param {Watcher Actor} watcher - */ -function maybeUnregisterProcessActor(watcher) { - const sizeBefore = gWatchers.size; - gWatchers.delete(watcher); - - if (sizeBefore == 1 && gWatchers.size == 0) { - ChromeUtils.unregisterProcessActor( - PROCESS_ACTOR_NAME, - PROCESS_ACTOR_OPTIONS - ); - - Services.ppmm.removeDelayedProcessScript(PROCESS_SCRIPT_URL); - } -} - -/** - * Return the list of all DOM Processes except the one for the parent process - * - * @return Array<nsIDOMProcessParent> - */ -function getAllContentProcesses() { - return ChromeUtils.getAllDOMProcesses().filter( - process => process.childID !== 0 - ); -} - -/** - * Force creating targets for all existing service workers for a given Watcher Actor. - * - * @param WatcherActor watcher - * The Watcher Actor requesting to watch for new targets. - */ -async function createTargets(watcher) { - maybeRegisterProcessActor(watcher); - // Go over all existing content process in order to: - // - Force the instantiation of a DevToolsServiceWorkerChild - // - Have the DevToolsServiceWorkerChild to spawn the WorkerTargetActors - - const promises = []; - for (const process of getAllContentProcesses()) { - const promise = process - .getActor(PROCESS_ACTOR_NAME) - .instantiateServiceWorkerTargets({ - watcherActorID: watcher.actorID, - connectionPrefix: watcher.conn.prefix, - sessionContext: watcher.sessionContext, - sessionData: watcher.sessionData, - }); - promises.push(promise); - } - - // Await for the different queries in order to try to resolve only *after* we received - // the already available worker targets. - return Promise.all(promises); -} - -/** - * Force destroying all worker targets which were related to a given watcher. - * - * @param WatcherActor watcher - * The Watcher Actor requesting to stop watching for new targets. - */ -async function destroyTargets(watcher) { - // Go over all existing content processes in order to destroy all targets - for (const process of getAllContentProcesses()) { - let processActor; - try { - processActor = process.getActor(PROCESS_ACTOR_NAME); - } catch (e) { - // Ignore any exception during destroy as we may be closing firefox/devtools/tab - // and that can easily lead to many exceptions. - continue; - } - - processActor.destroyServiceWorkerTargets({ - watcherActorID: watcher.actorID, - sessionContext: watcher.sessionContext, - }); - } - - // browser_dbg-breakpoints-columns.js crashes if we unregister the Process Actor - // in the same event loop as we call destroyServiceWorkerTargets. - await waitForTick(); - - maybeUnregisterProcessActor(watcher); -} - -/** - * Go over all existing JSProcessActor in order to communicate about new data entries - * - * @param WatcherActor watcher - * The Watcher Actor requesting to update data entries. - * @param string type - * The type of data to be added - * @param Array<Object> 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. - */ -async function addOrSetSessionDataEntry({ - watcher, - type, - entries, - updateType, -}) { - maybeRegisterProcessActor(watcher); - const promises = []; - for (const process of getAllContentProcesses()) { - const promise = process - .getActor(PROCESS_ACTOR_NAME) - .addOrSetSessionDataEntry({ - watcherActorID: watcher.actorID, - sessionContext: watcher.sessionContext, - type, - entries, - updateType, - }); - promises.push(promise); - } - // Await for the queries in order to try to resolve only *after* the remote code processed the new data - return Promise.all(promises); -} - -/** - * Notify all existing frame targets that some data entries have been removed - * - * See addOrSetSessionDataEntry for argument documentation. - */ -function removeSessionDataEntry({ watcher, type, entries }) { - for (const process of getAllContentProcesses()) { - process.getActor(PROCESS_ACTOR_NAME).removeSessionDataEntry({ - watcherActorID: watcher.actorID, - sessionContext: watcher.sessionContext, - type, - entries, - }); - } -} - -module.exports = { - createTargets, - destroyTargets, - addOrSetSessionDataEntry, - removeSessionDataEntry, -}; diff --git a/devtools/server/actors/watcher/target-helpers/service-worker-jsprocessactor-startup.js b/devtools/server/actors/watcher/target-helpers/service-worker-jsprocessactor-startup.js deleted file mode 100644 index 03f042ad68..0000000000 --- a/devtools/server/actors/watcher/target-helpers/service-worker-jsprocessactor-startup.js +++ /dev/null @@ -1,26 +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"; - -const { setTimeout } = ChromeUtils.importESModule( - "resource://gre/modules/Timer.sys.mjs" -); - -/* - We can't spawn the JSProcessActor right away and have to spin the event loop. - Otherwise it isn't registered yet and isn't listening to observer service. - Could it be the reason why JSProcessActor aren't spawn via process actor option's child.observers notifications ?? -*/ -setTimeout(function () { - /* - This notification is registered in DevToolsServiceWorker JS process actor's options's `observers` attribute - and will force the JS Process actor to be instantiated in all processes. - */ - Services.obs.notifyObservers(null, "init-devtools-service-worker-actor"); - /* - Instead of using observer service, we could also manually call some method of the actor: - ChromeUtils.domProcessChild.getActor("DevToolsServiceWorker").observe(null, "foo"); - */ -}, 0); diff --git a/devtools/server/actors/watcher/target-helpers/worker-helper.js b/devtools/server/actors/watcher/target-helpers/worker-helper.js deleted file mode 100644 index 671d1dc706..0000000000 --- a/devtools/server/actors/watcher/target-helpers/worker-helper.js +++ /dev/null @@ -1,137 +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"; - -const DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME = "DevToolsWorker"; - -/** - * Force creating targets for all existing workers for a given Watcher Actor. - * - * @param WatcherActor watcher - * The Watcher Actor requesting to watch for new targets. - */ -async function createTargets(watcher) { - // Go over all existing BrowsingContext in order to: - // - Force the instantiation of a DevToolsWorkerChild - // - Have the DevToolsWorkerChild to spawn the WorkerTargetActors - const browsingContexts = watcher.getAllBrowsingContexts({ - acceptSameProcessIframes: true, - forceAcceptTopLevelTarget: true, - }); - const promises = []; - for (const browsingContext of browsingContexts) { - const promise = browsingContext.currentWindowGlobal - .getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME) - .instantiateWorkerTargets({ - watcherActorID: watcher.actorID, - connectionPrefix: watcher.conn.prefix, - sessionContext: watcher.sessionContext, - sessionData: watcher.sessionData, - }); - promises.push(promise); - } - - // Await for the different queries in order to try to resolve only *after* we received - // the already available worker targets. - return Promise.all(promises); -} - -/** - * Force destroying all worker targets which were related to a given watcher. - * - * @param WatcherActor watcher - * The Watcher Actor requesting to stop watching for new targets. - */ -async function destroyTargets(watcher) { - // Go over all existing BrowsingContext in order to destroy all targets - const browsingContexts = watcher.getAllBrowsingContexts({ - acceptSameProcessIframes: true, - forceAcceptTopLevelTarget: true, - }); - for (const browsingContext of browsingContexts) { - let windowActor; - try { - windowActor = browsingContext.currentWindowGlobal.getActor( - DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME - ); - } catch (e) { - continue; - } - - windowActor.destroyWorkerTargets({ - watcherActorID: watcher.actorID, - sessionContext: watcher.sessionContext, - }); - } -} - -/** - * Go over all existing BrowsingContext in order to communicate about new data entries - * - * @param WatcherActor watcher - * The Watcher Actor requesting to stop watching for new targets. - * @param string type - * The type of data to be added - * @param Array<Object> 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. - */ -async function addOrSetSessionDataEntry({ - watcher, - type, - entries, - updateType, -}) { - const browsingContexts = watcher.getAllBrowsingContexts({ - acceptSameProcessIframes: true, - forceAcceptTopLevelTarget: true, - }); - const promises = []; - for (const browsingContext of browsingContexts) { - const promise = browsingContext.currentWindowGlobal - .getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME) - .addOrSetSessionDataEntry({ - watcherActorID: watcher.actorID, - sessionContext: watcher.sessionContext, - type, - entries, - updateType, - }); - promises.push(promise); - } - // Await for the queries in order to try to resolve only *after* the remote code processed the new data - return Promise.all(promises); -} - -/** - * Notify all existing frame targets that some data entries have been removed - * - * See addOrSetSessionDataEntry for argument documentation. - */ -function removeSessionDataEntry({ watcher, type, entries }) { - const browsingContexts = watcher.getAllBrowsingContexts({ - acceptSameProcessIframes: true, - forceAcceptTopLevelTarget: true, - }); - for (const browsingContext of browsingContexts) { - browsingContext.currentWindowGlobal - .getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME) - .removeSessionDataEntry({ - watcherActorID: watcher.actorID, - sessionContext: watcher.sessionContext, - type, - entries, - }); - } -} - -module.exports = { - createTargets, - destroyTargets, - addOrSetSessionDataEntry, - removeSessionDataEntry, -}; diff --git a/devtools/server/actors/webconsole/commands/manager.js b/devtools/server/actors/webconsole/commands/manager.js index 025e197e3b..e96e0a617f 100644 --- a/devtools/server/actors/webconsole/commands/manager.js +++ b/devtools/server/actors/webconsole/commands/manager.js @@ -11,13 +11,6 @@ loader.lazyRequireGetter( true ); -loader.lazyRequireGetter( - this, - ["DOM_MUTATIONS"], - "resource://devtools/server/tracer/tracer.jsm", - true -); - loader.lazyGetter(this, "l10n", () => { return new Localization( [ @@ -27,6 +20,16 @@ loader.lazyGetter(this, "l10n", () => { true ); }); + +const lazy = {}; +ChromeUtils.defineESModuleGetters( + lazy, + { + JSTracer: "resource://devtools/server/tracer/tracer.sys.mjs", + }, + { global: "contextual" } +); + const USAGE_STRING_MAPPING = { block: "webconsole-commands-usage-block", trace: "webconsole-commands-usage-trace3", @@ -888,7 +891,7 @@ WebConsoleCommandsManager.register({ } else if (typeof args["dom-mutations"] == "string") { // Otherwise consider the value as coma seperated list and remove any white space. traceDOMMutations = args["dom-mutations"].split(",").map(e => e.trim()); - const acceptedValues = Object.values(DOM_MUTATIONS); + const acceptedValues = Object.values(lazy.JSTracer.DOM_MUTATIONS); if (!traceDOMMutations.every(e => acceptedValues.includes(e))) { throw new Error( `:trace --dom-mutations only accept a list of strings whose values can be: ${acceptedValues}` diff --git a/devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs b/devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs new file mode 100644 index 0000000000..41ce80c9fd --- /dev/null +++ b/devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs @@ -0,0 +1,430 @@ +/* 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/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters( + lazy, + { + releaseDistinctSystemPrincipalLoader: + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", + useDistinctSystemPrincipalLoader: + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", + loader: "resource://devtools/shared/loader/Loader.sys.mjs", + }, + { global: "contextual" } +); + +// Name of the attribute into which we save data in `sharedData` object. +const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; + +// Map(String => Object) +// Map storing the data objects for all currently active watcher actors. +// The data objects are defined by `createWatcherDataObject()`. +// The main attribute of interest is the `sessionData` one which is set alongside +// various other attributes necessary to maintain state per watcher in the content process. +// +// The Session Data object is maintained by ParentProcessWatcherRegistry, in the parent process +// and is fetched from the content process via `sharedData` API. +// It is then manually maintained via DevToolsProcess JS Actor queries. +let gAllWatcherData = null; + +export const ContentProcessWatcherRegistry = { + _getAllWatchersDataMap() { + if (gAllWatcherData) { + return gAllWatcherData; + } + const { sharedData } = Services.cpmm; + const sessionDataByWatcherActorID = sharedData.get(SHARED_DATA_KEY_NAME); + if (!sessionDataByWatcherActorID) { + throw new Error("Missing session data in `sharedData`"); + } + + // Initialize a distinct Map to replicate the one read from `sharedData`. + // This distinct Map will be updated via DevToolsProcess JS Actor queries. + // This helps better control the execution flow. + gAllWatcherData = new Map(); + + // The Browser Toolbox will load its server modules in a distinct global/compartment whose name is "DevTools global". + // (See https://searchfox.org/mozilla-central/rev/0e9ea50a999420d93df0e4e27094952af48dd3b8/js/xpconnect/loader/mozJSModuleLoader.cpp#699) + // It means that this class will be instantiated twice, one in each global (the shared one and the browser toolbox one). + // We then have to distinguish the two subset of watcher actors accordingly within `sharedMap`, + // as `sharedMap` will be shared between the two module instances. + // Session type "all" relates to the Browser Toolbox. + const isInBrowserToolboxLoader = + // eslint-disable-next-line mozilla/reject-globalThis-modification + Cu.getRealmLocation(globalThis) == "DevTools global"; + + for (const [watcherActorID, sessionData] of sessionDataByWatcherActorID) { + // Filter in/out the watchers based on the current module loader and the watcher session type. + const isBrowserToolboxWatcher = sessionData.sessionContext.type == "all"; + if ( + (isInBrowserToolboxLoader && !isBrowserToolboxWatcher) || + (!isInBrowserToolboxLoader && isBrowserToolboxWatcher) + ) { + continue; + } + + gAllWatcherData.set( + watcherActorID, + createWatcherDataObject(watcherActorID, sessionData) + ); + } + + return gAllWatcherData; + }, + + /** + * Get all data objects for all currently active watcher actors. + * If a specific target type is passed, this will only return objects of watcher actively watching for a given target type. + * + * @param {String} targetType + * Optional target type to filter only a subset of watchers. + * @return {Array|Iterator} + * List of data objects. (see createWatcherDataObject) + */ + getAllWatchersDataObjects(targetType) { + if (targetType) { + const list = []; + for (const watcherDataObject of this._getAllWatchersDataMap().values()) { + if (watcherDataObject.sessionData.targets?.includes(targetType)) { + list.push(watcherDataObject); + } + } + return list; + } + return this._getAllWatchersDataMap().values(); + }, + + /** + * Get the watcher data object for a given watcher actor. + * + * @param {String} watcherActorID + * @param {Boolean} onlyFromCache + * If set explicitly to true, will avoid falling back to shared data. + * This is typically useful on destructor/removing/cleanup to avoid creating unexpected data. + * It is also used to avoid the exception thrown when sharedData is cleared on toolbox destruction. + */ + getWatcherDataObject(watcherActorID, onlyFromCache = false) { + let data = + ContentProcessWatcherRegistry._getAllWatchersDataMap().get( + watcherActorID + ); + if (!data && !onlyFromCache) { + // When there is more than one DevTools opened, the DevToolsProcess JS Actor spawned by the first DevTools + // created a cached Map in `_getAllWatchersDataMap`. + // When opening a second DevTools, this cached Map may miss some new SessionData related to this new DevTools instance, + // and new Watcher Actor. + // When such scenario happens, fallback to `sharedData` which should hopefully be containing the latest DevTools instance SessionData. + // + // May be the watcher should trigger a very first JS Actor query before any others in order to transfer the base Session Data object? + const { sharedData } = Services.cpmm; + const sessionDataByWatcherActorID = sharedData.get(SHARED_DATA_KEY_NAME); + const sessionData = sessionDataByWatcherActorID.get(watcherActorID); + if (!sessionData) { + throw new Error("Unable to find data for watcher " + watcherActorID); + } + data = createWatcherDataObject(watcherActorID, sessionData); + gAllWatcherData.set(watcherActorID, data); + } + return data; + }, + + /** + * Instantiate a DevToolsServerConnection for a given Watcher. + * + * This function will be the one forcing to load the first DevTools CommonJS modules + * and spawning the DevTools Loader as well as the DevToolsServer. So better call it + * only once when it is strictly necessary. + * + * This connection will be the communication channel for RDP between this content process + * and the parent process, which will route RDP packets from/to the client by using + * a unique "forwarding prefix". + * + * @param {String} watcherActorID + * @param {Boolean} useDistinctLoader + * To be set to true when debugging a privileged context running the shared system principal global. + * This is a requirement for spidermonkey Debugger API used by the thread actor. + * @return {Object} + * Object with connection (DevToolsServerConnection) and loader (DevToolsLoader) attributes. + */ + getOrCreateConnectionForWatcher(watcherActorID, useDistinctLoader) { + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); + let { connection, loader } = watcherDataObject; + + if (connection) { + return { connection, loader }; + } + + const { sessionContext, forwardingPrefix } = watcherDataObject; + // For the browser toolbox, we need to use a distinct loader in order to debug privileged JS. + // The thread actor ultimately need to be in a distinct compartments from its debuggees. + loader = + useDistinctLoader || sessionContext.type == "all" + ? lazy.useDistinctSystemPrincipalLoader(watcherDataObject) + : lazy.loader; + watcherDataObject.loader = loader; + + // Note that this a key step in loading DevTools backend / modules. + const { DevToolsServer } = loader.require( + "resource://devtools/server/devtools-server.js" + ); + + DevToolsServer.init(); + + // Within the content process, we only need the target scoped actors. + // (inspector, console, storage,...) + DevToolsServer.registerActors({ target: true }); + + // Instantiate a DevToolsServerConnection which will pipe all its outgoing RDP packets + // up to the parent process manager via DevToolsProcess JS Actor messages. + connection = DevToolsServer.connectToParentWindowActor( + watcherDataObject.jsProcessActor, + forwardingPrefix, + "DevToolsProcessChild:packet" + ); + watcherDataObject.connection = connection; + + return { connection, loader }; + }, + + /** + * Method to be called each time a new target actor is instantiated. + * + * @param {Object} watcherDataObject + * @param {Actor} targetActor + * @param {Boolean} isDocumentCreation + */ + onNewTargetActor(watcherDataObject, targetActor, isDocumentCreation = false) { + // There is no root actor in content processes and so + // the target actor can't be managed by it, but we do have to manage + // the actor to have it working and be registered in the DevToolsServerConnection. + // We make it manage itself and become a top level actor. + targetActor.manage(targetActor); + + const { watcherActorID } = watcherDataObject; + targetActor.once("destroyed", options => { + // Maintain the registry and notify the parent process + ContentProcessWatcherRegistry.destroyTargetActor( + watcherDataObject, + targetActor, + options + ); + }); + + watcherDataObject.actors.push(targetActor); + + // Immediately queue a message for the parent process, + // in order to ensure that the JSWindowActorTransport is instantiated + // before any packet is sent from the content process. + // As messages are guaranteed to be delivered in the order they + // were queued, we don't have to wait for anything around this sendAsyncMessage call. + // In theory, the Target Actor may emit events in its constructor. + // If it does, such RDP packets may be lost. But in practice, no events + // are emitted during its construction. Instead the frontend will start + // the communication first. + const { forwardingPrefix } = watcherDataObject; + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:targetAvailable", + { + watcherActorID, + forwardingPrefix, + targetActorForm: targetActor.form(), + } + ); + + // Pass initialization data to the target actor + const { sessionData } = watcherDataObject; + for (const type in sessionData) { + // `sessionData` will also contain `browserId` as well as entries with empty arrays, + // which shouldn't be processed. + const entries = sessionData[type]; + if (!Array.isArray(entries) || !entries.length) { + continue; + } + targetActor.addOrSetSessionDataEntry( + type, + sessionData[type], + isDocumentCreation, + "set" + ); + } + }, + + /** + * Method to be called each time a target actor is meant to be destroyed. + * + * @param {Object} watcherDataObject + * @param {Actor} targetActor + * @param {object} options + * @param {boolean} options.isModeSwitching + * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref + */ + destroyTargetActor(watcherDataObject, targetActor, options) { + const idx = watcherDataObject.actors.indexOf(targetActor); + if (idx != -1) { + watcherDataObject.actors.splice(idx, 1); + } + const form = targetActor.form(); + targetActor.destroy(options); + + // And this will destroy the parent process one + try { + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:targetDestroyed", + { + actors: [ + { + watcherActorID: watcherDataObject.watcherActorID, + targetActorForm: form, + }, + ], + options, + } + ); + } catch (e) { + // Ignore exception when the JSProcessActorChild has already been destroyed. + // We often try to emit this message while the process is being destroyed, + // but sendAsyncMessage doesn't have time to complete and throws. + if ( + !e.message.includes("JSProcessActorChild cannot send at the moment") + ) { + throw e; + } + } + }, + + /** + * Method to know if a given Watcher Actor is still registered. + * + * @param {String} watcherActorID + * @return {Boolean} + */ + has(watcherActorID) { + return gAllWatcherData.has(watcherActorID); + }, + + /** + * Method to unregister a given Watcher Actor. + * + * @param {Object} watcherDataObject + */ + remove(watcherDataObject) { + // We do not need to destroy each actor individually as they + // are all registered in this DevToolsServerConnection, which will + // destroy all the registered actors. + if (watcherDataObject.connection) { + watcherDataObject.connection.close(); + } + // If we were using a distinct and dedicated loader, + // we have to manually release it. + if (watcherDataObject.loader && watcherDataObject.loader !== lazy.loader) { + lazy.releaseDistinctSystemPrincipalLoader(watcherDataObject); + } + + gAllWatcherData.delete(watcherDataObject.watcherActorID); + if (gAllWatcherData.size == 0) { + gAllWatcherData = null; + } + }, + + /** + * Method to know if there is no more Watcher registered. + * + * @return {Boolean} + */ + isEmpty() { + return !gAllWatcherData || gAllWatcherData.size == 0; + }, + + /** + * Method to unregister all the Watcher Actors + */ + clear() { + if (!gAllWatcherData) { + return; + } + // Query gAllWatcherData internal map directly as we don't want to re-create the map from sharedData + for (const watcherDataObject of gAllWatcherData.values()) { + ContentProcessWatcherRegistry.remove(watcherDataObject); + } + gAllWatcherData = null; + }, +}; + +function createWatcherDataObject(watcherActorID, sessionData) { + // The prefix of the DevToolsServerConnection of the Watcher Actor in the parent process. + // This is used to compute a unique ID for this process. + const parentConnectionPrefix = sessionData.connectionPrefix; + + // Compute a unique prefix, just for this DOM Process. + // (nsIDOMProcessChild's childID should be unique across processes) + // + // This prefix will be used to create a JSWindowActorTransport pair between content and parent processes. + // This is slightly hacky as we typically compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, + // but here, we can't have access to any DevTools connection as we could run really early in the content process startup. + // + // Ensure appending a final slash, otherwise the prefix may be the same between childID 1 and 10... + const forwardingPrefix = + parentConnectionPrefix + + "process" + + ChromeUtils.domProcessChild.childID + + "/"; + + // The browser toolbox uses a distinct JS Actor, loaded in the "devtools" ESM loader. + const jsActorName = + sessionData.sessionContext.type == "all" + ? "BrowserToolboxDevToolsProcess" + : "DevToolsProcess"; + const jsProcessActor = ChromeUtils.domProcessChild.getActor(jsActorName); + + return { + // {String} + // Actor ID for this watcher + watcherActorID, + + // {Array<String>} + // List of currently watched target types for this watcher + watchingTargetTypes: [], + + // {DevtoolsServerConnection} + // Connection bridge made from this content process to the parent process. + connection: null, + + // {JSActor} + // Reference to the related DevToolsProcessChild instance. + jsProcessActor, + + // {Object} + // Watcher's sessionContext object, which help identify the browser toolbox usecase. + sessionContext: sessionData.sessionContext, + + // {Object} + // Watcher's sessionData object, which is initiated with `sharedData` version, + // but is later updated on each Session Data update (addOrSetSessionDataEntry/removeSessionDataEntry). + // `sharedData` isn't timely updated and can be out of date. + sessionData, + + // {String} + // Prefix used against all RDP packets to route them correctly from/to this content process + forwardingPrefix, + + // {Array<Object>} + // List of active WindowGlobal and ContentProcess target actor instances. + actors: [], + + // {Array<Object>} + // We store workers independently as we don't have access to the TargetActor instance (it is in the worker thread) + // and we need to keep reference to some other specifics + // - {WorkerDebugger} dbg + workers: [], + + // {Set<Array<Object>>} + // A Set of arrays which will be populated with concurrent Session Data updates + // being done while a worker target is being instantiated. + // Each pending worker being initialized register a new dedicated array which will be removed + // from the Set once its initialization is over. + pendingWorkers: new Set(), + }; +} diff --git a/devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs b/devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs index 9e8ad64eea..d98c416e34 100644 --- a/devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs +++ b/devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs @@ -3,260 +3,260 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; +import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters( lazy, { - releaseDistinctSystemPrincipalLoader: - "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", - useDistinctSystemPrincipalLoader: - "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", + ProcessTargetWatcher: + "resource://devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs", + SessionDataHelpers: + "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", + ServiceWorkerTargetWatcher: + "resource://devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs", + WorkerTargetWatcher: + "resource://devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs", + WindowGlobalTargetWatcher: + "resource://devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs", }, { global: "contextual" } ); -// Name of the attribute into which we save data in `sharedData` object. -const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; - -// If true, log info about DOMProcess's being created. -const DEBUG = false; - -/** - * Print information about operation being done against each content process. - * - * @param {nsIDOMProcessChild} domProcessChild - * The process for which we should log a message. - * @param {String} message - * Message to log. - */ -function logDOMProcess(domProcessChild, message) { - if (!DEBUG) { - return; - } - dump(" [pid:" + domProcessChild + "] " + message + "\n"); -} +// TargetActorRegistery has to be shared between all devtools instances +// and so is loaded into the shared global. +ChromeUtils.defineESModuleGetters( + lazy, + { + TargetActorRegistry: + "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs", + }, + { global: "shared" } +); export class DevToolsProcessChild extends JSProcessActorChild { constructor() { super(); - // The map is indexed by the Watcher Actor ID. - // The values are objects containing the following properties: - // - connection: the DevToolsServerConnection itself - // - actor: the ContentProcessTargetActor instance - this._connections = new Map(); - - this._onConnectionChange = this._onConnectionChange.bind(this); + // The EventEmitter interface is used for DevToolsTransport's packet-received event. EventEmitter.decorate(this); } + #watchers = { + // Keys are target types, which are defined in this CommonJS Module: + // https://searchfox.org/mozilla-central/rev/0e9ea50a999420d93df0e4e27094952af48dd3b8/devtools/server/actors/targets/index.js#7-14 + // We avoid loading it as this ESM should be lightweight and avoid spawning DevTools CommonJS Loader until + // whe know we have to instantiate a Target Actor. + frame: { + // Number of active watcher actors currently watching for the given target type + activeListener: 0, + + // Instance of a target watcher class whose task is to observe new target instances + get watcher() { + return lazy.WindowGlobalTargetWatcher; + }, + }, + + process: { + activeListener: 0, + get watcher() { + return lazy.ProcessTargetWatcher; + }, + }, + + worker: { + activeListener: 0, + get watcher() { + return lazy.WorkerTargetWatcher; + }, + }, + + service_worker: { + activeListener: 0, + get watcher() { + return lazy.ServiceWorkerTargetWatcher; + }, + }, + }; + + #initialized = false; + + /** + * Called when this JSProcess Actor instantiate either when we start observing for first target types, + * or when the process just started. + */ instantiate() { - const { sharedData } = Services.cpmm; - const watchedDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME); - if (!watchedDataByWatcherActor) { - throw new Error( - "Request to instantiate the target(s) for the process, but `sharedData` is empty about watched targets" - ); + if (this.#initialized) { + return; } - - // Create one Target actor for each prefix/client which listen to processes - for (const [watcherActorID, sessionData] of watchedDataByWatcherActor) { - const { connectionPrefix } = sessionData; - - if (sessionData.targets?.includes("process")) { - this._createTargetActor(watcherActorID, connectionPrefix, sessionData); - } + this.#initialized = true; + // Create and watch for future target actors for each watcher currently watching some target types + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) { + this.#watchInitialTargetsForWatcher(watcherDataObject); } } /** - * Instantiate a new ProcessTarget for the given connection. + * Instantiate and watch future target actors based on the already watched targets. * - * @param String watcherActorID - * The ID of the WatcherActor who requested to observe and create these target actors. - * @param String parentConnectionPrefix - * The prefix of the DevToolsServerConnection of the Watcher Actor. - * This is used to compute a unique ID for the target actor. - * @param Object sessionData - * All data managed by the Watcher Actor and WatcherRegistry.sys.mjs, containing - * target types, resources types to be listened as well as breakpoints and any - * other data meant to be shared across processes and threads. + * @param Object watcherDataObject + * See ContentProcessWatcherRegistry. */ - _createTargetActor(watcherActorID, parentConnectionPrefix, sessionData) { - // This method will be concurrently called from `observe()` and `DevToolsProcessParent:instantiate-already-available` - // When the JSprocessActor initializes itself and when the watcher want to force instantiating existing targets. - // Simply ignore the second call as there is nothing to return, neither to wait for as this method is synchronous. - if (this._connections.has(watcherActorID)) { - return; - } - - // Compute a unique prefix, just for this DOM Process, - // which will be used to create a JSWindowActorTransport pair between content and parent processes. - // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, - // but here, we can't have access to any DevTools connection as we are really early in the content process startup - // XXX: nsIDOMProcessChild's childID should be unique across processes, I think. So that should be safe? - // (this.manager == nsIDOMProcessChild interface) - // Ensure appending a final slash, otherwise the prefix may be the same between childID 1 and 10... - const forwardingPrefix = - parentConnectionPrefix + "contentProcess" + this.manager.childID + "/"; - - logDOMProcess( - this.manager, - "Instantiate ContentProcessTarget with prefix: " + forwardingPrefix - ); - - const { connection, targetActor } = this._createConnectionAndActor( - watcherActorID, - forwardingPrefix, - sessionData - ); - this._connections.set(watcherActorID, { - connection, - actor: targetActor, - }); - - // Immediately queue a message for the parent process, - // in order to ensure that the JSWindowActorTransport is instantiated - // before any packet is sent from the content process. - // As the order of messages is guaranteed to be delivered in the order they - // were queued, we don't have to wait for anything around this sendAsyncMessage call. - // In theory, the ContentProcessTargetActor may emit events in its constructor. - // If it does, such RDP packets may be lost. But in practice, no events - // are emitted during its construction. Instead the frontend will start - // the communication first. - this.sendAsyncMessage("DevToolsProcessChild:connectFromContent", { - watcherActorID, - forwardingPrefix, - actor: targetActor.form(), - }); - - // Pass initialization data to the target actor - for (const type in sessionData) { - // `sessionData` will also contain `browserId` as well as entries with empty arrays, - // which shouldn't be processed. - const entries = sessionData[type]; - if (!Array.isArray(entries) || !entries.length) { - continue; - } - targetActor.addOrSetSessionDataEntry( - type, - sessionData[type], - false, - "set" + #watchInitialTargetsForWatcher(watcherDataObject) { + const { sessionData, sessionContext } = watcherDataObject; + + // About WebExtension, see note in addOrSetSessionDataEntry. + // Their target actor aren't created by this class, but session data is still managed by it + // and we need to pass the initial session data coming to already instantiated target actor. + if (sessionContext.type == "webextension") { + const { watcherActorID } = watcherDataObject; + const connectionPrefix = watcherActorID.replace(/watcher\d+$/, ""); + const targetActors = lazy.TargetActorRegistry.getTargetActors( + sessionContext, + connectionPrefix ); + if (targetActors.length) { + // Pass initialization data to the target actor + for (const type in sessionData) { + // `sessionData` will also contain `browserId` as well as entries with empty arrays, + // which shouldn't be processed. + const entries = sessionData[type]; + if (!Array.isArray(entries) || !entries.length) { + continue; + } + targetActors[0].addOrSetSessionDataEntry(type, entries, false, "set"); + } + } } - } - _destroyTargetActor(watcherActorID, isModeSwitching) { - const connectionInfo = this._connections.get(watcherActorID); - // This connection has already been cleaned? - if (!connectionInfo) { - throw new Error( - `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}` - ); + // Ignore the call if the watched targets property isn't populated yet. + // This typically happens when instantiating the JS Process Actor on toolbox opening, + // where the actor is spawn early and a watchTarget message comes later with the `targets` array set. + if (!sessionData.targets) { + return; } - connectionInfo.connection.close({ isModeSwitching }); - this._connections.delete(watcherActorID); - if (this._connections.size == 0) { - this.didDestroy({ isModeSwitching }); + + for (const targetType of sessionData.targets) { + this.#watchNewTargetTypeForWatcher(watcherDataObject, targetType, true); } } - _createConnectionAndActor(watcherActorID, forwardingPrefix, sessionData) { - if (!this.loader) { - this.loader = lazy.useDistinctSystemPrincipalLoader(this); + /** + * Instantiate and watch future target actors based on the already watched targets. + * + * @param Object watcherDataObject + * See ContentProcessWatcherRegistry. + * @param String targetType + * New typeof target to start watching. + * @param Boolean isProcessActorStartup + * True when we are watching for targets during this JS Process actor instantiation. + * It shouldn't be the case on toolbox opening, but only when a new process starts. + * On toolbox opening, the Actor will receive an explicit watchTargets query. + */ + #watchNewTargetTypeForWatcher( + watcherDataObject, + targetType, + isProcessActorStartup + ) { + const { watchingTargetTypes } = watcherDataObject; + // Ensure creating and watching only once per target type and watcher actor. + if (watchingTargetTypes.includes(targetType)) { + return; } - const { DevToolsServer } = this.loader.require( - "devtools/server/devtools-server" + watchingTargetTypes.push(targetType); + + // Update sessionData as watched target types are a Session Data + // used later for example by worker target watcher + lazy.SessionDataHelpers.addOrSetSessionDataEntry( + watcherDataObject.sessionData, + "targets", + [targetType], + "add" ); - const { ContentProcessTargetActor } = this.loader.require( - "devtools/server/actors/targets/content-process" - ); - - DevToolsServer.init(); + this.#watchers[targetType].activeListener++; - // For browser content toolbox, we do need a regular root actor and all tab - // actors, but don't need all the "browser actors" that are only useful when - // debugging the parent process via the browser toolbox. - DevToolsServer.registerActors({ target: true }); - DevToolsServer.on("connectionchange", this._onConnectionChange); + // Start listening for platform events when we are observing this type for the first time + if (this.#watchers[targetType].activeListener === 1) { + this.#watchers[targetType].watcher.watch(); + } - const connection = DevToolsServer.connectToParentWindowActor( - this, - forwardingPrefix, - "DevToolsProcessChild:packet" + // And instantiate targets for the already existing instances + this.#watchers[targetType].watcher.createTargetsForWatcher( + watcherDataObject, + isProcessActorStartup ); - - // Create the actual target actor. - const targetActor = new ContentProcessTargetActor(connection, { - sessionContext: sessionData.sessionContext, - }); - // There is no root actor in content processes and so - // the target actor can't be managed by it, but we do have to manage - // the actor to have it working and be registered in the DevToolsServerConnection. - // We make it manage itself and become a top level actor. - targetActor.manage(targetActor); - - const form = targetActor.form(); - targetActor.once("destroyed", options => { - // This will destroy the content process one - this._destroyTargetActor(watcherActorID, options.isModeSwitching); - // And this will destroy the parent process one - try { - this.sendAsyncMessage("DevToolsProcessChild:destroy", { - actors: [ - { - watcherActorID, - form, - }, - ], - options, - }); - } catch (e) { - // Ignore exception when the JSProcessActorChild has already been destroyed. - // We often try to emit this message while the process is being destroyed, - // but sendAsyncMessage doesn't have time to complete and throws. - if ( - !e.message.includes("JSProcessActorChild cannot send at the moment") - ) { - throw e; - } - } - }); - - return { connection, targetActor }; } /** - * Destroy the server once its last connection closes. Note that multiple - * frame scripts may be running in parallel and reuse the same server. + * Stop watching for all target types and destroy all existing targets actor + * related to a given watcher actor. + * + * @param {Object} watcherDataObject + * @param {String} targetType + * @param {Object} options */ - _onConnectionChange() { - if (this._destroyed) { + #unwatchTargetsForWatcher(watcherDataObject, targetType, options) { + const { watchingTargetTypes } = watcherDataObject; + const targetTypeIndex = watchingTargetTypes.indexOf(targetType); + // Ignore targetTypes which were not observed + if (targetTypeIndex === -1) { return; } - this._destroyed = true; + // Update to the new list of currently watched target types + watchingTargetTypes.splice(targetTypeIndex, 1); + + // Update sessionData as watched target types are a Session Data + // used later for example by worker target watcher + lazy.SessionDataHelpers.removeSessionDataEntry( + watcherDataObject.sessionData, + "targets", + [targetType] + ); - const { DevToolsServer } = this.loader.require( - "devtools/server/devtools-server" + this.#watchers[targetType].activeListener--; + + // Stop observing for platform events + if (this.#watchers[targetType].activeListener === 0) { + this.#watchers[targetType].watcher.unwatch(); + } + + // Destroy all targets which are still instantiated for this type + this.#watchers[targetType].watcher.destroyTargetsForWatcher( + watcherDataObject, + options ); - // Only destroy the server if there is no more connections to it. It may be - // used to debug another tab running in the same process. - if (DevToolsServer.hasConnection() || DevToolsServer.keepAlive) { - return; + // Unregister the watcher if we stopped watching for all target types + if (!watchingTargetTypes.length) { + ContentProcessWatcherRegistry.remove(watcherDataObject); } - DevToolsServer.off("connectionchange", this._onConnectionChange); - DevToolsServer.destroy(); + // If we removed the last watcher, clean the internal state of this class. + if (ContentProcessWatcherRegistry.isEmpty()) { + this.didDestroy(options); + } } /** - * Supported Queries + * Cleanup everything around a given watcher actor + * + * @param {Object} watcherDataObject */ + #destroyWatcher(watcherDataObject) { + const { watchingTargetTypes } = watcherDataObject; + // Clone the array as it will be modified during the loop execution + for (const targetType of [...watchingTargetTypes]) { + this.#unwatchTargetsForWatcher(watcherDataObject, targetType); + } + } + /** + * Used by DevTools Transport to send packets to the content process. + * + * @param {JSON} packet + * @param {String} prefix + */ sendPacket(packet, prefix) { this.sendAsyncMessage("DevToolsProcessChild:packet", { packet, prefix }); } @@ -276,23 +276,33 @@ export class DevToolsProcessChild extends JSProcessActorChild { } } + /** + * Called by the JSProcessActor API when the process process sent us a message. + */ receiveMessage(message) { switch (message.name) { - case "DevToolsProcessParent:instantiate-already-available": { - const { watcherActorID, connectionPrefix, sessionData } = message.data; - return this._createTargetActor( - watcherActorID, - connectionPrefix, - sessionData + case "DevToolsProcessParent:watchTargets": { + const { watcherActorID, targetType } = message.data; + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); + return this.#watchNewTargetTypeForWatcher( + watcherDataObject, + targetType ); } - case "DevToolsProcessParent:destroy": { - const { watcherActorID, isModeSwitching } = message.data; - return this._destroyTargetActor(watcherActorID, isModeSwitching); + case "DevToolsProcessParent:unwatchTargets": { + const { watcherActorID, targetType, options } = message.data; + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); + return this.#unwatchTargetsForWatcher( + watcherDataObject, + targetType, + options + ); } case "DevToolsProcessParent:addOrSetSessionDataEntry": { const { watcherActorID, type, entries, updateType } = message.data; - return this._addOrSetSessionDataEntry( + return this.#addOrSetSessionDataEntry( watcherActorID, type, entries, @@ -301,7 +311,20 @@ export class DevToolsProcessChild extends JSProcessActorChild { } case "DevToolsProcessParent:removeSessionDataEntry": { const { watcherActorID, type, entries } = message.data; - return this._removeSessionDataEntry(watcherActorID, type, entries); + return this.#removeSessionDataEntry(watcherActorID, type, entries); + } + case "DevToolsProcessParent:destroyWatcher": { + const { watcherActorID } = message.data; + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject( + watcherActorID, + true + ); + // The watcher may already be destroyed if the client unwatched for all target types. + if (watcherDataObject) { + return this.#destroyWatcher(watcherDataObject); + } + return null; } case "DevToolsProcessParent:packet": return this.emit("packet-received", message); @@ -312,51 +335,160 @@ export class DevToolsProcessChild extends JSProcessActorChild { } } - _getTargetActorForWatcherActorID(watcherActorID) { - const connectionInfo = this._connections.get(watcherActorID); - return connectionInfo?.actor; - } - - _addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) { - const targetActor = this._getTargetActorForWatcherActorID(watcherActorID); - if (!targetActor) { - throw new Error( - `No target actor for this Watcher Actor ID:"${watcherActorID}"` - ); - } - return targetActor.addOrSetSessionDataEntry( + /** + * The parent process requested that some session data have been added or set. + * + * @param {String} watcherActorID + * The Watcher Actor ID requesting to add new session data + * @param {String} type + * The type of data to be added + * @param {Array<Object>} 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. + */ + async #addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) { + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); + + // Maintain the copy of `sessionData` so that it is up-to-date when + // a new worker target needs to be instantiated + const { sessionData } = watcherDataObject; + lazy.SessionDataHelpers.addOrSetSessionDataEntry( + sessionData, type, entries, - false, updateType ); + + // This type is really specific to Service Workers and doesn't need to be transferred to any target. + // We only need to instantiate and destroy the target actors based on this new host. + const { watchingTargetTypes } = watcherDataObject; + if (type == "browser-element-host") { + if (watchingTargetTypes.includes("service_worker")) { + this.#watchers.service_worker.watcher.updateBrowserElementHost( + watcherDataObject + ); + } + return; + } + + const promises = []; + for (const targetActor of watcherDataObject.actors) { + promises.push( + targetActor.addOrSetSessionDataEntry(type, entries, false, updateType) + ); + } + + // Very special codepath for Web Extensions. + // Their WebExtension Target Actor is still created manually by WebExtensionDescritpor.getTarget, + // via a message manager. That, instead of being instantiated via the WatcherActor.watchTargets and this JSProcess actor. + // The Watcher Actor will still instantiate a JS Actor for the WebExt DOM Content Process + // and send the addOrSetSessionDataEntry query. But as the target actor isn't managed by the JS Actor, + // we have to manually retrieve it via the TargetActorRegistry. + if (sessionData.sessionContext.type == "webextension") { + const connectionPrefix = watcherActorID.replace(/watcher\d+$/, ""); + const targetActors = lazy.TargetActorRegistry.getTargetActors( + sessionData.sessionContext, + connectionPrefix + ); + // We will have a single match only in the DOM Process where the add-on runs + if (targetActors.length) { + promises.push( + targetActors[0].addOrSetSessionDataEntry( + type, + entries, + false, + updateType + ) + ); + } + } + await Promise.all(promises); + + if (watchingTargetTypes.includes("worker")) { + await this.#watchers.worker.watcher.addOrSetSessionDataEntry( + watcherDataObject, + type, + entries, + updateType + ); + } + if (watchingTargetTypes.includes("service_worker")) { + await this.#watchers.service_worker.watcher.addOrSetSessionDataEntry( + watcherDataObject, + type, + entries, + updateType + ); + } } - _removeSessionDataEntry(watcherActorID, type, entries) { - const targetActor = this._getTargetActorForWatcherActorID(watcherActorID); - // By the time we are calling this, the target may already have been destroyed. - if (!targetActor) { - return null; + /** + * The parent process requested that some session data have been removed. + * + * @param {String} watcherActorID + * The Watcher Actor ID requesting to remove session data + * @param {String}} type + * The type of data to be removed + * @param {Array<Object>} entries + * The values to be removed to this type of data + */ + #removeSessionDataEntry(watcherActorID, type, entries) { + const watcherDataObject = + ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID, true); + + // When we unwatch resources after targets during the devtools shutdown, + // the watcher will be removed on last target type unwatch. + if (!watcherDataObject) { + return; + } + + // Maintain the copy of `sessionData` so that it is up-to-date when + // a new worker target needs to be instantiated + lazy.SessionDataHelpers.removeSessionDataEntry( + watcherDataObject.sessionData, + type, + entries + ); + + for (const targetActor of watcherDataObject.actors) { + targetActor.removeSessionDataEntry(type, entries); } - return targetActor.removeSessionDataEntry(type, entries); } - observe(subject, topic) { + /** + * Observer service notification handler. + * + * @param {DOMWindow|Document} subject + * A window for *-document-global-created + * A document for *-page-{shown|hide} + * @param {String} topic + */ + observe = (subject, topic) => { if (topic === "init-devtools-content-process-actor") { // This is triggered by the process actor registration and some code in process-helper.js // which defines a unique topic to be observed this.instantiate(); } - } + }; - didDestroy(options) { - for (const { connection } of this._connections.values()) { - connection.close(options); - } - this._connections.clear(); - if (this.loader) { - lazy.releaseDistinctSystemPrincipalLoader(this); - this.loader = null; + /** + * Called by JS Process Actor API when the current process is destroyed, + * but also within this class when the last watcher stopped watching for targets. + */ + didDestroy() { + // Stop watching for all target types + for (const entry of Object.values(this.#watchers)) { + if (entry.activeListener > 0) { + entry.watcher.unwatch(); + entry.activeListener = 0; + } } + + ContentProcessWatcherRegistry.clear(); } } + +export class BrowserToolboxDevToolsProcessChild extends DevToolsProcessChild {} diff --git a/devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs b/devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs index 28e11def68..303c85e68f 100644 --- a/devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs +++ b/devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs @@ -5,9 +5,9 @@ import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs"; import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; -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" } ); @@ -24,8 +24,6 @@ export class DevToolsProcessParent extends JSProcessActorParent { constructor() { super(); - this._destroyed = false; - // Map of DevToolsServerConnection's used to forward the messages from/to // the client. The connections run in the parent process, as this code. We // may have more than one when there is more than one client debugging the @@ -44,41 +42,38 @@ export class DevToolsProcessParent extends JSProcessActorParent { // which can be considered as a kind of id. On top of this, parent process // DevToolsServerConnections also have forwarding prefixes because they are // responsible for forwarding messages to content process connections. - this._connections = new Map(); - this._onConnectionClosed = this._onConnectionClosed.bind(this); EventEmitter.decorate(this); } + #destroyed = false; + #connections = new Map(); + /** - * Request the content process to create the ContentProcessTarget + * Request the content process to create all the targets currently watched + * and start observing for new ones to be created later. */ - instantiateTarget({ - watcherActorID, - connectionPrefix, - sessionContext, - sessionData, - }) { - return this.sendQuery( - "DevToolsProcessParent:instantiate-already-available", - { - watcherActorID, - connectionPrefix, - sessionContext, - sessionData, - } - ); + watchTargets({ watcherActorID, targetType }) { + return this.sendQuery("DevToolsProcessParent:watchTargets", { + watcherActorID, + targetType, + }); } - destroyTarget({ watcherActorID, isModeSwitching }) { - this.sendAsyncMessage("DevToolsProcessParent:destroy", { + /** + * Request the content process to stop observing for currently watched targets + * and destroy all the currently active ones. + */ + unwatchTargets({ watcherActorID, targetType, options }) { + this.sendAsyncMessage("DevToolsProcessParent:unwatchTargets", { watcherActorID, - isModeSwitching, + targetType, + options, }); } /** - * Communicate to the content process that some data have been added. + * Communicate to the content process that some data have been added or set. */ addOrSetSessionDataEntry({ watcherActorID, type, entries, updateType }) { return this.sendQuery("DevToolsProcessParent:addOrSetSessionDataEntry", { @@ -100,8 +95,17 @@ export class DevToolsProcessParent extends JSProcessActorParent { }); } - connectFromContent({ watcherActorID, forwardingPrefix, actor }) { - const watcher = WatcherRegistry.getWatcher(watcherActorID); + destroyWatcher({ watcherActorID }) { + return this.sendAsyncMessage("DevToolsProcessParent:destroyWatcher", { + watcherActorID, + }); + } + + /** + * Called when the content process notified us about a new target actor + */ + #onTargetAvailable({ watcherActorID, forwardingPrefix, targetActorForm }) { + const watcher = ParentProcessWatcherRegistry.getWatcher(watcherActorID); if (!watcher) { throw new Error( @@ -110,45 +114,86 @@ export class DevToolsProcessParent extends JSProcessActorParent { } const connection = watcher.conn; - connection.on("closed", this._onConnectionClosed); + // If this is the first target actor for this watcher, + // hook up the DevToolsServerConnection which will bridge + // communication between the parent process DevToolsServer + // and the content process. + if (!this.#connections.get(watcher.conn.prefix)) { + connection.on("closed", this.#onConnectionClosed); - // Create a js-window-actor based transport. - const transport = new lazy.JsWindowActorTransport( - this, - forwardingPrefix, - "DevToolsProcessParent:packet" - ); - transport.hooks = { - onPacket: connection.send.bind(connection), - onClosed() {}, - }; - transport.ready(); + // Create a js-window-actor based transport. + const transport = new lazy.JsWindowActorTransport( + this, + forwardingPrefix, + "DevToolsProcessParent:packet" + ); + transport.hooks = { + onPacket: connection.send.bind(connection), + onClosed() {}, + }; + transport.ready(); - connection.setForwarding(forwardingPrefix, transport); + connection.setForwarding(forwardingPrefix, transport); - this._connections.set(watcher.conn.prefix, { - watcher, - connection, - // This prefix is the prefix of the DevToolsServerConnection, running - // in the content process, for which we should forward packets to, based on its prefix. - // While `watcher.connection` is also a DevToolsServerConnection, but from this process, - // the parent process. It is the one receiving Client packets and the one, from which - // we should forward packets from. - forwardingPrefix, - transport, - actor, - }); + this.#connections.set(watcher.conn.prefix, { + watcher, + connection, + // This prefix is the prefix of the DevToolsServerConnection, running + // in the content process, for which we should forward packets to, based on its prefix. + // While `watcher.connection` is also a DevToolsServerConnection, but from this process, + // the parent process. It is the one receiving Client packets and the one, from which + // we should forward packets from. + forwardingPrefix, + transport, + targetActorForms: [], + }); + } - watcher.notifyTargetAvailable(actor); + this.#connections + .get(watcher.conn.prefix) + .targetActorForms.push(targetActorForm); + + watcher.notifyTargetAvailable(targetActorForm); } - _onConnectionClosed(status, prefix) { - if (this._connections.has(prefix)) { - const { connection } = this._connections.get(prefix); - this._cleanupConnection(connection); + /** + * Called when the content process notified us about a target actor that has been destroyed. + */ + #onTargetDestroyed({ actors, options }) { + for (const { watcherActorID, targetActorForm } of actors) { + const watcher = ParentProcessWatcherRegistry.getWatcher(watcherActorID); + // As we instruct to destroy all targets when the watcher is destroyed, + // we may easily receive the target destruction notification *after* + // the watcher has been removed from the registry. + if (!watcher || watcher.isDestroyed()) { + continue; + } + watcher.notifyTargetDestroyed(targetActorForm, options); + const connectionInfo = this.#connections.get(watcher.conn.prefix); + if (connectionInfo) { + const idx = connectionInfo.targetActorForms.findIndex( + form => form.actor == targetActorForm.actor + ); + if (idx != -1) { + connectionInfo.targetActorForms.splice(idx, 1); + } + // Once the last active target is removed, disconnect the DevTools transport + // and cleanup everything bound to this DOM Process. We will re-instantiate + // a new connection/transport on the next reported target actor. + if (!connectionInfo.targetActorForms.length) { + this.#cleanupConnection(connectionInfo.connection); + } + } } } + #onConnectionClosed = (status, prefix) => { + if (this.#connections.has(prefix)) { + const { connection } = this.#connections.get(prefix); + this.#cleanupConnection(connection); + } + }; + /** * Close and unregister a given DevToolsServerConnection. * @@ -157,30 +202,27 @@ export class DevToolsProcessParent extends JSProcessActorParent { * @param {boolean} options.isModeSwitching * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref */ - async _cleanupConnection(connection, options = {}) { - const connectionInfo = this._connections.get(connection.prefix); - if (!connectionInfo) { - return; + async #cleanupConnection(connection, options = {}) { + const watcherConnectionInfo = this.#connections.get(connection.prefix); + if (watcherConnectionInfo) { + const { forwardingPrefix, transport } = watcherConnectionInfo; + if (transport) { + // If we have a child transport, the actor has already + // been created. We need to stop using this transport. + transport.close(options); + } + // When cancelling the forwarding, one RDP event is sent to the client to purge all requests + // and actors related to a given prefix. + // Be careful that any late RDP event would be ignored by the client passed this call. + connection.cancelForwarding(forwardingPrefix); } - const { forwardingPrefix, transport } = connectionInfo; - connection.off("closed", this._onConnectionClosed); - if (transport) { - // If we have a child transport, the actor has already - // been created. We need to stop using this transport. - transport.close(options); - } + connection.off("closed", this.#onConnectionClosed); - this._connections.delete(connection.prefix); - if (!this._connections.size) { - this._destroy(options); + this.#connections.delete(connection.prefix); + if (!this.#connections.size) { + this.#destroy(options); } - - // When cancelling the forwarding, one RDP event is sent to the client to purge all requests - // and actors related to a given prefix. Do this *after* calling _destroy which will emit - // the target-destroyed RDP event. This helps the Watcher Front retrieve the related target front, - // otherwise it would be too eagerly destroyed by the purge event. - connection.cancelForwarding(forwardingPrefix); } /** @@ -190,20 +232,26 @@ export class DevToolsProcessParent extends JSProcessActorParent { * @param {boolean} options.isModeSwitching * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref */ - _destroy(options) { - if (this._destroyed) { + #destroy(options) { + if (this.#destroyed) { return; } - this._destroyed = true; + this.#destroyed = true; - for (const { actor, connection, watcher } of this._connections.values()) { - watcher.notifyTargetDestroyed(actor, options); - this._cleanupConnection(connection, options); + for (const { + targetActorForms, + connection, + watcher, + } of this.#connections.values()) { + for (const actor of targetActorForms) { + watcher.notifyTargetDestroyed(actor, options); + } + this.#cleanupConnection(connection, options); } } /** - * Supported Queries + * Used by DevTools Transport to send packets to the content process. */ sendPacket(packet, prefix) { @@ -211,7 +259,7 @@ export class DevToolsProcessParent extends JSProcessActorParent { } /** - * JsWindowActor API + * JsProcessActor API */ async sendQuery(msg, args) { @@ -225,24 +273,43 @@ export class DevToolsProcessParent extends JSProcessActorParent { } } + /** + * Called by the JSProcessActor API when the content process sent us a message + */ receiveMessage(message) { switch (message.name) { - case "DevToolsProcessChild:connectFromContent": - return this.connectFromContent(message.data); + case "DevToolsProcessChild:targetAvailable": + return this.#onTargetAvailable(message.data); case "DevToolsProcessChild:packet": return this.emit("packet-received", message); - case "DevToolsProcessChild:destroy": - for (const { form, watcherActorID } of message.data.actors) { - const watcher = WatcherRegistry.getWatcher(watcherActorID); - // As we instruct to destroy all targets when the watcher is destroyed, - // we may easily receive the target destruction notification *after* - // the watcher has been removed from the registry. - if (watcher) { - watcher.notifyTargetDestroyed(form, message.data.options); - this._cleanupConnection(watcher.conn, message.data.options); - } + case "DevToolsProcessChild:targetDestroyed": + return this.#onTargetDestroyed(message.data); + case "DevToolsProcessChild:bf-cache-navigation-pageshow": { + const browsingContext = BrowsingContext.get( + message.data.browsingContextId + ); + for (const watcherActor of ParentProcessWatcherRegistry.getWatchersForBrowserId( + browsingContext.browserId + )) { + watcherActor.emit("bf-cache-navigation-pageshow", { + windowGlobal: browsingContext.currentWindowGlobal, + }); } return null; + } + case "DevToolsProcessChild:bf-cache-navigation-pagehide": { + const browsingContext = BrowsingContext.get( + message.data.browsingContextId + ); + for (const watcherActor of ParentProcessWatcherRegistry.getWatchersForBrowserId( + browsingContext.browserId + )) { + watcherActor.emit("bf-cache-navigation-pagehide", { + windowGlobal: browsingContext.currentWindowGlobal, + }); + } + return null; + } default: throw new Error( "Unsupported message in DevToolsProcessParent: " + message.name @@ -250,7 +317,12 @@ export class DevToolsProcessParent extends JSProcessActorParent { } } + /** + * Called by the JSProcessActor API when this content process is destroyed. + */ didDestroy() { - this._destroy(); + this.#destroy(); } } + +export class BrowserToolboxDevToolsProcessParent extends DevToolsProcessParent {} diff --git a/devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js b/devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js new file mode 100644 index 0000000000..fbc71e2d90 --- /dev/null +++ b/devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js @@ -0,0 +1,33 @@ +/* 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"; + +/* + We want this to only startup the DevToolsProcess JS Actor on process start + and not when we only register the JS Process Actor when watching the first target type. + The Watcher Actor will query each individual JS Process Actor and fine control + the ordering of requests. It is especially important to spawn the top level target first. +*/ +const isContentProcessStartup = !Services.ww + .getWindowEnumerator() + .hasMoreElements(); +if (isContentProcessStartup) { + /* + We can't spawn the JSProcessActor right away and have to spin the event loop. + Otherwise it isn't registered yet and isn't listening to observer service. + Could it be the reason why JSProcessActor aren't spawn via process actor option's child.observers notifications ?? + */ + Services.tm.dispatchToMainThread(() => { + /* + This notification is registered in DevToolsServiceWorker JS process actor's options's `observers` attribute + and will force the JS Process actor to be instantiated in all processes. + */ + Services.obs.notifyObservers(null, "init-devtools-content-process-actor"); + /* + Instead of using observer service, we could also manually call some method of the actor: + ChromeUtils.domProcessChild.getActor("DevToolsProcess").observe(null, "init-devtools-content-process-actor"); + */ + }); +} diff --git a/devtools/server/connectors/js-process-actor/moz.build b/devtools/server/connectors/js-process-actor/moz.build index e1a1f5dc9d..c1843b4e16 100644 --- a/devtools/server/connectors/js-process-actor/moz.build +++ b/devtools/server/connectors/js-process-actor/moz.build @@ -4,7 +4,13 @@ # 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/. +DIRS += [ + "target-watchers", +] + DevToolsModules( + "content-process-jsprocessactor-startup.js", + "ContentProcessWatcherRegistry.sys.mjs", "DevToolsProcessChild.sys.mjs", "DevToolsProcessParent.sys.mjs", ) diff --git a/devtools/server/connectors/process-actor/moz.build b/devtools/server/connectors/js-process-actor/target-watchers/moz.build index 63f768bd3c..0574b0399e 100644 --- a/devtools/server/connectors/process-actor/moz.build +++ b/devtools/server/connectors/js-process-actor/target-watchers/moz.build @@ -5,6 +5,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. DevToolsModules( - "DevToolsServiceWorkerChild.sys.mjs", - "DevToolsServiceWorkerParent.sys.mjs", + "process.sys.mjs", + "service_worker.sys.mjs", + "window-global.sys.mjs", + "worker.sys.mjs", ) diff --git a/devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs b/devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs new file mode 100644 index 0000000000..c2b6dd807c --- /dev/null +++ b/devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs @@ -0,0 +1,95 @@ +/* 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/. */ + +import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs"; + +function watch() { + // There is nothing to watch. This JS Process Actor will automatically be spawned + // for each new DOM Process. +} +function unwatch() {} + +function createTargetsForWatcher(watcherDataObject) { + // Always ignore the parent process. A special WindowGlobal target actor will be spawned. + if (ChromeUtils.domProcessChild.childID == 0) { + return; + } + + createContentProcessTargetActor(watcherDataObject); +} + +/** + * Instantiate a content process target actor for the current process + * and for a given watcher actor. + * + * @param {Object} watcherDataObject + */ +function createContentProcessTargetActor(watcherDataObject) { + logDOMProcess( + ChromeUtils.domProcessChild, + "Instantiate ContentProcessTarget" + ); + + const { connection, loader } = + ContentProcessWatcherRegistry.getOrCreateConnectionForWatcher( + watcherDataObject.watcherActorID + ); + + const { ContentProcessTargetActor } = loader.require( + "devtools/server/actors/targets/content-process" + ); + + // Create the actual target actor. + const targetActor = new ContentProcessTargetActor(connection, { + sessionContext: watcherDataObject.sessionContext, + }); + + ContentProcessWatcherRegistry.onNewTargetActor( + watcherDataObject, + targetActor + ); +} + +function destroyTargetsForWatcher(watcherDataObject, options) { + // Unregister and destroy the existing target actors for this target type + const actorsToDestroy = watcherDataObject.actors.filter( + actor => actor.targetType == "process" + ); + watcherDataObject.actors = watcherDataObject.actors.filter( + actor => actor.targetType != "process" + ); + + for (const actor of actorsToDestroy) { + ContentProcessWatcherRegistry.destroyTargetActor( + watcherDataObject, + actor, + options + ); + } +} + +// If true, log info about DOMProcess's being created. +const DEBUG = false; + +/** + * Print information about operation being done against each content process. + * + * @param {nsIDOMProcessChild} domProcessChild + * The process for which we should log a message. + * @param {String} message + * Message to log. + */ +function logDOMProcess(domProcessChild, message) { + if (!DEBUG) { + return; + } + dump(" [pid:" + domProcessChild + "] " + message + "\n"); +} + +export const ProcessTargetWatcher = { + watch, + unwatch, + createTargetsForWatcher, + destroyTargetsForWatcher, +}; diff --git a/devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs b/devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs new file mode 100644 index 0000000000..f2f307f297 --- /dev/null +++ b/devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs @@ -0,0 +1,51 @@ +/* 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/. */ + +import { WorkerTargetWatcherClass } from "resource://devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; +XPCOMUtils.defineLazyServiceGetter( + lazy, + "wdm", + "@mozilla.org/dom/workers/workerdebuggermanager;1", + "nsIWorkerDebuggerManager" +); + +class ServiceWorkerTargetWatcherClass extends WorkerTargetWatcherClass { + constructor() { + super("service_worker"); + } + + /** + * Called whenever the debugged browser element navigates to a new page + * and the URL's host changes. + * This is used to maintain the list of active Service Worker targets + * based on that host name. + * + * @param {Object} watcherDataObject + * See ContentProcessWatcherRegistry + */ + async updateBrowserElementHost(watcherDataObject) { + const { sessionData } = watcherDataObject; + + // Create target actor matching this new host. + // Note that we may be navigating to the same host name and the target will already exist. + const promises = []; + for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) { + const alreadyCreated = watcherDataObject.workers.some( + info => info.dbg === dbg + ); + if ( + this.shouldHandleWorker(sessionData, dbg, "service_worker") && + !alreadyCreated + ) { + promises.push(this.createWorkerTargetActor(watcherDataObject, dbg)); + } + } + await Promise.all(promises); + } +} + +export const ServiceWorkerTargetWatcher = new ServiceWorkerTargetWatcherClass(); diff --git a/devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs b/devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs new file mode 100644 index 0000000000..66c71cbc1e --- /dev/null +++ b/devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs @@ -0,0 +1,574 @@ +/* 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/. */ + +import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters( + lazy, + { + isWindowGlobalPartOfContext: + "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", + WindowGlobalLogger: + "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs", + }, + { global: "contextual" } +); + +// TargetActorRegistery has to be shared between all devtools instances +// and so is loaded into the shared global. +ChromeUtils.defineESModuleGetters( + lazy, + { + TargetActorRegistry: + "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs", + }, + { global: "shared" } +); + +const isEveryFrameTargetEnabled = Services.prefs.getBoolPref( + "devtools.every-frame-target.enabled", + false +); + +// If true, log info about DOMProcess's being created. +const DEBUG = false; + +/** + * Print information about operation being done against each Window Global. + * + * @param {WindowGlobalChild} windowGlobal + * The window global for which we should log a message. + * @param {String} message + * Message to log. + */ +function logWindowGlobal(windowGlobal, message) { + if (!DEBUG) { + return; + } + lazy.WindowGlobalLogger.logWindowGlobal(windowGlobal, message); +} + +function watch() { + // Set the following preference in this function, so that we can easily + // toggle these preferences on and off from tests and have the new value being picked up. + + // bfcache-in-parent changes significantly how navigation behaves. + // We may start reusing previously existing WindowGlobal and so reuse + // previous set of JSWindowActor pairs (i.e. DevToolsProcessParent/DevToolsProcessChild). + // When enabled, regular navigations may also change and spawn new BrowsingContexts. + // If the page we navigate from supports being stored in bfcache, + // the navigation will use a new BrowsingContext. And so force spawning + // a new top-level target. + ChromeUtils.defineLazyGetter( + lazy, + "isBfcacheInParentEnabled", + () => + Services.appinfo.sessionHistoryInParent && + Services.prefs.getBoolPref("fission.bfcacheInParent", false) + ); + + // Observe for all necessary event to track new and destroyed WindowGlobals. + Services.obs.addObserver(observe, "content-document-global-created"); + Services.obs.addObserver(observe, "chrome-document-global-created"); + Services.obs.addObserver(observe, "content-page-shown"); + Services.obs.addObserver(observe, "chrome-page-shown"); + Services.obs.addObserver(observe, "content-page-hidden"); + Services.obs.addObserver(observe, "chrome-page-hidden"); + Services.obs.addObserver(observe, "inner-window-destroyed"); + Services.obs.addObserver(observe, "initial-document-element-inserted"); +} + +function unwatch() { + // Observe for all necessary event to track new and destroyed WindowGlobals. + Services.obs.removeObserver(observe, "content-document-global-created"); + Services.obs.removeObserver(observe, "chrome-document-global-created"); + Services.obs.removeObserver(observe, "content-page-shown"); + Services.obs.removeObserver(observe, "chrome-page-shown"); + Services.obs.removeObserver(observe, "content-page-hidden"); + Services.obs.removeObserver(observe, "chrome-page-hidden"); + Services.obs.removeObserver(observe, "inner-window-destroyed"); + Services.obs.removeObserver(observe, "initial-document-element-inserted"); +} + +function createTargetsForWatcher(watcherDataObject, isProcessActorStartup) { + const { sessionContext } = watcherDataObject; + // Bug 1785266 - For now, in browser, when debugging the parent process (childID == 0), + // we spawn only the ParentProcessTargetActor, which will debug all the BrowsingContext running in the process. + // So that we have to avoid instantiating any here. + if ( + sessionContext.type == "all" && + ChromeUtils.domProcessChild.childID === 0 + ) { + return; + } + + function lookupForTargets(window) { + // Do not only track top level BrowsingContext in this content process, + // but also any nested iframe which may be running in the same process. + for (const browsingContext of window.docShell.browsingContext.getAllBrowsingContextsInSubtree()) { + const { currentWindowContext } = browsingContext; + // Only consider Window Global which are running in this process + if (!currentWindowContext || !currentWindowContext.isInProcess) { + continue; + } + + // WindowContext's windowGlobalChild should be defined for WindowGlobal running in this process + const { windowGlobalChild } = currentWindowContext; + + // getWindowEnumerator will expose somewhat unexpected WindowGlobal when a tab navigated. + // This will expose WindowGlobals of past navigations. Document which are in the bfcache + // and aren't the current WindowGlobal of their BrowsingContext. + if (!windowGlobalChild.isCurrentGlobal) { + continue; + } + + // Accept the initial about:blank document: + // - only from createTargetsForWatcher, when instantiating the target for the already existing WindowGlobals, + // - when we do that on toolbox opening, to prevent creating one when the process is starting. + // + // This is to allow debugging blank tabs, which are on an initial about:blank document. + // + // We want to avoid creating transient targets for initial about blank when a new WindowGlobal + // just get created as it will most likely navigate away just after and confuse the frontend with short lived target. + const acceptInitialDocument = !isProcessActorStartup; + + if ( + lazy.isWindowGlobalPartOfContext(windowGlobalChild, sessionContext, { + acceptInitialDocument, + }) + ) { + createWindowGlobalTargetActor(watcherDataObject, windowGlobalChild); + } else if ( + !browsingContext.parent && + sessionContext.browserId && + browsingContext.browserId == sessionContext.browserId && + browsingContext.window.document.isInitialDocument + ) { + // In order to succesfully get the devtools-html-content event in SourcesManager, + // we have to ensure flagging the initial about:blank document... + // While we don't create a target for it, we need to set this flag for this event to be emitted. + browsingContext.watchedByDevTools = true; + } + } + } + for (const window of Services.ww.getWindowEnumerator()) { + lookupForTargets(window); + + // `lookupForTargets` uses `getAllBrowsingContextsInSubTree`, but this will ignore browser elements + // using type="content". So manually retrieve the windows for these browser elements, + // in case we have tabs opened on document loaded in the same process. + // This codepath is meant when we are in the parent process, with browser.xhtml having these <browser type="content"> + // elements for tabs. + for (const browser of window.document.querySelectorAll( + `browser[type="content"]` + )) { + const childWindow = browser.browsingContext.window; + // If the tab isn't on a document loaded in the parent process, + // the window will be null. + if (childWindow) { + lookupForTargets(childWindow); + } + } + } +} + +function destroyTargetsForWatcher(watcherDataObject, options) { + // Unregister and destroy the existing target actors for this target type + const actorsToDestroy = watcherDataObject.actors.filter( + actor => actor.targetType == "frame" + ); + watcherDataObject.actors = watcherDataObject.actors.filter( + actor => actor.targetType != "frame" + ); + + for (const actor of actorsToDestroy) { + ContentProcessWatcherRegistry.destroyTargetActor( + watcherDataObject, + actor, + options + ); + } +} + +/** + * Called whenever a new WindowGlobal is instantiated either: + * - when navigating to a new page (DOMWindowCreated) + * - by a bfcache navigation (pageshow) + * + * @param {Window} window + * @param {Object} options + * @param {Boolean} options.isBFCache + * True, if the request to instantiate a new target comes from a bfcache navigation. + * i.e. when we receive a pageshow event with persisted=true. + * This will be true regardless of bfcacheInParent being enabled or disabled. + * @param {Boolean} options.ignoreIfExisting + * By default to false. If true is passed, we avoid instantiating a target actor + * if one already exists for this windowGlobal. + */ +function onWindowGlobalCreated( + window, + { isBFCache = false, ignoreIfExisting = false } = {} +) { + try { + const windowGlobal = window.windowGlobalChild; + + // For bfcache navigations, we only create new targets when bfcacheInParent is enabled, + // as this would be the only case where new DocShells will be created. This requires us to spawn a + // new WindowGlobalTargetActor as such actor is bound to a unique DocShell. + const forceAcceptTopLevelTarget = + isBFCache && lazy.isBfcacheInParentEnabled; + + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects( + "frame" + )) { + const { sessionContext } = watcherDataObject; + if ( + lazy.isWindowGlobalPartOfContext(windowGlobal, sessionContext, { + forceAcceptTopLevelTarget, + }) + ) { + // If this was triggered because of a navigation, we want to retrieve the existing + // target we were debugging so we can destroy it before creating the new target. + // This is important because we had cases where the destruction of an old target + // was unsetting a flag on the **new** target document, breaking the toolbox (See Bug 1721398). + + // We're checking for an existing target given a watcherActorID + browserId + browsingContext. + // Note that a target switch might create a new browsing context, so we wouldn't + // retrieve the existing target here. We are okay with this as: + // - this shouldn't happen much + // - in such case we weren't seeing the issue of Bug 1721398 (the old target can't access the new document) + const existingTarget = findTargetActor({ + watcherDataObject, + innerWindowId: windowGlobal.innerWindowId, + }); + + // See comment in `observe()` method and `DOMDocElementInserted` condition to know why we sometime + // ignore this method call if a target actor already exists. + // It means that we got a previous DOMWindowCreated event, related to a non-about:blank document, + // and we should ignore the DOMDocElementInserted. + // In any other scenario, destroy the already existing target and re-create a new one. + if (existingTarget && ignoreIfExisting) { + continue; + } + + // Bail if there is already an existing WindowGlobalTargetActor which wasn't + // created from a JSWIndowActor. + // This means we are reloading or navigating (same-process) a Target + // which has not been created using the Watcher, but from the client (most likely + // the initial target of a local-tab toolbox). + // However, we force overriding the first message manager based target in case of + // BFCache navigations. + if ( + existingTarget && + !existingTarget.createdFromJsWindowActor && + !isBFCache + ) { + continue; + } + + // If we decide to instantiate a new target and there was one before, + // first destroy the previous one. + // Otherwise its destroy sequence will be executed *after* the new one + // is being initialized and may easily revert changes made against platform API. + // (typically toggle platform boolean attributes back to default…) + if (existingTarget) { + existingTarget.destroy({ isTargetSwitching: true }); + } + + // When navigating to another process, the Watcher Actor won't have sent any query + // to the new process JS Actor as the debugged tab was on another process before navigation. + // But `sharedData` will have data about all the current watchers. + // Here we have to ensure calling watchTargetsForWatcher in order to populate #connections + // for the currently processed watcher actor and start listening for future targets. + if ( + !ContentProcessWatcherRegistry.has(watcherDataObject.watcherActorID) + ) { + throw new Error("Watcher data seems out of sync"); + } + + createWindowGlobalTargetActor(watcherDataObject, windowGlobal, true); + } + } + } catch (e) { + // Ensure logging exception as they are silently ignore otherwise + dump( + " Exception while observing a new window: " + e + "\n" + e.stack + "\n" + ); + } +} + +/** + * Called whenever a WindowGlobal just got destroyed, when closing the tab, or navigating to another one. + * + * @param {innerWindowId} innerWindowId + * The WindowGlobal's unique identifier. + */ +function onWindowGlobalDestroyed(innerWindowId) { + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects( + "frame" + )) { + const existingTarget = findTargetActor({ + watcherDataObject, + innerWindowId, + }); + + if (!existingTarget) { + continue; + } + + // Do not do anything if both bfcache in parent and server targets are disabled + // As history navigations will be handled within the same DocShell and by the + // same WindowGlobalTargetActor. The actor will listen to pageshow/pagehide by itself. + // We should not destroy any target. + if ( + !lazy.isBfcacheInParentEnabled && + !watcherDataObject.sessionContext.isServerTargetSwitchingEnabled + ) { + continue; + } + // If the target actor isn't in watcher data object, it is a top level actor + // instantiated via a Descriptor's getTarget method. It isn't registered into Watcher objects. + // But we still want to destroy such target actor, and need to manually emit the targetDestroyed to the parent process. + // Hopefully bug 1754452 should allow us to get rid of this workaround by making the top level actor + // be created and managed by the watcher universe, like all the others. + const isTopLevelActorRegisteredOutsideOfWatcherActor = + !watcherDataObject.actors.find( + actor => actor.innerWindowId == innerWindowId + ); + const targetActorForm = isTopLevelActorRegisteredOutsideOfWatcherActor + ? existingTarget.form() + : null; + + existingTarget.destroy(); + + if (isTopLevelActorRegisteredOutsideOfWatcherActor) { + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:targetDestroyed", + { + actors: [ + { + watcherActorID: watcherDataObject.watcherActorID, + targetActorForm, + }, + ], + options: {}, + } + ); + } + } +} + +/** + * Instantiate a WindowGlobal target actor for a given browsing context + * and for a given watcher actor. + * + * @param {Object} watcherDataObject + * @param {BrowsingContext} windowGlobalChild + * @param {Boolean} isDocumentCreation + */ +function createWindowGlobalTargetActor( + watcherDataObject, + windowGlobalChild, + isDocumentCreation = false +) { + logWindowGlobal(windowGlobalChild, "Instantiate WindowGlobalTarget"); + + // When debugging privileged pages running a the shared system compartment, and we aren't in the browser toolbox (which already uses a distinct loader), + // we have to use the distinct loader in order to ensure running DevTools in a distinct compartment than the page we are about to debug + // Such page could be about:addons, chrome://browser/content/browser.xhtml,... + const { browsingContext } = windowGlobalChild; + const useDistinctLoader = + browsingContext.associatedWindow.document.nodePrincipal.isSystemPrincipal; + const { connection, loader } = + ContentProcessWatcherRegistry.getOrCreateConnectionForWatcher( + watcherDataObject.watcherActorID, + useDistinctLoader + ); + + const { WindowGlobalTargetActor } = loader.require( + "devtools/server/actors/targets/window-global" + ); + + // In the case of the browser toolbox, tab's BrowsingContext don't have + // any parent BC and shouldn't be considered as top-level. + // This is why we check for browserId's. + const { sessionContext } = watcherDataObject; + const isTopLevelTarget = + !browsingContext.parent && + browsingContext.browserId == sessionContext.browserId; + + // Create the actual target actor. + const targetActor = new WindowGlobalTargetActor(connection, { + docShell: browsingContext.docShell, + // Targets created from the server side, via Watcher actor and DevToolsProcess JSWindow + // actor pairs are following WindowGlobal lifecycle. i.e. will be destroyed on any + // type of navigation/reload. + followWindowGlobalLifeCycle: true, + isTopLevelTarget, + ignoreSubFrames: isEveryFrameTargetEnabled, + sessionContext, + }); + targetActor.createdFromJsWindowActor = true; + + ContentProcessWatcherRegistry.onNewTargetActor( + watcherDataObject, + targetActor, + isDocumentCreation + ); +} + +/** + * Observer service notification handler. + * + * @param {DOMWindow|Document} subject + * A window for *-document-global-created + * A document for *-page-{shown|hide} + * @param {String} topic + */ +function observe(subject, topic) { + if ( + topic == "content-document-global-created" || + topic == "chrome-document-global-created" + ) { + onWindowGlobalCreated(subject); + } else if (topic == "inner-window-destroyed") { + const innerWindowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + onWindowGlobalDestroyed(innerWindowId); + } else if (topic == "content-page-shown" || topic == "chrome-page-shown") { + // The observer service notification doesn't receive the "persisted" DOM Event attribute, + // but thanksfully is fired just before the dispatching of that DOM event. + subject.defaultView.addEventListener("pageshow", handleEvent, { + capture: true, + once: true, + }); + } else if (topic == "content-page-hidden" || topic == "chrome-page-hidden") { + // Same as previous elseif branch + subject.defaultView.addEventListener("pagehide", handleEvent, { + capture: true, + once: true, + }); + } else if (topic == "initial-document-element-inserted") { + // We may be notified about SVG documents which we don't care about here. + if (!subject.location || !subject.defaultView) { + return; + } + // We might have ignored the DOMWindowCreated event because it was the initial about:blank document. + // But when loading same-process iframes, we reuse the WindowGlobal of the previously ignored about:bank document + // to load the actual URL loaded in the iframe. This means we won't have a new DOMWindowCreated + // for the actual document. But there is a DOMDocElementInserted fired just after, that we are processing here + // to create a target for same-process iframes. We only have to tell onWindowGlobalCreated to ignore + // the call if a target was created on the DOMWindowCreated event (if that was a non-about:blank document). + // + // All this means that we still do not create any target for the initial documents. + // It is complex to instantiate targets for initial documents because: + // - it would mean spawning two targets for the same WindowGlobal and sharing the same innerWindowId + // - or have WindowGlobalTargets to handle more than one document (it would mean reusing will-navigate/window-ready events + // both on client and server) + onWindowGlobalCreated(subject.defaultView, { ignoreIfExisting: true }); + } +} + +/** + * DOM Event handler. + * + * @param {String} type + * DOM event name + * @param {Boolean} persisted + * A flag set to true in cache of BFCache navigation + * @param {Document} target + * The navigating document + */ +function handleEvent({ type, persisted, target }) { + // If persisted=true, this is a BFCache navigation. + // + // With Fission enabled and bfcacheInParent, BFCache navigations will spawn a new DocShell + // in the same process: + // * the previous page won't be destroyed, its JSWindowActor will stay alive (`didDestroy` won't be called) + // and a 'pagehide' with persisted=true will be emitted on it. + // * the new page page won't emit any DOMWindowCreated, but instead a pageshow with persisted=true + // will be emitted. + if (type === "pageshow" && persisted) { + // Notify all bfcache navigations, even the one for which we don't create a new target + // as that's being useful for parent process storage resource watchers. + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) { + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:bf-cache-navigation-pageshow", + { + browsingContextId: target.defaultView.browsingContext.id, + } + ); + } + + // Here we are going to re-instantiate a target that got destroyed before while processing a pagehide event. + // We force instantiating a new top level target, within `instantiate()` even if server targets are disabled. + // But we only do that if bfcacheInParent is enabled. Otherwise for same-process, same-docshell bfcache navigation, + // we don't want to spawn new targets. + onWindowGlobalCreated(target.defaultView, { + isBFCache: true, + }); + } + + if (type === "pagehide" && persisted) { + // Notify all bfcache navigations, even the one for which we don't create a new target + // as that's being useful for parent process storage resource watchers. + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) { + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:bf-cache-navigation-pagehide", + { + browsingContextId: target.defaultView.browsingContext.id, + } + ); + } + + // We might navigate away for the first top level target, + // which isn't using JSWindowActor (it still uses messages manager and is created by the client, via TabDescriptor.getTarget). + // We have to unregister it from the TargetActorRegistry, otherwise, + // if we navigate back to it, the next DOMWindowCreated won't create a new target for it. + onWindowGlobalDestroyed(target.defaultView.windowGlobalChild.innerWindowId); + } +} + +/** + * Return an existing Window Global target for given a WatcherActor + * and against a given WindowGlobal. + * + * @param {Object} options + * @param {String} options.watcherDataObject + * @param {Number} options.innerWindowId + * The WindowGlobal inner window ID. + * + * @returns {WindowGlobalTargetActor|null} + */ +function findTargetActor({ watcherDataObject, innerWindowId }) { + // First let's check if a target was created for this watcher actor in this specific + // DevToolsProcessChild instance. + const targetActor = watcherDataObject.actors.find( + actor => actor.innerWindowId == innerWindowId + ); + if (targetActor) { + return targetActor; + } + + // Ensure retrieving the one target actor related to this connection. + // This allows to distinguish actors created for various toolboxes. + // For ex, regular toolbox versus browser console versus browser toolbox + const connectionPrefix = watcherDataObject.watcherActorID.replace( + /watcher\d+$/, + "" + ); + const targetActors = lazy.TargetActorRegistry.getTargetActors( + watcherDataObject.sessionContext, + connectionPrefix + ); + + return targetActors.find(actor => actor.innerWindowId == innerWindowId); +} + +export const WindowGlobalTargetWatcher = { + watch, + unwatch, + createTargetsForWatcher, + destroyTargetsForWatcher, +}; diff --git a/devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs b/devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs new file mode 100644 index 0000000000..0b67e8b038 --- /dev/null +++ b/devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs @@ -0,0 +1,457 @@ +/* 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/. */ + +import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; +XPCOMUtils.defineLazyServiceGetter( + lazy, + "wdm", + "@mozilla.org/dom/workers/workerdebuggermanager;1", + "nsIWorkerDebuggerManager" +); + +const { TYPE_DEDICATED, TYPE_SERVICE, TYPE_SHARED } = Ci.nsIWorkerDebugger; + +export class WorkerTargetWatcherClass { + constructor(workerTargetType = "worker") { + this.#workerTargetType = workerTargetType; + this.#workerDebuggerListener = { + onRegister: this.#onWorkerRegister.bind(this), + onUnregister: this.#onWorkerUnregister.bind(this), + }; + } + + // {String} + #workerTargetType; + // {nsIWorkerDebuggerListener} + #workerDebuggerListener; + + watch() { + lazy.wdm.addListener(this.#workerDebuggerListener); + } + + unwatch() { + lazy.wdm.removeListener(this.#workerDebuggerListener); + } + + createTargetsForWatcher(watcherDataObject) { + const { sessionData } = watcherDataObject; + for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) { + if (!this.shouldHandleWorker(sessionData, dbg, this.#workerTargetType)) { + continue; + } + this.createWorkerTargetActor(watcherDataObject, dbg); + } + } + + async addOrSetSessionDataEntry(watcherDataObject, type, entries, updateType) { + // Collect the SessionData update into `pendingWorkers` in order to notify + // about the updates to workers which are still in process of being hooked by devtools. + for (const concurrentSessionUpdates of watcherDataObject.pendingWorkers) { + concurrentSessionUpdates.push({ + type, + entries, + updateType, + }); + } + + const promises = []; + for (const { + dbg, + workerThreadServerForwardingPrefix, + } of watcherDataObject.workers) { + promises.push( + addOrSetSessionDataEntryInWorkerTarget({ + dbg, + workerThreadServerForwardingPrefix, + type, + entries, + updateType, + }) + ); + } + await Promise.all(promises); + } + + /** + * Called whenever a new Worker is instantiated in the current process + * + * @param {WorkerDebugger} dbg + */ + #onWorkerRegister(dbg) { + // Create a Target Actor for each watcher currently watching for Workers + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects( + this.#workerTargetType + )) { + const { sessionData } = watcherDataObject; + if (this.shouldHandleWorker(sessionData, dbg, this.#workerTargetType)) { + this.createWorkerTargetActor(watcherDataObject, dbg); + } + } + } + + /** + * Called whenever a Worker is destroyed in the current process + * + * @param {WorkerDebugger} dbg + */ + #onWorkerUnregister(dbg) { + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects( + this.#workerTargetType + )) { + const { watcherActorID, workers } = watcherDataObject; + // Check if the worker registration was handled for this watcherActorID. + const unregisteredActorIndex = workers.findIndex(worker => { + try { + // Accessing the WorkerDebugger id might throw (NS_ERROR_UNEXPECTED). + return worker.dbg.id === dbg.id; + } catch (e) { + return false; + } + }); + if (unregisteredActorIndex === -1) { + continue; + } + + const { workerTargetForm, transport } = workers[unregisteredActorIndex]; + // Close the transport made to the worker thread + transport.close(); + + try { + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:targetDestroyed", + { + actors: [ + { + watcherActorID, + targetActorForm: workerTargetForm, + }, + ], + options: {}, + } + ); + } catch (e) { + // This often throws as the JSActor is being destroyed when DevTools closes + // and we are trying to notify about the destroyed targets. + } + + workers.splice(unregisteredActorIndex, 1); + } + } + + /** + * Instantiate a worker target actor related to a given WorkerDebugger object + * and for a given watcher actor. + * + * @param {Object} watcherDataObject + * @param {WorkerDebugger} dbg + */ + async createWorkerTargetActor(watcherDataObject, dbg) { + // Prevent the debuggee from executing in this worker until the client has + // finished attaching to it. This call will throw if the debugger is already "registered" + // (i.e. if this is called outside of the register listener) + // See https://searchfox.org/mozilla-central/rev/84922363f4014eae684aabc4f1d06380066494c5/dom/workers/nsIWorkerDebugger.idl#55-66 + try { + dbg.setDebuggerReady(false); + } catch (e) { + if (!e.message.startsWith("Component returned failure code")) { + throw e; + } + } + + const { watcherActorID } = watcherDataObject; + const { connection, loader } = + ContentProcessWatcherRegistry.getOrCreateConnectionForWatcher( + watcherActorID + ); + + // Compute a unique prefix for the bridge made between this content process main thread + // and the worker thread. + const workerThreadServerForwardingPrefix = + connection.allocID("workerTarget"); + + const { connectToWorker } = loader.require( + "resource://devtools/server/connectors/worker-connector.js" + ); + + // Create the actual worker target actor, in the worker thread. + const { sessionData, sessionContext } = watcherDataObject; + const onConnectToWorker = connectToWorker( + connection, + dbg, + workerThreadServerForwardingPrefix, + { + sessionData, + sessionContext, + } + ); + + // Only add data to the connection if we successfully send the + // workerTargetAvailable message. + const workerInfo = { + dbg, + workerThreadServerForwardingPrefix, + }; + watcherDataObject.workers.push(workerInfo); + + // The onConnectToWorker is async and we may receive new Session Data (e.g breakpoints) + // while we are instantiating the worker targets. + // Let cache the pending session data and flush it after the targets are being instantiated. + const concurrentSessionUpdates = []; + watcherDataObject.pendingWorkers.add(concurrentSessionUpdates); + + try { + await onConnectToWorker; + } catch (e) { + // connectToWorker is supposed to call setDebuggerReady(true) to release the worker execution. + // But if anything goes wrong and an exception is thrown, ensure releasing its execution, + // otherwise if devtools is broken, it will freeze the worker indefinitely. + // + // onConnectToWorker can reject if the Worker Debugger is closed; so we only want to + // resume the debugger if it is not closed (otherwise it can cause crashes). + if (!dbg.isClosed) { + dbg.setDebuggerReady(true); + } + // Also unregister the worker + watcherDataObject.workers.splice( + watcherDataObject.workers.indexOf(workerInfo), + 1 + ); + watcherDataObject.pendingWorkers.delete(concurrentSessionUpdates); + return; + } + watcherDataObject.pendingWorkers.delete(concurrentSessionUpdates); + + const { workerTargetForm, transport } = await onConnectToWorker; + workerInfo.workerTargetForm = workerTargetForm; + workerInfo.transport = transport; + + const { forwardingPrefix } = watcherDataObject; + // Immediately queue a message for the parent process, before applying any SessionData + // as it may start emitting RDP events on the target actor and be lost if the client + // didn't get notified about the target actor first + try { + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:targetAvailable", + { + watcherActorID, + forwardingPrefix, + targetActorForm: workerTargetForm, + } + ); + } catch (e) { + // If there was an error while sending the message, we are not going to use this + // connection to communicate with the worker. + transport.close(); + // Also unregister the worker + watcherDataObject.workers.splice( + watcherDataObject.workers.indexOf(workerInfo), + 1 + ); + return; + } + + // Dispatch to the worker thread any SessionData updates which may have been notified + // while we were waiting for onConnectToWorker to resolve. + const promises = []; + for (const { type, entries, updateType } of concurrentSessionUpdates) { + promises.push( + addOrSetSessionDataEntryInWorkerTarget({ + dbg, + workerThreadServerForwardingPrefix, + type, + entries, + updateType, + }) + ); + } + await Promise.all(promises); + } + + destroyTargetsForWatcher(watcherDataObject) { + // Notify to all worker threads to destroy their target actor running in them + for (const { + dbg, + workerThreadServerForwardingPrefix, + transport, + } of watcherDataObject.workers) { + if (isWorkerDebuggerAlive(dbg)) { + try { + dbg.postMessage( + JSON.stringify({ + type: "disconnect", + forwardingPrefix: workerThreadServerForwardingPrefix, + }) + ); + } catch (e) {} + } + // Also cleanup the DevToolsTransport created in the main thread to bridge RDP to the worker thread + if (transport) { + transport.close(); + } + } + // Wipe all workers info + watcherDataObject.workers = []; + } + + /** + * Indicates whether or not we should handle the worker debugger + * + * @param {Object} sessionData + * The session data for a given watcher, which includes metadata + * about the debugged context. + * @param {WorkerDebugger} dbg + * The worker debugger we want to check. + * @param {String} targetType + * The expected worker target type. + * @returns {Boolean} + */ + shouldHandleWorker(sessionData, dbg, targetType) { + if (!isWorkerDebuggerAlive(dbg)) { + return false; + } + + if ( + (dbg.type === TYPE_DEDICATED && targetType != "worker") || + (dbg.type === TYPE_SERVICE && targetType != "service_worker") || + (dbg.type === TYPE_SHARED && targetType != "shared_worker") + ) { + return false; + } + + const { type: sessionContextType } = sessionData.sessionContext; + if (sessionContextType == "all") { + return true; + } + if (sessionContextType == "content-process") { + throw new Error( + "Content process session type shouldn't try to spawn workers" + ); + } + if (sessionContextType == "worker") { + throw new Error( + "worker session type should spawn only one target via the WorkerDescriptor" + ); + } + + if (dbg.type === TYPE_DEDICATED) { + // Assume that all dedicated workers executes in the same process as the debugged document. + const browsingContext = BrowsingContext.getCurrentTopByBrowserId( + sessionData.sessionContext.browserId + ); + // If we aren't executing in the same process as the worker and its BrowsingContext, + // it will be undefined. + if (!browsingContext) { + return false; + } + for (const subBrowsingContext of browsingContext.getAllBrowsingContextsInSubtree()) { + if ( + subBrowsingContext.currentWindowContext && + dbg.windowIDs.includes( + subBrowsingContext.currentWindowContext.innerWindowId + ) + ) { + return true; + } + } + return false; + } + + if (dbg.type === TYPE_SERVICE) { + // Accessing `nsIPrincipal.host` may easily throw on non-http URLs. + // Ignore all non-HTTP as they most likely don't have any valid host name. + if (!dbg.principal.scheme.startsWith("http")) { + return false; + } + + const workerHost = dbg.principal.hostPort; + return workerHost == sessionData["browser-element-host"][0]; + } + + if (dbg.type === TYPE_SHARED) { + // We still don't fully support instantiating targets for shared workers from the server side + throw new Error( + "Server side listening for shared workers isn't supported" + ); + } + + return false; + } +} + +/** + * Communicate the type and entries to the Worker Target actor, via the WorkerDebugger. + * + * @param {WorkerDebugger} dbg + * @param {String} workerThreadServerForwardingPrefix + * @param {String} type + * Session data type name + * @param {Array} entries + * Session data entries to add or set. + * @param {String} updateType + * Either "add" or "set", to control if we should only add some items, + * or replace the whole data set with the new entries. + * @returns {Promise} Returns a Promise that resolves once the data entry were handled + * by the worker target. + */ +function addOrSetSessionDataEntryInWorkerTarget({ + dbg, + workerThreadServerForwardingPrefix, + type, + entries, + updateType, +}) { + if (!isWorkerDebuggerAlive(dbg)) { + return Promise.resolve(); + } + + return new Promise(resolve => { + // Wait until we're notified by the worker that the resources are watched. + // This is important so we know existing resources were handled. + const listener = { + onMessage: message => { + message = JSON.parse(message); + if (message.type === "session-data-entry-added-or-set") { + dbg.removeListener(listener); + resolve(); + } + }, + // Resolve if the worker is being destroyed so we don't have a dangling promise. + onClose: () => { + dbg.removeListener(listener); + resolve(); + }, + }; + + dbg.addListener(listener); + + dbg.postMessage( + JSON.stringify({ + type: "add-or-set-session-data-entry", + forwardingPrefix: workerThreadServerForwardingPrefix, + dataEntryType: type, + entries, + updateType, + }) + ); + }); +} + +function isWorkerDebuggerAlive(dbg) { + if (dbg.isClosed) { + return false; + } + // Some workers are zombies. `isClosed` is false, but nothing works. + // `postMessage` is a noop, `addListener`'s `onClosed` doesn't work. + return ( + dbg.window?.docShell || + // consider dbg without `window` as being alive, as they aren't related + // to any docShell and probably do not suffer from this issue + !dbg.window + ); +} + +export const WorkerTargetWatcher = new WorkerTargetWatcherClass(); diff --git a/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs deleted file mode 100644 index acb5e97110..0000000000 --- a/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs +++ /dev/null @@ -1,710 +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/. */ - -import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; -import * as Loader from "resource://devtools/shared/loader/Loader.sys.mjs"; - -const lazy = {}; -ChromeUtils.defineESModuleGetters(lazy, { - isWindowGlobalPartOfContext: - "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", - releaseDistinctSystemPrincipalLoader: - "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", - TargetActorRegistry: - "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs", - useDistinctSystemPrincipalLoader: - "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", - WindowGlobalLogger: - "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs", -}); - -const isEveryFrameTargetEnabled = Services.prefs.getBoolPref( - "devtools.every-frame-target.enabled", - false -); - -// Name of the attribute into which we save data in `sharedData` object. -const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; - -// If true, log info about WindowGlobal's being created. -const DEBUG = false; -function logWindowGlobal(windowGlobal, message) { - if (!DEBUG) { - return; - } - lazy.WindowGlobalLogger.logWindowGlobal(windowGlobal, message); -} - -export class DevToolsFrameChild extends JSWindowActorChild { - constructor() { - super(); - - // The map is indexed by the Watcher Actor ID. - // The values are objects containing the following properties: - // - connection: the DevToolsServerConnection itself - // - actor: the WindowGlobalTargetActor instance - this._connections = new Map(); - - EventEmitter.decorate(this); - - // Set the following preference on the constructor, so that we can easily - // toggle these preferences on and off from tests and have the new value being picked up. - - // bfcache-in-parent changes significantly how navigation behaves. - // We may start reusing previously existing WindowGlobal and so reuse - // previous set of JSWindowActor pairs (i.e. DevToolsFrameParent/DevToolsFrameChild). - // When enabled, regular navigations may also change and spawn new BrowsingContexts. - // If the page we navigate from supports being stored in bfcache, - // the navigation will use a new BrowsingContext. And so force spawning - // a new top-level target. - ChromeUtils.defineLazyGetter( - this, - "isBfcacheInParentEnabled", - () => - Services.appinfo.sessionHistoryInParent && - Services.prefs.getBoolPref("fission.bfcacheInParent", false) - ); - } - - /** - * Try to instantiate new target actors for the current WindowGlobal, and that, - * for all the currently registered Watcher actors. - * - * Read the `sharedData` to get metadata about all registered watcher actors. - * If these watcher actors are interested in the current WindowGlobal, - * instantiate a new dedicated target actor for each of the watchers. - * - * @param Object options - * @param Boolean options.isBFCache - * True, if the request to instantiate a new target comes from a bfcache navigation. - * i.e. when we receive a pageshow event with persisted=true. - * This will be true regardless of bfcacheInParent being enabled or disabled. - * @param Boolean options.ignoreIfExisting - * By default to false. If true is passed, we avoid instantiating a target actor - * if one already exists for this windowGlobal. - */ - instantiate({ isBFCache = false, ignoreIfExisting = false } = {}) { - const { sharedData } = Services.cpmm; - const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME); - if (!sessionDataByWatcherActor) { - throw new Error( - "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets" - ); - } - - // Create one Target actor for each prefix/client which listen to frames - for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) { - const { connectionPrefix, sessionContext } = sessionData; - // For bfcache navigations, we only create new targets when bfcacheInParent is enabled, - // as this would be the only case where new DocShells will be created. This requires us to spawn a - // new WindowGlobalTargetActor as one such actor is bound to a unique DocShell. - const forceAcceptTopLevelTarget = - isBFCache && this.isBfcacheInParentEnabled; - if ( - sessionData.targets?.includes("frame") && - lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, { - forceAcceptTopLevelTarget, - }) - ) { - // If this was triggered because of a navigation, we want to retrieve the existing - // target we were debugging so we can destroy it before creating the new target. - // This is important because we had cases where the destruction of an old target - // was unsetting a flag on the **new** target document, breaking the toolbox (See Bug 1721398). - - // We're checking for an existing target given a watcherActorID + browserId + browsingContext - // Note that a target switch might create a new browsing context, so we wouldn't - // retrieve the existing target here. We are okay with this as: - // - this shouldn't happen much - // - in such case we weren't seeing the issue of Bug 1721398 (the old target can't access the new document) - const existingTarget = this._findTargetActor({ - watcherActorID, - sessionContext, - browsingContextId: this.manager.browsingContext.id, - }); - - // See comment in handleEvent(DOMDocElementInserted) to know why we try to - // create targets if none already exists - if (existingTarget && ignoreIfExisting) { - continue; - } - - // Bail if there is already an existing WindowGlobalTargetActor which wasn't - // created from a JSWIndowActor. - // This means we are reloading or navigating (same-process) a Target - // which has not been created using the Watcher, but from the client (most likely - // the initial target of a local-tab toolbox). - // However, we force overriding the first message manager based target in case of - // BFCache navigations. - if ( - existingTarget && - !existingTarget.createdFromJsWindowActor && - !isBFCache - ) { - continue; - } - - // If we decide to instantiate a new target and there was one before, - // first destroy the previous one. - // Otherwise its destroy sequence will be executed *after* the new one - // is being initialized and may easily revert changes made against platform API. - // (typically toggle platform boolean attributes back to default…) - if (existingTarget) { - existingTarget.destroy({ isTargetSwitching: true }); - } - - this._createTargetActor({ - watcherActorID, - parentConnectionPrefix: connectionPrefix, - sessionData, - isDocumentCreation: true, - }); - } - } - } - - /** - * Instantiate a new WindowGlobalTarget for the given connection. - * - * @param Object options - * @param String options.watcherActorID - * The ID of the WatcherActor who requested to observe and create these target actors. - * @param String options.parentConnectionPrefix - * The prefix of the DevToolsServerConnection of the Watcher Actor. - * This is used to compute a unique ID for the target actor. - * @param Object options.sessionData - * All data managed by the Watcher Actor and WatcherRegistry.sys.mjs, containing - * target types, resources types to be listened as well as breakpoints and any - * other data meant to be shared across processes and threads. - * @param Boolean options.isDocumentCreation - * Set to true if the function is called from `instantiate`, i.e. when we're - * handling a new document being created. - * @param Boolean options.fromInstantiateAlreadyAvailable - * Set to true if the function is called from handling `DevToolsFrameParent:instantiate-already-available` - * query. - */ - _createTargetActor({ - watcherActorID, - parentConnectionPrefix, - sessionData, - isDocumentCreation, - fromInstantiateAlreadyAvailable, - }) { - if (this._connections.get(watcherActorID)) { - // If this function is called as a result of a `DevToolsFrameParent:instantiate-already-available` - // message, we might have a legitimate race condition: - // In frame-helper, we want to create the initial targets for a given browser element. - // It might happen that the `DevToolsFrameParent:instantiate-already-available` is - // aborted if the page navigates (and the document is destroyed) while the query is still pending. - // In such case, frame-helper will try to send a new message. In the meantime, - // the DevToolsFrameChild `DOMWindowCreated` handler may already have been called and - // the new target already created. - // We don't want to throw in such case, as our end-goal, having a target for the document, - // is achieved. - if (fromInstantiateAlreadyAvailable) { - return; - } - throw new Error( - "DevToolsFrameChild _createTargetActor was called more than once" + - ` for the same Watcher (Actor ID: "${watcherActorID}")` - ); - } - - // Compute a unique prefix, just for this WindowGlobal, - // which will be used to create a JSWindowActorTransport pair between content and parent processes. - // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, - // but here, we can't have access to any DevTools connection as we are really early in the content process startup - // XXX: WindowGlobal's innerWindowId should be unique across processes, I think. So that should be safe? - // (this.manager == WindowGlobalChild interface) - const forwardingPrefix = - parentConnectionPrefix + "windowGlobal" + this.manager.innerWindowId; - - logWindowGlobal( - this.manager, - "Instantiate WindowGlobalTarget with prefix: " + forwardingPrefix - ); - - const { connection, targetActor } = this._createConnectionAndActor( - forwardingPrefix, - sessionData - ); - const form = targetActor.form(); - // Ensure unregistering and destroying the related DevToolsServerConnection+Transport - // on both content and parent process JSWindowActors. - targetActor.once("destroyed", options => { - // This will destroy the content process one - this._destroyTargetActor(watcherActorID, options); - // And this will destroy the parent process one - try { - this.sendAsyncMessage("DevToolsFrameChild:destroy", { - actors: [ - { - watcherActorID, - form, - }, - ], - options, - }); - } catch (e) { - // Ignore exception when the JSWindowActorChild has already been destroyed. - // We often try to emit this message while the WindowGlobal is in the process of being - // destroyed. We eagerly destroy the target actor during reloads, - // just before the WindowGlobal starts destroying, but sendAsyncMessage - // doesn't have time to complete and throws. - if ( - !e.message.includes("JSWindowActorChild cannot send at the moment") - ) { - throw e; - } - } - }); - this._connections.set(watcherActorID, { - connection, - actor: targetActor, - }); - - // Immediately queue a message for the parent process, - // in order to ensure that the JSWindowActorTransport is instantiated - // before any packet is sent from the content process. - // As the order of messages is guaranteed to be delivered in the order they - // were queued, we don't have to wait for anything around this sendAsyncMessage call. - // In theory, the WindowGlobalTargetActor may emit events in its constructor. - // If it does, such RDP packets may be lost. - // The important point here is to send this message before processing the sessionData, - // which will start the Watcher and start emitting resources on the target actor. - this.sendAsyncMessage("DevToolsFrameChild:connectFromContent", { - watcherActorID, - forwardingPrefix, - actor: targetActor.form(), - }); - - // Pass initialization data to the target actor - for (const type in sessionData) { - // `sessionData` will also contain `browserId` as well as entries with empty arrays, - // which shouldn't be processed. - const entries = sessionData[type]; - if (!Array.isArray(entries) || !entries.length) { - continue; - } - targetActor.addOrSetSessionDataEntry( - type, - entries, - isDocumentCreation, - "set" - ); - } - } - - /** - * @param {string} watcherActorID - * @param {object} options - * @param {boolean} options.isModeSwitching - * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref - */ - _destroyTargetActor(watcherActorID, options) { - const connectionInfo = this._connections.get(watcherActorID); - // This connection has already been cleaned? - if (!connectionInfo) { - throw new Error( - `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}` - ); - } - connectionInfo.connection.close(options); - this._connections.delete(watcherActorID); - if (this._connections.size == 0) { - this.didDestroy(options); - } - } - - _createConnectionAndActor(forwardingPrefix, sessionData) { - this.useCustomLoader = this.document.nodePrincipal.isSystemPrincipal; - - if (!this.loader) { - // When debugging chrome pages, use a new dedicated loader, using a distinct chrome compartment. - this.loader = this.useCustomLoader - ? lazy.useDistinctSystemPrincipalLoader(this) - : Loader; - } - const { DevToolsServer } = this.loader.require( - "resource://devtools/server/devtools-server.js" - ); - - const { WindowGlobalTargetActor } = this.loader.require( - "resource://devtools/server/actors/targets/window-global.js" - ); - - DevToolsServer.init(); - - // We want a special server without any root actor and only target-scoped actors. - // We are going to spawn a WindowGlobalTargetActor instance in the next few lines, - // it is going to act like a root actor without being one. - DevToolsServer.registerActors({ target: true }); - - const connection = DevToolsServer.connectToParentWindowActor( - this, - forwardingPrefix - ); - - // In the case of the browser toolbox, tab's BrowsingContext don't have - // any parent BC and shouldn't be considered as top-level. - // This is why we check for browserId's. - const browsingContext = this.manager.browsingContext; - const isTopLevelTarget = - !browsingContext.parent && - browsingContext.browserId == sessionData.sessionContext.browserId; - - // Create the actual target actor. - const targetActor = new WindowGlobalTargetActor(connection, { - docShell: this.docShell, - // Targets created from the server side, via Watcher actor and DevToolsFrame JSWindow - // actor pair are following WindowGlobal lifecycle. i.e. will be destroyed on any - // type of navigation/reload. - followWindowGlobalLifeCycle: true, - isTopLevelTarget, - ignoreSubFrames: isEveryFrameTargetEnabled, - sessionContext: sessionData.sessionContext, - }); - // There is no root actor in content processes and so - // the target actor can't be managed by it, but we do have to manage - // the actor to have it working and be registered in the DevToolsServerConnection. - // We make it manage itself and become a top level actor. - targetActor.manage(targetActor); - targetActor.createdFromJsWindowActor = true; - - return { connection, targetActor }; - } - - /** - * Supported Queries - */ - - sendPacket(packet, prefix) { - this.sendAsyncMessage("DevToolsFrameChild:packet", { packet, prefix }); - } - - /** - * JsWindowActor API - */ - - async sendQuery(msg, args) { - try { - const res = await super.sendQuery(msg, args); - return res; - } catch (e) { - console.error("Failed to sendQuery in DevToolsFrameChild", msg); - console.error(e.toString()); - throw e; - } - } - - receiveMessage(message) { - // Assert that the message is intended for this window global, - // except for "packet" messages which use a dedicated routing - if ( - message.name != "DevToolsFrameParent:packet" && - message.data.sessionContext.type == "browser-element" - ) { - const { browserId } = message.data.sessionContext; - // Re-check here, just to ensure that both parent and content processes agree - // on what should or should not be watched. - if ( - this.manager.browsingContext.browserId != browserId && - !lazy.isWindowGlobalPartOfContext( - this.manager, - message.data.sessionContext, - { - forceAcceptTopLevelTarget: true, - } - ) - ) { - throw new Error( - "Mismatch between DevToolsFrameParent and DevToolsFrameChild " + - (this.manager.browsingContext.browserId == browserId - ? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)" - : `expected browsing context with browserId ${browserId}, but got ${this.manager.browsingContext.browserId}`) - ); - } - } - switch (message.name) { - case "DevToolsFrameParent:instantiate-already-available": { - const { watcherActorID, connectionPrefix, sessionData } = message.data; - - return this._createTargetActor({ - watcherActorID, - parentConnectionPrefix: connectionPrefix, - sessionData, - fromInstantiateAlreadyAvailable: true, - }); - } - case "DevToolsFrameParent:destroy": { - const { watcherActorID, options } = message.data; - return this._destroyTargetActor(watcherActorID, options); - } - case "DevToolsFrameParent:addOrSetSessionDataEntry": { - const { watcherActorID, sessionContext, type, entries, updateType } = - message.data; - return this._addOrSetSessionDataEntry( - watcherActorID, - sessionContext, - type, - entries, - updateType - ); - } - case "DevToolsFrameParent:removeSessionDataEntry": { - const { watcherActorID, sessionContext, type, entries } = message.data; - return this._removeSessionDataEntry( - watcherActorID, - sessionContext, - type, - entries - ); - } - case "DevToolsFrameParent:packet": - return this.emit("packet-received", message); - default: - throw new Error( - "Unsupported message in DevToolsFrameParent: " + message.name - ); - } - } - - /** - * Return an existing target given a WatcherActor id, a browserId and an optional - * browsing context id. - * /!\ Note that we may have multiple targets for a given (watcherActorId, browserId) couple, - * for example if we have 2 remote iframes sharing the same origin, which is why you - * might want to pass a specific browsing context id to filter the list down. - * - * @param {Object} options - * @param {Object} options.watcherActorID - * @param {Object} options.sessionContext - * @param {Object} options.browsingContextId: Optional browsing context id to narrow the - * search to a specific browsing context. - * - * @returns {WindowGlobalTargetActor|null} - */ - _findTargetActor({ watcherActorID, sessionContext, browsingContextId }) { - // First let's check if a target was created for this watcher actor in this specific - // DevToolsFrameChild instance. - const connectionInfo = this._connections.get(watcherActorID); - const targetActor = connectionInfo ? connectionInfo.actor : null; - if (targetActor) { - return targetActor; - } - - // If we couldn't find such target, we want to see if a target was created for this - // (watcherActorId,browserId, {browsingContextId}) in another DevToolsFrameChild instance. - // This might be the case if we're navigating to a new page with server side target - // enabled and we want to retrieve the target of the page we're navigating from. - if ( - lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, { - forceAcceptTopLevelTarget: true, - }) - ) { - // Ensure retrieving the one target actor related to this connection. - // This allows to distinguish actors created for various toolboxes. - // For ex, regular toolbox versus browser console versus browser toolbox - const connectionPrefix = watcherActorID.replace(/watcher\d+$/, ""); - const targetActors = lazy.TargetActorRegistry.getTargetActors( - sessionContext, - connectionPrefix - ); - - if (!browsingContextId) { - return targetActors[0] || null; - } - return targetActors.find( - actor => actor.browsingContextID === browsingContextId - ); - } - return null; - } - - _addOrSetSessionDataEntry( - watcherActorID, - sessionContext, - type, - entries, - updateType - ) { - // /!\ We may have an issue here as there could be multiple targets for a given - // (watcherActorID,browserId) pair. - // This should be clarified as part of Bug 1725623. - const targetActor = this._findTargetActor({ - watcherActorID, - sessionContext, - }); - - if (!targetActor) { - throw new Error( - `No target actor for this Watcher Actor ID:"${watcherActorID}" / BrowserId:${sessionContext.browserId}` - ); - } - return targetActor.addOrSetSessionDataEntry( - type, - entries, - false, - updateType - ); - } - - _removeSessionDataEntry(watcherActorID, sessionContext, type, entries) { - // /!\ We may have an issue here as there could be multiple targets for a given - // (watcherActorID,browserId) pair. - // This should be clarified as part of Bug 1725623. - const targetActor = this._findTargetActor({ - watcherActorID, - sessionContext, - }); - // By the time we are calling this, the target may already have been destroyed. - if (!targetActor) { - return null; - } - return targetActor.removeSessionDataEntry(type, entries); - } - - handleEvent({ type, persisted, target }) { - // Ignore any event that may fire for children WindowGlobals/documents - if (target != this.document) { - return; - } - - // DOMWindowCreated is registered from FrameWatcher via `ActorManagerParent.addJSWindowActors` - // as a DOM event to be listened to and so is fired by JS Window Actor code platform code. - if (type == "DOMWindowCreated") { - this.instantiate(); - return; - } - // We might have ignored the DOMWindowCreated event because it was the initial about:blank document. - // But when loading same-process iframes, we reuse the WindowGlobal of the about:bank document - // to load the actual URL loaded in the iframe. This means we won't have a new DOMWindowCreated - // for the actual document. There is a DOMDocElementInserted fired just after, that we can catch - // to create a target for same-process iframes. - // This means that we still do not create any target for the initial documents. - // It is complex to instantiate targets for initial documents because: - // - it would mean spawning two targets for the same WindowGlobal and sharing the same innerWindowId - // - or have WindowGlobalTargets to handle more than one document (it would mean reusing will-navigate/window-ready events - // both on client and server) - if (type == "DOMDocElementInserted") { - this.instantiate({ ignoreIfExisting: true }); - return; - } - - // If persisted=true, this is a BFCache navigation. - // - // With Fission enabled and bfcacheInParent, BFCache navigations will spawn a new DocShell - // in the same process: - // * the previous page won't be destroyed, its JSWindowActor will stay alive (`didDestroy` won't be called) - // and a 'pagehide' with persisted=true will be emitted on it. - // * the new page page won't emit any DOMWindowCreated, but instead a pageshow with persisted=true - // will be emitted. - - if (type === "pageshow" && persisted) { - // Notify all bfcache navigations, even the one for which we don't create a new target - // as that's being useful for parent process storage resource watchers. - this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pageshow"); - - // Here we are going to re-instantiate a target that got destroyed before while processing a pagehide event. - // We force instantiating a new top level target, within `instantiate()` even if server targets are disabled. - // But we only do that if bfcacheInParent is enabled. Otherwise for same-process, same-docshell bfcache navigation, - // we don't want to spawn new targets. - this.instantiate({ - isBFCache: true, - }); - return; - } - - if (type === "pagehide" && persisted) { - // Notify all bfcache navigations, even the one for which we don't create a new target - // as that's being useful for parent process storage resource watchers. - this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pagehide"); - - // We might navigate away for the first top level target, - // which isn't using JSWindowActor (it still uses messages manager and is created by the client, via TabDescriptor.getTarget). - // We have to unregister it from the TargetActorRegistry, otherwise, - // if we navigate back to it, the next DOMWindowCreated won't create a new target for it. - const { sharedData } = Services.cpmm; - const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME); - if (!sessionDataByWatcherActor) { - throw new Error( - "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets" - ); - } - - const actors = []; - // A flag to know if the following for loop ended up destroying all the actors. - // It may not be the case if one Watcher isn't having server target switching enabled. - let allActorsAreDestroyed = true; - for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) { - const { sessionContext } = sessionData; - - // /!\ We may have an issue here as there could be multiple targets for a given - // (watcherActorID,browserId) pair. - // This should be clarified as part of Bug 1725623. - const existingTarget = this._findTargetActor({ - watcherActorID, - sessionContext, - }); - - if (!existingTarget) { - continue; - } - - // Use `originalWindow` as `window` can be set when a document was selected from - // the iframe picker in the toolbox toolbar. - if (existingTarget.originalWindow.document != target) { - throw new Error("Existing target actor is for a distinct document"); - } - // Do not do anything if both bfcache in parent and server targets are disabled - // As history navigations will be handled within the same DocShell and by the - // same WindowGlobalTargetActor. The actor will listen to pageshow/pagehide by itself. - // We should not destroy any target. - if ( - !this.isBfcacheInParentEnabled && - !sessionContext.isServerTargetSwitchingEnabled - ) { - allActorsAreDestroyed = false; - continue; - } - - actors.push({ - watcherActorID, - form: existingTarget.form(), - }); - existingTarget.destroy(); - } - - if (actors.length) { - // The most important is to unregister the actor from TargetActorRegistry, - // so that it is no longer present in the list when new DOMWindowCreated fires. - // This will also help notify the client that the target has been destroyed. - // And if we navigate back to this target, the client will receive the same target actor ID, - // so that it is really important to destroy it correctly on both server and client. - this.sendAsyncMessage("DevToolsFrameChild:destroy", { actors }); - } - - if (allActorsAreDestroyed) { - // Completely clear this JSWindow Actor. - // Do this after having called _findTargetActor, - // as it would clear the registered target actors. - this.didDestroy(); - } - } - } - - didDestroy(options) { - logWindowGlobal(this.manager, "Destroy WindowGlobalTarget"); - for (const { connection } of this._connections.values()) { - connection.close(options); - } - this._connections.clear(); - - if (this.loader) { - if (this.useCustomLoader) { - lazy.releaseDistinctSystemPrincipalLoader(this); - } - this.loader = null; - } - } -} diff --git a/devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs deleted file mode 100644 index 31750d58e4..0000000000 --- a/devtools/server/connectors/js-window-actor/DevToolsFrameParent.sys.mjs +++ /dev/null @@ -1,277 +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/. */ - -import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs"; -import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; - -const { WatcherRegistry } = ChromeUtils.importESModule( - "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs", - // WatcherRegistry needs to be a true singleton and loads ActorManagerParent - // which also has to be a true singleton. - { global: "shared" } -); - -const lazy = {}; - -loader.lazyRequireGetter( - lazy, - "JsWindowActorTransport", - "resource://devtools/shared/transport/js-window-actor-transport.js", - true -); - -export class DevToolsFrameParent extends JSWindowActorParent { - constructor() { - super(); - - // Map of DevToolsServerConnection's used to forward the messages from/to - // the client. The connections run in the parent process, as this code. We - // may have more than one when there is more than one client debugging the - // same frame. For example, a content toolbox and the browser toolbox. - // - // The map is indexed by the connection prefix. - // The values are objects containing the following properties: - // - actor: the frame target actor(as a form) - // - connection: the DevToolsServerConnection used to communicate with the - // frame target actor - // - prefix: the forwarding prefix used by the connection to know - // how to forward packets to the frame target - // - transport: the JsWindowActorTransport - // - // Reminder about prefixes: all DevToolsServerConnections have a `prefix` - // which can be considered as a kind of id. On top of this, parent process - // DevToolsServerConnections also have forwarding prefixes because they are - // responsible for forwarding messages to content process connections. - this._connections = new Map(); - - this._onConnectionClosed = this._onConnectionClosed.bind(this); - EventEmitter.decorate(this); - } - - /** - * Request the content process to create the Frame Target if there is one - * already available that matches the Browsing Context ID - */ - async instantiateTarget({ - watcherActorID, - connectionPrefix, - sessionContext, - sessionData, - }) { - await this.sendQuery("DevToolsFrameParent:instantiate-already-available", { - watcherActorID, - connectionPrefix, - sessionContext, - sessionData, - }); - } - - /** - * @param {object} arg - * @param {object} arg.sessionContext - * @param {object} arg.options - * @param {boolean} arg.options.isModeSwitching - * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref - */ - destroyTarget({ watcherActorID, sessionContext, options }) { - this.sendAsyncMessage("DevToolsFrameParent:destroy", { - watcherActorID, - sessionContext, - options, - }); - } - - /** - * Communicate to the content process that some data have been added. - */ - async addOrSetSessionDataEntry({ - watcherActorID, - sessionContext, - type, - entries, - updateType, - }) { - try { - await this.sendQuery("DevToolsFrameParent:addOrSetSessionDataEntry", { - watcherActorID, - sessionContext, - type, - entries, - updateType, - }); - } catch (e) { - console.warn( - "Failed to add session data entry for frame targets in browsing context", - this.browsingContext.id - ); - console.warn(e); - } - } - - /** - * Communicate to the content process that some data have been removed. - */ - removeSessionDataEntry({ watcherActorID, sessionContext, type, entries }) { - this.sendAsyncMessage("DevToolsFrameParent:removeSessionDataEntry", { - watcherActorID, - sessionContext, - type, - entries, - }); - } - - connectFromContent({ watcherActorID, forwardingPrefix, actor }) { - const watcher = WatcherRegistry.getWatcher(watcherActorID); - - if (!watcher) { - throw new Error( - `Watcher Actor with ID '${watcherActorID}' can't be found.` - ); - } - const connection = watcher.conn; - - connection.on("closed", this._onConnectionClosed); - - // Create a js-window-actor based transport. - const transport = new lazy.JsWindowActorTransport(this, forwardingPrefix); - transport.hooks = { - onPacket: connection.send.bind(connection), - onTransportClosed() {}, - }; - transport.ready(); - - connection.setForwarding(forwardingPrefix, transport); - - this._connections.set(watcher.conn.prefix, { - watcher, - connection, - // This prefix is the prefix of the DevToolsServerConnection, running - // in the content process, for which we should forward packets to, based on its prefix. - // While `watcher.connection` is also a DevToolsServerConnection, but from this process, - // the parent process. It is the one receiving Client packets and the one, from which - // we should forward packets from. - forwardingPrefix, - transport, - actor, - }); - - watcher.notifyTargetAvailable(actor); - } - - _onConnectionClosed(status, connectionPrefix) { - this._unregisterWatcher(connectionPrefix); - } - - /** - * Given a watcher connection prefix, unregister everything related to the Watcher - * in this JSWindowActor. - * - * @param {String} connectionPrefix - * The connection prefix of the watcher to unregister - */ - async _unregisterWatcher(connectionPrefix) { - const connectionInfo = this._connections.get(connectionPrefix); - if (!connectionInfo) { - return; - } - const { forwardingPrefix, transport, connection } = connectionInfo; - this._connections.delete(connectionPrefix); - - connection.off("closed", this._onConnectionClosed); - if (transport) { - // If we have a child transport, the actor has already - // been created. We need to stop using this transport. - transport.close(); - } - - connection.cancelForwarding(forwardingPrefix); - } - - /** - * Destroy everything that we did related to the current WindowGlobal that - * this JSWindow Actor represents: - * - close all transports that were used as bridge to communicate with the - * DevToolsFrameChild, running in the content process - * - unregister these transports from DevToolsServer (cancelForwarding) - * - notify the client, via the WatcherActor that all related targets, - * one per client/connection are all destroyed - * - * Note that with bfcacheInParent, we may reuse a JSWindowActor pair after closing all connections. - * This is can happen outside of the destruction of the actor. - * We may reuse a DevToolsFrameParent and DevToolsFrameChild pair. - * When navigating away, we will destroy them and call this method. - * Then when navigating back, we will reuse the same instances. - * So that we should be careful to keep the class fully function and only clear all its state. - * - * @param {object} options - * @param {boolean} options.isModeSwitching - * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref - */ - _closeAllConnections(options) { - for (const { actor, watcher } of this._connections.values()) { - watcher.notifyTargetDestroyed(actor, options); - this._unregisterWatcher(watcher.conn.prefix); - } - this._connections.clear(); - } - - /** - * Supported Queries - */ - - sendPacket(packet, prefix) { - this.sendAsyncMessage("DevToolsFrameParent:packet", { packet, prefix }); - } - - /** - * JsWindowActor API - */ - - receiveMessage(message) { - switch (message.name) { - case "DevToolsFrameChild:connectFromContent": - return this.connectFromContent(message.data); - case "DevToolsFrameChild:packet": - return this.emit("packet-received", message); - case "DevToolsFrameChild:destroy": - for (const { form, watcherActorID } of message.data.actors) { - const watcher = WatcherRegistry.getWatcher(watcherActorID); - // As we instruct to destroy all targets when the watcher is destroyed, - // we may easily receive the target destruction notification *after* - // the watcher has been removed from the registry. - if (watcher) { - watcher.notifyTargetDestroyed(form, message.data.options); - this._unregisterWatcher(watcher.conn.prefix); - } - } - return null; - case "DevToolsFrameChild:bf-cache-navigation-pageshow": - for (const watcherActor of WatcherRegistry.getWatchersForBrowserId( - this.browsingContext.browserId - )) { - watcherActor.emit("bf-cache-navigation-pageshow", { - windowGlobal: this.browsingContext.currentWindowGlobal, - }); - } - return null; - case "DevToolsFrameChild:bf-cache-navigation-pagehide": - for (const watcherActor of WatcherRegistry.getWatchersForBrowserId( - this.browsingContext.browserId - )) { - watcherActor.emit("bf-cache-navigation-pagehide", { - windowGlobal: this.browsingContext.currentWindowGlobal, - }); - } - return null; - default: - throw new Error( - "Unsupported message in DevToolsFrameParent: " + message.name - ); - } - } - - didDestroy() { - this._closeAllConnections(); - } -} diff --git a/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs deleted file mode 100644 index 6bbe4140c3..0000000000 --- a/devtools/server/connectors/js-window-actor/DevToolsWorkerChild.sys.mjs +++ /dev/null @@ -1,571 +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/. */ - -import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; - -import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; - -const lazy = {}; - -XPCOMUtils.defineLazyServiceGetter( - lazy, - "wdm", - "@mozilla.org/dom/workers/workerdebuggermanager;1", - "nsIWorkerDebuggerManager" -); - -ChromeUtils.defineLazyGetter(lazy, "Loader", () => - ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs") -); - -ChromeUtils.defineLazyGetter(lazy, "DevToolsUtils", () => - lazy.Loader.require("resource://devtools/shared/DevToolsUtils.js") -); -XPCOMUtils.defineLazyModuleGetters(lazy, { - SessionDataHelpers: - "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm", -}); -ChromeUtils.defineESModuleGetters(lazy, { - isWindowGlobalPartOfContext: - "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", -}); - -// Name of the attribute into which we save data in `sharedData` object. -const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; - -export class DevToolsWorkerChild extends JSWindowActorChild { - constructor() { - super(); - - // The map is indexed by the Watcher Actor ID. - // The values are objects containing the following properties: - // - connection: the DevToolsServerConnection itself - // - workers: An array of object containing the following properties: - // - dbg: A WorkerDebuggerInstance - // - workerTargetForm: The associated worker target instance form - // - workerThreadServerForwardingPrefix: The prefix used to forward events to the - // worker target on the worker thread (). - // - forwardingPrefix: Prefix used by the JSWindowActorTransport pair to communicate - // between content and parent processes. - // - sessionData: Data (targets, resources, …) the watcher wants to be notified about. - // See WatcherRegistry.getSessionData to see the full list of properties. - this._connections = new Map(); - - EventEmitter.decorate(this); - } - - _onWorkerRegistered(dbg) { - if (!this._shouldHandleWorker(dbg)) { - return; - } - - for (const [watcherActorID, { connection, forwardingPrefix }] of this - ._connections) { - this._createWorkerTargetActor({ - dbg, - connection, - forwardingPrefix, - watcherActorID, - }); - } - } - - _onWorkerUnregistered(dbg) { - for (const [watcherActorID, { workers, forwardingPrefix }] of this - ._connections) { - // Check if the worker registration was handled for this watcherActorID. - const unregisteredActorIndex = workers.findIndex(worker => { - try { - // Accessing the WorkerDebugger id might throw (NS_ERROR_UNEXPECTED). - return worker.dbg.id === dbg.id; - } catch (e) { - return false; - } - }); - if (unregisteredActorIndex === -1) { - continue; - } - - const { workerTargetForm, transport } = workers[unregisteredActorIndex]; - transport.close(); - - try { - this.sendAsyncMessage("DevToolsWorkerChild:workerTargetDestroyed", { - watcherActorID, - forwardingPrefix, - workerTargetForm, - }); - } catch (e) { - return; - } - - workers.splice(unregisteredActorIndex, 1); - } - } - - onDOMWindowCreated() { - const { sharedData } = Services.cpmm; - const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME); - if (!sessionDataByWatcherActor) { - throw new Error( - "Request to instantiate the target(s) for the Worker, but `sharedData` is empty about watched targets" - ); - } - - // Create one Target actor for each prefix/client which listen to workers - for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) { - const { targets, connectionPrefix, sessionContext } = sessionData; - if ( - targets?.includes("worker") && - lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, { - acceptInitialDocument: true, - forceAcceptTopLevelTarget: true, - acceptSameProcessIframes: true, - }) - ) { - this._watchWorkerTargets({ - watcherActorID, - parentConnectionPrefix: connectionPrefix, - sessionData, - }); - } - } - } - - /** - * Function handling messages sent by DevToolsWorkerParent (part of JSWindowActor API). - * - * @param {Object} message - * @param {String} message.name - * @param {*} message.data - */ - receiveMessage(message) { - // All messages pass `sessionContext` (except packet) and are expected - // to match isWindowGlobalPartOfContext result. - if (message.name != "DevToolsWorkerParent:packet") { - const { browserId } = message.data.sessionContext; - // Re-check here, just to ensure that both parent and content processes agree - // on what should or should not be watched. - if ( - this.manager.browsingContext.browserId != browserId && - !lazy.isWindowGlobalPartOfContext( - this.manager, - message.data.sessionContext, - { - acceptInitialDocument: true, - } - ) - ) { - throw new Error( - "Mismatch between DevToolsWorkerParent and DevToolsWorkerChild " + - (this.manager.browsingContext.browserId == browserId - ? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)" - : `expected browsing context with ID ${browserId}, but got ${this.manager.browsingContext.browserId}`) - ); - } - } - - switch (message.name) { - case "DevToolsWorkerParent:instantiate-already-available": { - const { watcherActorID, connectionPrefix, sessionData } = message.data; - - return this._watchWorkerTargets({ - watcherActorID, - parentConnectionPrefix: connectionPrefix, - sessionData, - }); - } - case "DevToolsWorkerParent:destroy": { - const { watcherActorID } = message.data; - return this._destroyTargetActors(watcherActorID); - } - case "DevToolsWorkerParent:addOrSetSessionDataEntry": { - const { watcherActorID, type, entries, updateType } = message.data; - return this._addOrSetSessionDataEntry( - watcherActorID, - type, - entries, - updateType - ); - } - case "DevToolsWorkerParent:removeSessionDataEntry": { - const { watcherActorID, type, entries } = message.data; - return this._removeSessionDataEntry(watcherActorID, type, entries); - } - case "DevToolsWorkerParent:packet": - return this.emit("packet-received", message); - default: - throw new Error( - "Unsupported message in DevToolsWorkerParent: " + message.name - ); - } - } - - /** - * Instantiate targets for existing workers, watch for worker registration and listen - * for resources on those workers, for given connection and context. Targets are sent - * to the DevToolsWorkerParent via the DevToolsWorkerChild:workerTargetAvailable message. - * - * @param {Object} options - * @param {String} options.watcherActorID: The ID of the WatcherActor who requested to - * observe and create these target actors. - * @param {String} options.parentConnectionPrefix: The prefix of the DevToolsServerConnection - * of the Watcher Actor. This is used to compute a unique ID for the target actor. - * @param {Object} options.sessionData: Data (targets, resources, …) the watcher wants - * to be notified about. See WatcherRegistry.getSessionData to see the full list - * of properties. - */ - async _watchWorkerTargets({ - watcherActorID, - parentConnectionPrefix, - sessionData, - }) { - if (this._connections.has(watcherActorID)) { - throw new Error( - "DevToolsWorkerChild _watchWorkerTargets was called more than once" + - ` for the same Watcher (Actor ID: "${watcherActorID}")` - ); - } - - // Listen for new workers that will be spawned. - if (!this._workerDebuggerListener) { - this._workerDebuggerListener = { - onRegister: this._onWorkerRegistered.bind(this), - onUnregister: this._onWorkerUnregistered.bind(this), - }; - lazy.wdm.addListener(this._workerDebuggerListener); - } - - // Compute a unique prefix, just for this WindowGlobal, - // which will be used to create a JSWindowActorTransport pair between content and parent processes. - // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, - // but here, we can't have access to any DevTools connection as we are really early in the content process startup - // WindowGlobalChild's innerWindowId should be unique across processes, so it should be safe? - // (this.manager == WindowGlobalChild interface) - const forwardingPrefix = - parentConnectionPrefix + "workerGlobal" + this.manager.innerWindowId; - - const connection = this._createConnection(forwardingPrefix); - - this._connections.set(watcherActorID, { - connection, - workers: [], - forwardingPrefix, - sessionData, - }); - - const promises = []; - for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) { - if (!this._shouldHandleWorker(dbg)) { - continue; - } - promises.push( - this._createWorkerTargetActor({ - dbg, - connection, - forwardingPrefix, - watcherActorID, - }) - ); - } - await Promise.all(promises); - } - - _createConnection(forwardingPrefix) { - const { DevToolsServer } = lazy.Loader.require( - "resource://devtools/server/devtools-server.js" - ); - - DevToolsServer.init(); - - // We want a special server without any root actor and only target-scoped actors. - // We are going to spawn a WorkerTargetActor instance in the next few lines, - // it is going to act like a root actor without being one. - DevToolsServer.registerActors({ target: true }); - - const connection = DevToolsServer.connectToParentWindowActor( - this, - forwardingPrefix - ); - - return connection; - } - - /** - * Indicates whether or not we should handle the worker debugger - * - * @param {WorkerDebugger} dbg: The worker debugger we want to check. - * @returns {Boolean} - */ - _shouldHandleWorker(dbg) { - // We only want to create targets for non-closed dedicated worker, in the same document - return ( - lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg) && - dbg.type === Ci.nsIWorkerDebugger.TYPE_DEDICATED && - dbg.windowIDs.includes(this.manager.innerWindowId) - ); - } - - async _createWorkerTargetActor({ - dbg, - connection, - forwardingPrefix, - watcherActorID, - }) { - // Prevent the debuggee from executing in this worker until the client has - // finished attaching to it. This call will throw if the debugger is already "registered" - // (i.e. if this is called outside of the register listener) - // See https://searchfox.org/mozilla-central/rev/84922363f4014eae684aabc4f1d06380066494c5/dom/workers/nsIWorkerDebugger.idl#55-66 - try { - dbg.setDebuggerReady(false); - } catch (e) {} - - const watcherConnectionData = this._connections.get(watcherActorID); - const { sessionData } = watcherConnectionData; - const workerThreadServerForwardingPrefix = - connection.allocID("workerTarget"); - - // Create the actual worker target actor, in the worker thread. - const { connectToWorker } = lazy.Loader.require( - "resource://devtools/server/connectors/worker-connector.js" - ); - - const onConnectToWorker = connectToWorker( - connection, - dbg, - workerThreadServerForwardingPrefix, - { - sessionData, - sessionContext: sessionData.sessionContext, - } - ); - - try { - await onConnectToWorker; - } catch (e) { - // onConnectToWorker can reject if the Worker Debugger is closed; so we only want to - // resume the debugger if it is not closed (otherwise it can cause crashes). - if (!dbg.isClosed) { - dbg.setDebuggerReady(true); - } - return; - } - - const { workerTargetForm, transport } = await onConnectToWorker; - - try { - this.sendAsyncMessage("DevToolsWorkerChild:workerTargetAvailable", { - watcherActorID, - forwardingPrefix, - workerTargetForm, - }); - } catch (e) { - // If there was an error while sending the message, we are not going to use this - // connection to communicate with the worker. - transport.close(); - return; - } - - // Only add data to the connection if we successfully send the - // workerTargetAvailable message. - watcherConnectionData.workers.push({ - dbg, - transport, - workerTargetForm, - workerThreadServerForwardingPrefix, - }); - } - - _destroyTargetActors(watcherActorID) { - const watcherConnectionData = this._connections.get(watcherActorID); - this._connections.delete(watcherActorID); - - // This connection has already been cleaned? - if (!watcherConnectionData) { - console.error( - `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}` - ); - return; - } - - for (const { - dbg, - transport, - workerThreadServerForwardingPrefix, - } of watcherConnectionData.workers) { - try { - if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) { - dbg.postMessage( - JSON.stringify({ - type: "disconnect", - forwardingPrefix: workerThreadServerForwardingPrefix, - }) - ); - } - } catch (e) {} - - transport.close(); - } - - watcherConnectionData.connection.close(); - } - - async sendPacket(packet, prefix) { - return this.sendAsyncMessage("DevToolsWorkerChild:packet", { - packet, - prefix, - }); - } - - async _addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) { - const watcherConnectionData = this._connections.get(watcherActorID); - if (!watcherConnectionData) { - return; - } - - lazy.SessionDataHelpers.addOrSetSessionDataEntry( - watcherConnectionData.sessionData, - type, - entries, - updateType - ); - - const promises = []; - for (const { - dbg, - workerThreadServerForwardingPrefix, - } of watcherConnectionData.workers) { - promises.push( - addOrSetSessionDataEntryInWorkerTarget({ - dbg, - workerThreadServerForwardingPrefix, - type, - entries, - updateType, - }) - ); - } - await Promise.all(promises); - } - - _removeSessionDataEntry(watcherActorID, type, entries) { - const watcherConnectionData = this._connections.get(watcherActorID); - - if (!watcherConnectionData) { - return; - } - - lazy.SessionDataHelpers.removeSessionDataEntry( - watcherConnectionData.sessionData, - type, - entries - ); - - for (const { - dbg, - workerThreadServerForwardingPrefix, - } of watcherConnectionData.workers) { - if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) { - dbg.postMessage( - JSON.stringify({ - type: "remove-session-data-entry", - forwardingPrefix: workerThreadServerForwardingPrefix, - dataEntryType: type, - entries, - }) - ); - } - } - } - - handleEvent({ type }) { - // DOMWindowCreated is registered from the WatcherRegistry via `ActorManagerParent.addJSWindowActors` - // as a DOM event to be listened to and so is fired by JSWindowActor platform code. - if (type == "DOMWindowCreated") { - this.onDOMWindowCreated(); - } - } - - _removeExistingWorkerDebuggerListener() { - if (this._workerDebuggerListener) { - lazy.wdm.removeListener(this._workerDebuggerListener); - this._workerDebuggerListener = null; - } - } - - /** - * Part of JSActor API - * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52 - * - * > The didDestroy method, if present, will be called after the actor is no - * > longer able to receive any more messages. - */ - didDestroy() { - this._removeExistingWorkerDebuggerListener(); - - for (const [watcherActorID, watcherConnectionData] of this._connections) { - const { connection } = watcherConnectionData; - this._destroyTargetActors(watcherActorID); - - connection.close(); - } - - this._connections.clear(); - } -} - -/** - * Communicate the type and entries to the Worker Target actor, via the WorkerDebugger. - * - * @param {WorkerDebugger} dbg - * @param {String} workerThreadServerForwardingPrefix - * @param {String} type - * Session data type name - * @param {Array} entries - * Session data entries to add or set. - * @param {String} updateType - * Either "add" or "set", to control if we should only add some items, - * or replace the whole data set with the new entries. - * @returns {Promise} Returns a Promise that resolves once the data entry were handled - * by the worker target. - */ -function addOrSetSessionDataEntryInWorkerTarget({ - dbg, - workerThreadServerForwardingPrefix, - type, - entries, - updateType, -}) { - if (!lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) { - return Promise.resolve(); - } - - return new Promise(resolve => { - // Wait until we're notified by the worker that the resources are watched. - // This is important so we know existing resources were handled. - const listener = { - onMessage: message => { - message = JSON.parse(message); - if (message.type === "session-data-entry-added-or-set") { - resolve(); - dbg.removeListener(listener); - } - }, - // Resolve if the worker is being destroyed so we don't have a dangling promise. - onClose: () => resolve(), - }; - - dbg.addListener(listener); - - dbg.postMessage( - JSON.stringify({ - type: "add-or-set-session-data-entry", - forwardingPrefix: workerThreadServerForwardingPrefix, - dataEntryType: type, - entries, - updateType, - }) - ); - }); -} diff --git a/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs deleted file mode 100644 index cb9bffc2ca..0000000000 --- a/devtools/server/connectors/js-window-actor/DevToolsWorkerParent.sys.mjs +++ /dev/null @@ -1,294 +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/. */ - -import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs"; -import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; - -const { WatcherRegistry } = ChromeUtils.importESModule( - "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs", - // WatcherRegistry needs to be a true singleton and loads ActorManagerParent - // which also has to be a true singleton. - { global: "shared" } -); - -const lazy = {}; - -loader.lazyRequireGetter( - lazy, - "JsWindowActorTransport", - "resource://devtools/shared/transport/js-window-actor-transport.js", - true -); - -export class DevToolsWorkerParent extends JSWindowActorParent { - constructor() { - super(); - - this._destroyed = false; - - // Map of DevToolsServerConnection's used to forward the messages from/to - // the client. The connections run in the parent process, as this code. We - // may have more than one when there is more than one client debugging the - // same worker. For example, a content toolbox and the browser toolbox. - // - // The map is indexed by the connection prefix, and the values are object with the - // following properties: - // - watcher: The WatcherActor - // - actors: A Map of the worker target actors form, indexed by WorkerTarget actorID - // - transport: the JsWindowActorTransport - // - // Reminder about prefixes: all DevToolsServerConnections have a `prefix` - // which can be considered as a kind of id. On top of this, parent process - // DevToolsServerConnections also have forwarding prefixes because they are - // responsible for forwarding messages to content process connections. - this._connections = new Map(); - - this._onConnectionClosed = this._onConnectionClosed.bind(this); - EventEmitter.decorate(this); - } - - /** - * Request the content process to create Worker Targets if workers matching the context - * are already available. - */ - async instantiateWorkerTargets({ - watcherActorID, - connectionPrefix, - sessionContext, - sessionData, - }) { - try { - await this.sendQuery( - "DevToolsWorkerParent:instantiate-already-available", - { - watcherActorID, - connectionPrefix, - sessionContext, - sessionData, - } - ); - } catch (e) { - console.warn( - "Failed to create DevTools Worker target for browsingContext", - this.browsingContext.id, - "and watcher actor id", - watcherActorID - ); - console.warn(e); - } - } - - destroyWorkerTargets({ watcherActorID, sessionContext }) { - return this.sendAsyncMessage("DevToolsWorkerParent:destroy", { - watcherActorID, - sessionContext, - }); - } - - /** - * Communicate to the content process that some data have been added. - */ - async addOrSetSessionDataEntry({ - watcherActorID, - sessionContext, - type, - entries, - updateType, - }) { - try { - await this.sendQuery("DevToolsWorkerParent:addOrSetSessionDataEntry", { - watcherActorID, - sessionContext, - type, - entries, - updateType, - }); - } catch (e) { - console.warn( - "Failed to add session data entry for worker targets in browsing context", - this.browsingContext.id, - "and watcher actor id", - watcherActorID - ); - console.warn(e); - } - } - - /** - * Communicate to the content process that some data have been removed. - */ - removeSessionDataEntry({ watcherActorID, sessionContext, type, entries }) { - this.sendAsyncMessage("DevToolsWorkerParent:removeSessionDataEntry", { - watcherActorID, - sessionContext, - type, - entries, - }); - } - - workerTargetAvailable({ - watcherActorID, - forwardingPrefix, - workerTargetForm, - }) { - if (this._destroyed) { - return; - } - - const watcher = WatcherRegistry.getWatcher(watcherActorID); - - if (!watcher) { - throw new Error( - `Watcher Actor with ID '${watcherActorID}' can't be found.` - ); - } - - const connection = watcher.conn; - const { prefix } = connection; - if (!this._connections.has(prefix)) { - connection.on("closed", this._onConnectionClosed); - - // Create a js-window-actor based transport. - const transport = new lazy.JsWindowActorTransport(this, forwardingPrefix); - transport.hooks = { - onPacket: connection.send.bind(connection), - onTransportClosed() {}, - }; - transport.ready(); - - connection.setForwarding(forwardingPrefix, transport); - - this._connections.set(prefix, { - watcher, - transport, - actors: new Map(), - }); - } - - const workerTargetActorId = workerTargetForm.actor; - this._connections - .get(prefix) - .actors.set(workerTargetActorId, workerTargetForm); - watcher.notifyTargetAvailable(workerTargetForm); - } - - workerTargetDestroyed({ watcherActorID, workerTargetForm }) { - const watcher = WatcherRegistry.getWatcher(watcherActorID); - - if (!watcher) { - throw new Error( - `Watcher Actor with ID '${watcherActorID}' can't be found.` - ); - } - - const connection = watcher.conn; - const { prefix } = connection; - if (!this._connections.has(prefix)) { - return; - } - - const workerTargetActorId = workerTargetForm.actor; - const { actors } = this._connections.get(prefix); - if (!actors.has(workerTargetActorId)) { - return; - } - - actors.delete(workerTargetActorId); - watcher.notifyTargetDestroyed(workerTargetForm); - } - - _onConnectionClosed(status, prefix) { - this._unregisterWatcher(prefix); - } - - async _unregisterWatcher(connectionPrefix) { - const connectionInfo = this._connections.get(connectionPrefix); - if (!connectionInfo) { - return; - } - - const { watcher, transport } = connectionInfo; - const connection = watcher.conn; - - connection.off("closed", this._onConnectionClosed); - if (transport) { - // If we have a child transport, the actor has already - // been created. We need to stop using this transport. - connection.cancelForwarding(transport._prefix); - transport.close(); - } - - this._connections.delete(connectionPrefix); - - if (!this._connections.size) { - this._destroy(); - } - } - - _destroy() { - if (this._destroyed) { - return; - } - this._destroyed = true; - - for (const { actors, watcher } of this._connections.values()) { - for (const actor of actors.values()) { - watcher.notifyTargetDestroyed(actor); - } - - this._unregisterWatcher(watcher.conn.prefix); - } - } - - /** - * Part of JSActor API - * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52 - * - * > The didDestroy method, if present, will be called after the (JSWindow)actor is no - * > longer able to receive any more messages. - */ - didDestroy() { - this._destroy(); - } - - /** - * Supported Queries - */ - - async sendPacket(packet, prefix) { - return this.sendAsyncMessage("DevToolsWorkerParent:packet", { - packet, - prefix, - }); - } - - /** - * JsWindowActor API - */ - - async sendQuery(msg, args) { - try { - const res = await super.sendQuery(msg, args); - return res; - } catch (e) { - console.error("Failed to sendQuery in DevToolsWorkerParent", msg, e); - throw e; - } - } - - receiveMessage(message) { - switch (message.name) { - case "DevToolsWorkerChild:workerTargetAvailable": - return this.workerTargetAvailable(message.data); - case "DevToolsWorkerChild:workerTargetDestroyed": - return this.workerTargetDestroyed(message.data); - case "DevToolsWorkerChild:packet": - return this.emit("packet-received", message); - default: - throw new Error( - "Unsupported message in DevToolsWorkerParent: " + message.name - ); - } - } -} diff --git a/devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs b/devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs deleted file mode 100644 index ae15c030fe..0000000000 --- a/devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs +++ /dev/null @@ -1,76 +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/. */ - -function getWindowGlobalUri(windowGlobal) { - let windowGlobalUri = ""; - - if (windowGlobal.documentURI) { - // If windowGlobal is a WindowGlobalParent documentURI should be available. - windowGlobalUri = windowGlobal.documentURI.spec; - } else if (windowGlobal.browsingContext?.window) { - // If windowGlobal is a WindowGlobalChild, this code runs in the same - // process as the document and we can directly access the window.location - // object. - windowGlobalUri = windowGlobal.browsingContext.window.location.href; - if (!windowGlobalUri) { - windowGlobalUri = - windowGlobal.browsingContext.window.document.documentURI; - } - } - - return windowGlobalUri; -} - -export const WindowGlobalLogger = { - /** - * This logger can run from the content or parent process, and windowGlobal - * will either be of type `WindowGlobalParent` or `WindowGlobalChild`. - * - * The interface for each type can be found in WindowGlobalActors.webidl - * (https://searchfox.org/mozilla-central/source/dom/chrome-webidl/WindowGlobalActors.webidl) - * - * @param {WindowGlobalParent|WindowGlobalChild} windowGlobal - * The window global to log. See WindowGlobalActors.webidl for details - * about the types. - * @param {String} message - * A custom message that will be displayed at the beginning of the log. - */ - logWindowGlobal(windowGlobal, message) { - const { browsingContext } = windowGlobal; - const { parent } = browsingContext; - const windowGlobalUri = getWindowGlobalUri(windowGlobal); - const isInitialDocument = - "isInitialDocument" in windowGlobal - ? windowGlobal.isInitialDocument - : windowGlobal.browsingContext.window?.document.isInitialDocument; - - const details = []; - details.push( - "BrowsingContext.browserId: " + browsingContext.browserId, - "BrowsingContext.id: " + browsingContext.id, - "innerWindowId: " + windowGlobal.innerWindowId, - "opener.id: " + browsingContext.opener?.id, - "pid: " + windowGlobal.osPid, - "isClosed: " + windowGlobal.isClosed, - "isInProcess: " + windowGlobal.isInProcess, - "isCurrentGlobal: " + windowGlobal.isCurrentGlobal, - "isProcessRoot: " + windowGlobal.isProcessRoot, - "currentRemoteType: " + browsingContext.currentRemoteType, - "hasParent: " + (parent ? parent.id : "no"), - "uri: " + (windowGlobalUri ? windowGlobalUri : "no uri"), - "isProcessRoot: " + windowGlobal.isProcessRoot, - "BrowsingContext.isContent: " + windowGlobal.browsingContext.isContent, - "isInitialDocument: " + isInitialDocument - ); - - const header = "[WindowGlobalLogger] " + message; - - // Use a padding for multiline display. - const padding = " "; - const formattedDetails = details.map(s => padding + s); - const detailsString = formattedDetails.join("\n"); - - dump(header + "\n" + detailsString + "\n"); - }, -}; diff --git a/devtools/server/connectors/js-window-actor/moz.build b/devtools/server/connectors/js-window-actor/moz.build deleted file mode 100644 index faaaa8dd54..0000000000 --- a/devtools/server/connectors/js-window-actor/moz.build +++ /dev/null @@ -1,13 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# 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/. - -DevToolsModules( - "DevToolsFrameChild.sys.mjs", - "DevToolsFrameParent.sys.mjs", - "DevToolsWorkerChild.sys.mjs", - "DevToolsWorkerParent.sys.mjs", - "WindowGlobalLogger.sys.mjs", -) diff --git a/devtools/server/connectors/moz.build b/devtools/server/connectors/moz.build index fd4baf81ff..ba0f17f63c 100644 --- a/devtools/server/connectors/moz.build +++ b/devtools/server/connectors/moz.build @@ -6,8 +6,6 @@ DIRS += [ "js-process-actor", - "js-window-actor", - "process-actor", ] DevToolsModules( diff --git a/devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs b/devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs deleted file mode 100644 index 2e461cbd03..0000000000 --- a/devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs +++ /dev/null @@ -1,741 +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/. */ - -import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; -import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; - -const lazy = {}; -ChromeUtils.defineESModuleGetters(lazy, { - loader: "resource://devtools/shared/loader/Loader.sys.mjs", -}); - -XPCOMUtils.defineLazyServiceGetter( - lazy, - "wdm", - "@mozilla.org/dom/workers/workerdebuggermanager;1", - "nsIWorkerDebuggerManager" -); - -XPCOMUtils.defineLazyModuleGetters(lazy, { - SessionDataHelpers: - "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm", -}); - -ChromeUtils.defineLazyGetter(lazy, "DevToolsUtils", () => - lazy.loader.require("devtools/shared/DevToolsUtils") -); - -// Name of the attribute into which we save data in `sharedData` object. -const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; - -export class DevToolsServiceWorkerChild extends JSProcessActorChild { - constructor() { - super(); - - // The map is indexed by the Watcher Actor ID. - // The values are objects containing the following properties: - // - connection: the DevToolsServerConnection itself - // - workers: An array of object containing the following properties: - // - dbg: A WorkerDebuggerInstance - // - serviceWorkerTargetForm: The associated worker target instance form - // - workerThreadServerForwardingPrefix: The prefix used to forward events to the - // worker target on the worker thread (). - // - forwardingPrefix: Prefix used by the JSWindowActorTransport pair to communicate - // between content and parent processes. - // - sessionData: Data (targets, resources, …) the watcher wants to be notified about. - // See WatcherRegistry.getSessionData to see the full list of properties. - this._connections = new Map(); - - this._onConnectionChange = this._onConnectionChange.bind(this); - - EventEmitter.decorate(this); - } - - /** - * Called by nsIWorkerDebuggerManager when a worker get created. - * - * Go through all registered connections (in case we have more than one client connected) - * to eventually instantiate a target actor for this worker. - * - * @param {nsIWorkerDebugger} dbg - */ - _onWorkerRegistered(dbg) { - // Only consider service workers - if (dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) { - return; - } - - for (const [ - watcherActorID, - { connection, forwardingPrefix, sessionData }, - ] of this._connections) { - if (this._shouldHandleWorker(sessionData, dbg)) { - this._createWorkerTargetActor({ - dbg, - connection, - forwardingPrefix, - watcherActorID, - }); - } - } - } - - /** - * Called by nsIWorkerDebuggerManager when a worker get destroyed. - * - * Go through all registered connections (in case we have more than one client connected) - * to destroy the related target which may have been created for this worker. - * - * @param {nsIWorkerDebugger} dbg - */ - _onWorkerUnregistered(dbg) { - // Only consider service workers - if (dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) { - return; - } - - for (const [watcherActorID, watcherConnectionData] of this._connections) { - this._destroyServiceWorkerTargetForWatcher( - watcherActorID, - watcherConnectionData, - dbg - ); - } - } - - /** - * To be called when we know a Service Worker target should be destroyed for a specific connection - * for which we pass the related "watcher connection data". - * - * @param {String} watcherActorID - * Watcher actor ID for which we should unregister this service worker. - * @param {Object} watcherConnectionData - * The metadata object for a given watcher, stored in the _connections Map. - * @param {nsIWorkerDebugger} dbg - */ - _destroyServiceWorkerTargetForWatcher( - watcherActorID, - watcherConnectionData, - dbg - ) { - const { workers, forwardingPrefix } = watcherConnectionData; - - // Check if the worker registration was handled for this watcher. - const unregisteredActorIndex = workers.findIndex(worker => { - try { - // Accessing the WorkerDebugger id might throw (NS_ERROR_UNEXPECTED). - return worker.dbg.id === dbg.id; - } catch (e) { - return false; - } - }); - - // Ignore this worker if it wasn't registered for this watcher. - if (unregisteredActorIndex === -1) { - return; - } - - const { serviceWorkerTargetForm, transport } = - workers[unregisteredActorIndex]; - - // Remove the entry from this._connection dictionnary - workers.splice(unregisteredActorIndex, 1); - - // Close the transport made against the worker thread. - transport.close(); - - // Note that we do not need to post the "disconnect" message from this destruction codepath - // as this method is only called when the worker is unregistered and so, - // we can't send any message anyway, and the worker is being destroyed anyway. - - // Also notify the parent process that this worker target got destroyed. - // As the worker thread may be already destroyed, it may not have time to send a destroy event. - try { - this.sendAsyncMessage( - "DevToolsServiceWorkerChild:serviceWorkerTargetDestroyed", - { - watcherActorID, - forwardingPrefix, - serviceWorkerTargetForm, - } - ); - } catch (e) { - // Ignore exception which may happen on content process destruction - } - } - - /** - * Function handling messages sent by DevToolsServiceWorkerParent (part of ProcessActor API). - * - * @param {Object} message - * @param {String} message.name - * @param {*} message.data - */ - receiveMessage(message) { - switch (message.name) { - case "DevToolsServiceWorkerParent:instantiate-already-available": { - const { watcherActorID, connectionPrefix, sessionData } = message.data; - return this._watchWorkerTargets({ - watcherActorID, - parentConnectionPrefix: connectionPrefix, - sessionData, - }); - } - case "DevToolsServiceWorkerParent:destroy": { - const { watcherActorID } = message.data; - return this._destroyTargetActors(watcherActorID); - } - case "DevToolsServiceWorkerParent:addOrSetSessionDataEntry": { - const { watcherActorID, type, entries, updateType } = message.data; - return this._addOrSetSessionDataEntry( - watcherActorID, - type, - entries, - updateType - ); - } - case "DevToolsServiceWorkerParent:removeSessionDataEntry": { - const { watcherActorID, type, entries } = message.data; - return this._removeSessionDataEntry(watcherActorID, type, entries); - } - case "DevToolsServiceWorkerParent:packet": - return this.emit("packet-received", message); - default: - throw new Error( - "Unsupported message in DevToolsServiceWorkerParent: " + message.name - ); - } - } - - /** - * "chrome-event-target-created" event handler. Supposed to be fired very early when the process starts - */ - observe() { - const { sharedData } = Services.cpmm; - const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME); - if (!sessionDataByWatcherActor) { - throw new Error( - "Request to instantiate the target(s) for the Service Worker, but `sharedData` is empty about watched targets" - ); - } - - // Create one Target actor for each prefix/client which listen to workers - for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) { - const { targets, connectionPrefix } = sessionData; - if (targets?.includes("service_worker")) { - this._watchWorkerTargets({ - watcherActorID, - parentConnectionPrefix: connectionPrefix, - sessionData, - }); - } - } - } - - /** - * Instantiate targets for existing workers, watch for worker registration and listen - * for resources on those workers, for given connection and context. Targets are sent - * to the DevToolsServiceWorkerParent via the DevToolsServiceWorkerChild:serviceWorkerTargetAvailable message. - * - * @param {Object} options - * @param {String} options.watcherActorID: The ID of the WatcherActor who requested to - * observe and create these target actors. - * @param {String} options.parentConnectionPrefix: The prefix of the DevToolsServerConnection - * of the Watcher Actor. This is used to compute a unique ID for the target actor. - * @param {Object} options.sessionData: Data (targets, resources, …) the watcher wants - * to be notified about. See WatcherRegistry.getSessionData to see the full list - * of properties. - */ - async _watchWorkerTargets({ - watcherActorID, - parentConnectionPrefix, - sessionData, - }) { - // We might already have been called from observe method if the process was initializing - if (this._connections.has(watcherActorID)) { - // In such case, wait for the promise in order to ensure resolving only after - // we notified about the existing targets - await this._connections.get(watcherActorID).watchPromise; - return; - } - - // Compute a unique prefix, just for this Service Worker, - // which will be used to create a JSWindowActorTransport pair between content and parent processes. - // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, - // but here, we can't have access to any DevTools connection as we are really early in the content process startup - // WindowGlobalChild's innerWindowId should be unique across processes, so it should be safe? - // (this.manager == WindowGlobalChild interface) - const forwardingPrefix = - parentConnectionPrefix + "serviceWorkerProcess" + this.manager.childID; - - const connection = this._createConnection(forwardingPrefix); - - // This method will be concurrently called from `observe()` and `DevToolsServiceWorkerParent:instantiate-already-available` - // When the JSprocessActor initializes itself and when the watcher want to force instantiating existing targets. - // Wait for the existing promise when the second call arise. - // - // Also, _connections has to be populated *before* calling _createWorkerTargetActor, - // so create a deferred promise right away. - let resolveWatchPromise; - const watchPromise = new Promise( - resolve => (resolveWatchPromise = resolve) - ); - - this._connections.set(watcherActorID, { - connection, - watchPromise, - workers: [], - forwardingPrefix, - sessionData, - }); - - // Listen for new workers that will be spawned. - if (!this._workerDebuggerListener) { - this._workerDebuggerListener = { - onRegister: this._onWorkerRegistered.bind(this), - onUnregister: this._onWorkerUnregistered.bind(this), - }; - lazy.wdm.addListener(this._workerDebuggerListener); - } - - const promises = []; - for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) { - if (!this._shouldHandleWorker(sessionData, dbg)) { - continue; - } - promises.push( - this._createWorkerTargetActor({ - dbg, - connection, - forwardingPrefix, - watcherActorID, - }) - ); - } - await Promise.all(promises); - resolveWatchPromise(); - } - - /** - * Initialize a DevTools Server and return a new DevToolsServerConnection - * using this server in order to communicate to the parent process via - * the JSProcessActor message / queries. - * - * @param String forwardingPrefix - * A unique prefix used to distinguish message coming from distinct service workers. - * @return DevToolsServerConnection - * A connection to communicate with the parent process. - */ - _createConnection(forwardingPrefix) { - const { DevToolsServer } = lazy.loader.require( - "devtools/server/devtools-server" - ); - - DevToolsServer.init(); - - // We want a special server without any root actor and only target-scoped actors. - // We are going to spawn a WorkerTargetActor instance in the next few lines, - // it is going to act like a root actor without being one. - DevToolsServer.registerActors({ target: true }); - DevToolsServer.on("connectionchange", this._onConnectionChange); - - const connection = DevToolsServer.connectToParentWindowActor( - this, - forwardingPrefix - ); - - return connection; - } - - /** - * Indicates whether or not we should handle the worker debugger for a given - * watcher's session data. - * - * @param {Object} sessionData - * The session data for a given watcher, which includes metadata - * about the debugged context. - * @param {WorkerDebugger} dbg - * The worker debugger we want to check. - * - * @returns {Boolean} - */ - _shouldHandleWorker(sessionData, dbg) { - if (dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) { - return false; - } - // We only want to create targets for non-closed service worker - if (!lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) { - return false; - } - - // Accessing `nsIPrincipal.host` may easily throw on non-http URLs. - // Ignore all non-HTTP as they most likely don't have any valid host name. - if (!dbg.principal.scheme.startsWith("http")) { - return false; - } - - const workerHost = dbg.principal.hostPort; - return workerHost == sessionData["browser-element-host"][0]; - } - - async _createWorkerTargetActor({ - dbg, - connection, - forwardingPrefix, - watcherActorID, - }) { - // Freeze the worker execution as soon as possible in order to wait for DevTools bootstrap. - // We typically want to: - // - startup the Thread Actor, - // - pass the initial session data which includes breakpoints to the worker thread, - // - register the breakpoints, - // before release its execution. - // `connectToWorker` is going to call setDebuggerReady(true) when all of this is done. - try { - dbg.setDebuggerReady(false); - } catch (e) { - // This call will throw if the debugger is already "registered" - // (i.e. if this is called outside of the register listener) - // See https://searchfox.org/mozilla-central/rev/84922363f4014eae684aabc4f1d06380066494c5/dom/workers/nsIWorkerDebugger.idl#55-66 - } - - const watcherConnectionData = this._connections.get(watcherActorID); - const { sessionData } = watcherConnectionData; - const workerThreadServerForwardingPrefix = connection.allocID( - "serviceWorkerTarget" - ); - - // Create the actual worker target actor, in the worker thread. - const { connectToWorker } = lazy.loader.require( - "devtools/server/connectors/worker-connector" - ); - - const onConnectToWorker = connectToWorker( - connection, - dbg, - workerThreadServerForwardingPrefix, - { - sessionData, - sessionContext: sessionData.sessionContext, - } - ); - - try { - await onConnectToWorker; - } catch (e) { - // connectToWorker is supposed to call setDebuggerReady(true) to release the worker execution. - // But if anything goes wrong and an exception is thrown, ensure releasing its execution, - // otherwise if devtools is broken, it will freeze the worker indefinitely. - // - // onConnectToWorker can reject if the Worker Debugger is closed; so we only want to - // resume the debugger if it is not closed (otherwise it can cause crashes). - if (!dbg.isClosed) { - dbg.setDebuggerReady(true); - } - return; - } - - const { workerTargetForm, transport } = await onConnectToWorker; - - try { - this.sendAsyncMessage( - "DevToolsServiceWorkerChild:serviceWorkerTargetAvailable", - { - watcherActorID, - forwardingPrefix, - serviceWorkerTargetForm: workerTargetForm, - } - ); - } catch (e) { - // If there was an error while sending the message, we are not going to use this - // connection to communicate with the worker. - transport.close(); - return; - } - - // Only add data to the connection if we successfully send the - // serviceWorkerTargetAvailable message. - watcherConnectionData.workers.push({ - dbg, - transport, - serviceWorkerTargetForm: workerTargetForm, - workerThreadServerForwardingPrefix, - }); - } - - /** - * Request the service worker threads to destroy all their service worker Targets currently registered for a given Watcher actor. - * - * @param {String} watcherActorID - */ - _destroyTargetActors(watcherActorID) { - const watcherConnectionData = this._connections.get(watcherActorID); - this._connections.delete(watcherActorID); - - // This connection has already been cleaned? - if (!watcherConnectionData) { - console.error( - `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}` - ); - return; - } - - for (const { - dbg, - transport, - workerThreadServerForwardingPrefix, - } of watcherConnectionData.workers) { - try { - if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) { - dbg.postMessage( - JSON.stringify({ - type: "disconnect", - forwardingPrefix: workerThreadServerForwardingPrefix, - }) - ); - } - } catch (e) {} - - transport.close(); - } - - watcherConnectionData.connection.close(); - } - - /** - * Destroy the server once its last connection closes. Note that multiple - * worker scripts may be running in parallel and reuse the same server. - */ - _onConnectionChange() { - const { DevToolsServer } = lazy.loader.require( - "devtools/server/devtools-server" - ); - - // Only destroy the server if there is no more connections to it. It may be - // used to debug another tab running in the same process. - if (DevToolsServer.hasConnection() || DevToolsServer.keepAlive) { - return; - } - - if (this._destroyed) { - return; - } - this._destroyed = true; - - DevToolsServer.off("connectionchange", this._onConnectionChange); - DevToolsServer.destroy(); - } - - /** - * Used by DevTools transport layer to communicate with the parent process. - * - * @param {String} packet - * @param {String prefix - */ - async sendPacket(packet, prefix) { - return this.sendAsyncMessage("DevToolsServiceWorkerChild:packet", { - packet, - prefix, - }); - } - - /** - * Go through all registered service workers for a given watcher actor - * to send them new session data entries. - * - * See addOrSetSessionDataEntryInWorkerTarget for more info about arguments. - */ - async _addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) { - const watcherConnectionData = this._connections.get(watcherActorID); - if (!watcherConnectionData) { - return; - } - - lazy.SessionDataHelpers.addOrSetSessionDataEntry( - watcherConnectionData.sessionData, - type, - entries, - updateType - ); - - // This type is really specific to Service Workers and doesn't need to be transferred to the worker threads. - // We only need to instantiate and destroy the target actors based on this new host. - if (type == "browser-element-host") { - this.updateBrowserElementHost(watcherActorID, watcherConnectionData); - return; - } - - const promises = []; - for (const { - dbg, - workerThreadServerForwardingPrefix, - } of watcherConnectionData.workers) { - promises.push( - addOrSetSessionDataEntryInWorkerTarget({ - dbg, - workerThreadServerForwardingPrefix, - type, - entries, - updateType, - }) - ); - } - await Promise.all(promises); - } - - /** - * Called whenever the debugged browser element navigates to a new page - * and the URL's host changes. - * This is used to maintain the list of active Service Worker targets - * based on that host name. - * - * @param {String} watcherActorID - * Watcher actor ID for which we should unregister this service worker. - * @param {Object} watcherConnectionData - * The metadata object for a given watcher, stored in the _connections Map. - */ - async updateBrowserElementHost(watcherActorID, watcherConnectionData) { - const { sessionData, connection, forwardingPrefix } = watcherConnectionData; - - // Create target actor matching this new host. - // Note that we may be navigating to the same host name and the target will already exist. - const dbgToInstantiate = []; - for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) { - const alreadyCreated = watcherConnectionData.workers.some( - info => info.dbg === dbg - ); - if (this._shouldHandleWorker(sessionData, dbg) && !alreadyCreated) { - dbgToInstantiate.push(dbg); - } - } - await Promise.all( - dbgToInstantiate.map(dbg => { - return this._createWorkerTargetActor({ - dbg, - connection, - forwardingPrefix, - watcherActorID, - }); - }) - ); - } - - /** - * Go through all registered service workers for a given watcher actor - * to send them request to clear some session data entries. - * - * See addOrSetSessionDataEntryInWorkerTarget for more info about arguments. - */ - _removeSessionDataEntry(watcherActorID, type, entries) { - const watcherConnectionData = this._connections.get(watcherActorID); - - if (!watcherConnectionData) { - return; - } - - lazy.SessionDataHelpers.removeSessionDataEntry( - watcherConnectionData.sessionData, - type, - entries - ); - - for (const { - dbg, - workerThreadServerForwardingPrefix, - } of watcherConnectionData.workers) { - if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) { - dbg.postMessage( - JSON.stringify({ - type: "remove-session-data-entry", - forwardingPrefix: workerThreadServerForwardingPrefix, - dataEntryType: type, - entries, - }) - ); - } - } - } - - _removeExistingWorkerDebuggerListener() { - if (this._workerDebuggerListener) { - lazy.wdm.removeListener(this._workerDebuggerListener); - this._workerDebuggerListener = null; - } - } - - /** - * Part of JSActor API - * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52 - * - * > The didDestroy method, if present, will be called after the actor is no - * > longer able to receive any more messages. - */ - didDestroy() { - this._removeExistingWorkerDebuggerListener(); - - for (const [watcherActorID, watcherConnectionData] of this._connections) { - const { connection } = watcherConnectionData; - this._destroyTargetActors(watcherActorID); - - connection.close(); - } - - this._connections.clear(); - } -} - -/** - * Communicate the type and entries to the Worker Target actor, via the WorkerDebugger. - * - * @param {WorkerDebugger} dbg - * @param {String} workerThreadServerForwardingPrefix - * @param {String} type - * Session data type name - * @param {Array} entries - * Session data entries to add or set. - * @param {String} updateType - * Either "add" or "set", to control if we should only add some items, - * or replace the whole data set with the new entries. - * @returns {Promise} Returns a Promise that resolves once the data entry were handled - * by the worker target. - */ -function addOrSetSessionDataEntryInWorkerTarget({ - dbg, - workerThreadServerForwardingPrefix, - type, - entries, - updateType, -}) { - if (!lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) { - return Promise.resolve(); - } - - return new Promise(resolve => { - // Wait until we're notified by the worker that the resources are watched. - // This is important so we know existing resources were handled. - const listener = { - onMessage: message => { - message = JSON.parse(message); - if (message.type === "session-data-entry-added-or-set") { - resolve(); - dbg.removeListener(listener); - } - }, - // Resolve if the worker is being destroyed so we don't have a dangling promise. - onClose: () => resolve(), - }; - - dbg.addListener(listener); - - dbg.postMessage( - JSON.stringify({ - type: "add-or-set-session-data-entry", - forwardingPrefix: workerThreadServerForwardingPrefix, - dataEntryType: type, - entries, - updateType, - }) - ); - }); -} diff --git a/devtools/server/connectors/process-actor/DevToolsServiceWorkerParent.sys.mjs b/devtools/server/connectors/process-actor/DevToolsServiceWorkerParent.sys.mjs deleted file mode 100644 index 2073f47e76..0000000000 --- a/devtools/server/connectors/process-actor/DevToolsServiceWorkerParent.sys.mjs +++ /dev/null @@ -1,308 +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/. */ - -import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; - -const { WatcherRegistry } = ChromeUtils.importESModule( - "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs", - // WatcherRegistry needs to be a true singleton and loads ActorManagerParent - // which also has to be a true singleton. - { global: "shared" } -); - -const lazy = {}; -ChromeUtils.defineESModuleGetters(lazy, { - loader: "resource://devtools/shared/loader/Loader.sys.mjs", -}); - -ChromeUtils.defineLazyGetter( - lazy, - "JsWindowActorTransport", - () => - lazy.loader.require("devtools/shared/transport/js-window-actor-transport") - .JsWindowActorTransport -); - -export class DevToolsServiceWorkerParent extends JSProcessActorParent { - constructor() { - super(); - - this._destroyed = false; - - // Map of DevToolsServerConnection's used to forward the messages from/to - // the client. The connections run in the parent process, as this code. We - // may have more than one when there is more than one client debugging the - // same worker. For example, a content toolbox and the browser toolbox. - // - // The map is indexed by the connection prefix, and the values are object with the - // following properties: - // - watcher: The WatcherActor - // - actors: A Map of the worker target actors form, indexed by WorkerTarget actorID - // - transport: the JsWindowActorTransport - // - // Reminder about prefixes: all DevToolsServerConnections have a `prefix` - // which can be considered as a kind of id. On top of this, parent process - // DevToolsServerConnections also have forwarding prefixes because they are - // responsible for forwarding messages to content process connections. - this._connections = new Map(); - - this._onConnectionClosed = this._onConnectionClosed.bind(this); - EventEmitter.decorate(this); - } - - /** - * Request the content process to create Service Worker Targets if workers matching the context - * are already available. - */ - async instantiateServiceWorkerTargets({ - watcherActorID, - connectionPrefix, - sessionContext, - sessionData, - }) { - try { - await this.sendQuery( - "DevToolsServiceWorkerParent:instantiate-already-available", - { - watcherActorID, - connectionPrefix, - sessionContext, - sessionData, - } - ); - } catch (e) { - console.warn( - "Failed to create DevTools Service Worker target for process", - this.manager.osPid, - "and watcher actor id", - watcherActorID - ); - console.warn(e); - } - } - - destroyServiceWorkerTargets({ watcherActorID, sessionContext }) { - return this.sendAsyncMessage("DevToolsServiceWorkerParent:destroy", { - watcherActorID, - sessionContext, - }); - } - - /** - * Communicate to the content process that some data have been added. - */ - async addOrSetSessionDataEntry({ - watcherActorID, - sessionContext, - type, - entries, - updateType, - }) { - try { - await this.sendQuery( - "DevToolsServiceWorkerParent:addOrSetSessionDataEntry", - { - watcherActorID, - sessionContext, - type, - entries, - updateType, - } - ); - } catch (e) { - console.warn( - "Failed to add session data entry for worker targets in process", - this.manager.osPid, - "and watcher actor id", - watcherActorID - ); - console.warn(e); - } - } - - /** - * Communicate to the content process that some data have been removed. - */ - removeSessionDataEntry({ watcherActorID, sessionContext, type, entries }) { - this.sendAsyncMessage( - "DevToolsServiceWorkerParent:removeSessionDataEntry", - { - watcherActorID, - sessionContext, - type, - entries, - } - ); - } - - serviceWorkerTargetAvailable({ - watcherActorID, - forwardingPrefix, - serviceWorkerTargetForm, - }) { - if (this._destroyed) { - return; - } - - const watcher = WatcherRegistry.getWatcher(watcherActorID); - - if (!watcher) { - throw new Error( - `Watcher Actor with ID '${watcherActorID}' can't be found.` - ); - } - - const connection = watcher.conn; - const { prefix } = connection; - if (!this._connections.has(prefix)) { - connection.on("closed", this._onConnectionClosed); - - // Create a js-window-actor based transport. - const transport = new lazy.JsWindowActorTransport(this, forwardingPrefix); - transport.hooks = { - onPacket: connection.send.bind(connection), - onTransportClosed() {}, - }; - transport.ready(); - - connection.setForwarding(forwardingPrefix, transport); - - this._connections.set(prefix, { - connection, - watcher, - transport, - actors: new Map(), - }); - } - - const serviceWorkerTargetActorId = serviceWorkerTargetForm.actor; - this._connections - .get(prefix) - .actors.set(serviceWorkerTargetActorId, serviceWorkerTargetForm); - watcher.notifyTargetAvailable(serviceWorkerTargetForm); - } - - serviceWorkerTargetDestroyed({ watcherActorID, serviceWorkerTargetForm }) { - const watcher = WatcherRegistry.getWatcher(watcherActorID); - - if (!watcher) { - throw new Error( - `Watcher Actor with ID '${watcherActorID}' can't be found.` - ); - } - - const connection = watcher.conn; - const { prefix } = connection; - if (!this._connections.has(prefix)) { - return; - } - - const serviceWorkerTargetActorId = serviceWorkerTargetForm.actor; - const { actors } = this._connections.get(prefix); - if (!actors.has(serviceWorkerTargetActorId)) { - return; - } - - actors.delete(serviceWorkerTargetActorId); - watcher.notifyTargetDestroyed(serviceWorkerTargetForm); - } - - _onConnectionClosed(status, prefix) { - if (this._connections.has(prefix)) { - const { connection } = this._connections.get(prefix); - this._cleanupConnection(connection); - } - } - - async _cleanupConnection(connection) { - if (!this._connections || !this._connections.has(connection.prefix)) { - return; - } - - const { transport } = this._connections.get(connection.prefix); - - connection.off("closed", this._onConnectionClosed); - if (transport) { - // If we have a child transport, the actor has already - // been created. We need to stop using this transport. - connection.cancelForwarding(transport._prefix); - transport.close(); - } - - this._connections.delete(connection.prefix); - if (!this._connections.size) { - this._destroy(); - } - } - - _destroy() { - if (this._destroyed) { - return; - } - this._destroyed = true; - - for (const { actors, watcher } of this._connections.values()) { - for (const actor of actors.values()) { - watcher.notifyTargetDestroyed(actor); - } - - this._cleanupConnection(watcher.conn); - } - } - - /** - * Part of JSActor API - * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52 - * - * > The didDestroy method, if present, will be called after the ProcessActor is no - * > longer able to receive any more messages. - */ - didDestroy() { - this._destroy(); - } - - /** - * Supported Queries - */ - - async sendPacket(packet, prefix) { - return this.sendAsyncMessage("DevToolsServiceWorkerParent:packet", { - packet, - prefix, - }); - } - - /** - * JsWindowActor API - */ - - async sendQuery(msg, args) { - try { - const res = await super.sendQuery(msg, args); - return res; - } catch (e) { - console.error( - "Failed to sendQuery in DevToolsServiceWorkerParent", - msg, - e - ); - throw e; - } - } - - receiveMessage(message) { - switch (message.name) { - case "DevToolsServiceWorkerChild:serviceWorkerTargetAvailable": - return this.serviceWorkerTargetAvailable(message.data); - case "DevToolsServiceWorkerChild:serviceWorkerTargetDestroyed": - return this.serviceWorkerTargetDestroyed(message.data); - case "DevToolsServiceWorkerChild:packet": - return this.emit("packet-received", message); - default: - throw new Error( - "Unsupported message in DevToolsServiceWorkerParent: " + message.name - ); - } - } -} diff --git a/devtools/server/connectors/worker-connector.js b/devtools/server/connectors/worker-connector.js index 90d55d7a69..dc72993d7a 100644 --- a/devtools/server/connectors/worker-connector.js +++ b/devtools/server/connectors/worker-connector.js @@ -46,9 +46,9 @@ function connectToWorker(connection, dbg, forwardingPrefix, options) { onMessage: message => { message = JSON.parse(message); if (message.type !== "rpc") { - if (message.type == "worker-thread-attached") { - // The thread actor has finished attaching and can hit installed - // breakpoints. Allow content to begin executing in the worker. + if (message.type == "session-data-processed") { + // The thread actor has finished processing session data, including breakpoints. + // Allow content to begin executing in the worker and possibly hit early breakpoints. dbg.setDebuggerReady(true); } return; diff --git a/devtools/server/startup/content-process-script.js b/devtools/server/startup/content-process-script.js index 3449eb465a..fa91ab0c28 100644 --- a/devtools/server/startup/content-process-script.js +++ b/devtools/server/startup/content-process-script.js @@ -143,7 +143,7 @@ class ContentProcessStartup { /** * Called when the content process just started. - * This will start creating ContentProcessTarget actors, but only if DevTools code (WatcherActor / WatcherRegistry.sys.mjs) + * This will start creating ContentProcessTarget actors, but only if DevTools code (WatcherActor / ParentProcessWatcherRegistry.sys.mjs) * put some data in `sharedData` telling us to do so. */ maybeCreateExistingTargetActors() { @@ -187,7 +187,7 @@ class ContentProcessStartup { * The prefix of the DevToolsServerConnection of the Watcher Actor. * This is used to compute a unique ID for the target actor. * @param Object sessionData - * All data managed by the Watcher Actor and WatcherRegistry.jsm, containing + * All data managed by the Watcher Actor and ParentProcessWatcherRegistry.jsm, containing * target types, resources types to be listened as well as breakpoints and any * other data meant to be shared across processes and threads. * @param Object options Dictionary with optional values: diff --git a/devtools/server/startup/worker.js b/devtools/server/startup/worker.js index 42034831ee..6ed880eb28 100644 --- a/devtools/server/startup/worker.js +++ b/devtools/server/startup/worker.js @@ -4,7 +4,7 @@ "use strict"; -/* global worker, loadSubScript, global */ +/* global global */ /* * Worker debugger script that listens for requests to start a `DevToolsServer` for a @@ -44,7 +44,10 @@ this.rpc = function (method, ...params) { }); }.bind(this); -loadSubScript("resource://devtools/shared/loader/worker-loader.js"); +const { worker } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/worker-loader.sys.mjs", + { global: "current" } +); const { WorkerTargetActor } = worker.require( "resource://devtools/server/actors/targets/worker.js" @@ -86,13 +89,6 @@ this.addEventListener("message", async function (event) { // Make the worker manage itself so it is put in a Pool and assigned an actorID. workerTargetActor.manage(workerTargetActor); - workerTargetActor.on( - "worker-thread-attached", - function onThreadAttached() { - postMessage(JSON.stringify({ type: "worker-thread-attached" })); - } - ); - // Step 5: Send a response packet to the parent to notify // it that a connection has been established. connections.set(forwardingPrefix, { @@ -100,6 +96,11 @@ this.addEventListener("message", async function (event) { workerTargetActor, }); + // Immediately notify about the target actor form, + // so that we can emit RDP events from the target actor + // and have them correctly routed up to the frontend. + // The target front has to be first created by receiving its form + // before being able to receive RDP events. postMessage( JSON.stringify({ type: "connected", @@ -126,6 +127,11 @@ this.addEventListener("message", async function (event) { await Promise.all(promises); } + // Finally, notify when we are done processing session data + // We are processing breakpoints, which means we can release the execution of the worker + // from the main thread via `WorkerDebugger.setDebuggerReady(true)` + postMessage(JSON.stringify({ type: "session-data-processed" })); + break; case "add-or-set-session-data-entry": diff --git a/devtools/server/tests/browser/browser_inspector-anonymous.js b/devtools/server/tests/browser/browser_inspector-anonymous.js index 024b7af1bb..526a3a5572 100644 --- a/devtools/server/tests/browser/browser_inspector-anonymous.js +++ b/devtools/server/tests/browser/browser_inspector-anonymous.js @@ -7,11 +7,11 @@ add_task(async function () { await SpecialPowers.pushPermissions([ - { type: "allowXULXBL", allow: true, context: MAIN_DOMAIN }, + { type: "allowXULXBL", allow: true, context: MAIN_DOMAIN_HTTPS }, ]); const { walker } = await initInspectorFront( - MAIN_DOMAIN + "inspector-traversal-data.html" + MAIN_DOMAIN_HTTPS + "inspector-traversal-data.html" ); await testXBLAnonymousInHTMLDocument(walker); diff --git a/devtools/server/tests/browser/doc_accessibility_keyboard_audit.html b/devtools/server/tests/browser/doc_accessibility_keyboard_audit.html index 00c002efe9..c4991a5f40 100644 --- a/devtools/server/tests/browser/doc_accessibility_keyboard_audit.html +++ b/devtools/server/tests/browser/doc_accessibility_keyboard_audit.html @@ -95,8 +95,10 @@ <div role="grid" aria-label="Interactive grid"> <div id="columnheader-1" role="columnheader"></div> <div id="rowheader-1" role="rowheader"></div> - <div id="gridcell-1" role="gridcell"></div> - <div id="gridcell-2" role="gridcell" tabindex="0"></div> + <div role="row"> + <div id="gridcell-1" role="gridcell"></div> + <div id="gridcell-2" role="gridcell" tabindex="0"></div> + </div> </div> <div role="table" aria-label="Non-interactive table"> <div id="columnheader-2" role="columnheader"></div> diff --git a/devtools/server/tests/browser/doc_accessibility_text_label_audit.html b/devtools/server/tests/browser/doc_accessibility_text_label_audit.html index 982cc5c243..24a23c96f0 100644 --- a/devtools/server/tests/browser/doc_accessibility_text_label_audit.html +++ b/devtools/server/tests/browser/doc_accessibility_text_label_audit.html @@ -61,13 +61,17 @@ <tr><th id="rowheader-7" scope="row" aria-labelledby="columnheader-7-label"></th></tr> </tbody> </table> - <div role="columnheader" id="columnheader-8">Film Title</div> - <div role="columnheader" id="columnheader-9"></div> - <div role="columnheader" id="columnheader-10"> </div> - <div role="columnheader" id="columnheader-11" aria-label="Worldwide Gross"></div> - <div role="columnheader" id="columnheader-12" aria-label=""></div> - <div role="columnheader" id="columnheader-13" aria-label=" "></div> - <div role="columnheader" id="columnheader-14" aria-labelledby="columnheader-7-label"></div> + <div role="grid"> + <div role="row"> + <div role="columnheader" id="columnheader-8">Film Title</div> + <div role="columnheader" id="columnheader-9"></div> + <div role="columnheader" id="columnheader-10"> </div> + <div role="columnheader" id="columnheader-11" aria-label="Worldwide Gross"></div> + <div role="columnheader" id="columnheader-12" aria-label=""></div> + <div role="columnheader" id="columnheader-13" aria-label=" "></div> + <div role="columnheader" id="columnheader-14" aria-labelledby="columnheader-7-label"></div> + </div> + </div> <label for="combobox-1">Choose a pet:</label> <select id="combobox-1"> <option id="combobox-option-1" value="">--Please choose an option--</option> @@ -263,14 +267,16 @@ <mi><mglyph id="mglyph-5" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" alt=""/></mi> <mi><mglyph id="mglyph-6" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAJklEQVRIie3NMREAAAgAoe9fWls4eAzMVM0xoVAoFAqFQqFQ+C9chp4NHvu+4Q4AAAAASUVORK5CYII=" aria-labelledby="mglyph-6-label"/></mi> </math> - <span id="menuitem-1" role="menuitem"></span> - <span id="menuitem-2" aria-label="" role="menuitem"></span> - <span id="menuitem-3" aria-label="Menu Item" role="menuitem"></span> - <p id="menuitem-4-label">Menu Item</p> - <span id="menuitem-4" aria-labelledby="menuitem-4-label" role="menuitem"></span> - <p id="menuitem-5-label"></p> - <span id="menuitem-5" aria-labelledby="menuitem-5-label" role="menuitem"></span> - <span id="menuitem-6" role="menuitem">Menu Item</span> + <div role="menu"> + <span id="menuitem-1" role="menuitem"></span> + <span id="menuitem-2" aria-label="" role="menuitem"></span> + <span id="menuitem-3" aria-label="Menu Item" role="menuitem"></span> + <p id="menuitem-4-label">Menu Item</p> + <span id="menuitem-4" aria-labelledby="menuitem-4-label" role="menuitem"></span> + <p id="menuitem-5-label"></p> + <span id="menuitem-5" aria-labelledby="menuitem-5-label" role="menuitem"></span> + <span id="menuitem-6" role="menuitem">Menu Item</span> + </div> <label for="listbox-1">Choose a pet:</label> <select id="listbox-1" size="6"> <option id="option-1" value="">--Please choose an option--</option> @@ -299,14 +305,16 @@ <p id="option-15-label"> </p> <div role="option" id="option-15" aria-labelledby="option-15-label"></div> </div> - <span id="treeitem-1" role="treeitem"></span> - <span id="treeitem-2" aria-label="" role="treeitem"></span> - <span id="treeitem-3" aria-label="Tree Item" role="treeitem"></span> - <p id="treeitem-4-label">Tree Item</p> - <span id="treeitem-4" aria-labelledby="treeitem-4-label" role="treeitem"></span> - <p id="treeitem-5-label"></p> - <span id="treeitem-5" aria-labelledby="treeitem-5-label" role="treeitem"></span> - <span id="treeitem-6" role="treeitem">Tree Item</span> + <div role="tree"> + <span id="treeitem-1" role="treeitem"></span> + <span id="treeitem-2" aria-label="" role="treeitem"></span> + <span id="treeitem-3" aria-label="Tree Item" role="treeitem"></span> + <p id="treeitem-4-label">Tree Item</p> + <span id="treeitem-4" aria-labelledby="treeitem-4-label" role="treeitem"></span> + <p id="treeitem-5-label"></p> + <span id="treeitem-5" aria-labelledby="treeitem-5-label" role="treeitem"></span> + <span id="treeitem-6" role="treeitem">Tree Item</span> + </div> <div role="tablist"> <span id="tab-1" role="tab"></span> <span id="tab-2" aria-label="" role="tab"></span> @@ -368,13 +376,17 @@ <div id="menuitemradio-2" role="menuitemradio"></div> <div id="menuitemradio-3" role="menuitemradio"> </div> </div> - <div role="rowheader" id="rowheader-8">Toy Story 3</div> - <div role="rowheader" id="rowheader-9"></div> - <div role="rowheader" id="rowheader-10"> </div> - <div role="rowheader" id="rowheader-11" aria-label="Alladin"></div> - <div role="rowheader" id="rowheader-12" aria-label=""></div> - <div role="rowheader" id="rowheader-13" aria-label=" "></div> - <div role="rowheader" id="rowheader-14" aria-labelledby="columnheader-7-label"></div> + <div role="grid"> + <div role="row"> + <div role="rowheader" id="rowheader-8">Toy Story 3</div> + <div role="rowheader" id="rowheader-9"></div> + <div role="rowheader" id="rowheader-10"> </div> + <div role="rowheader" id="rowheader-11" aria-label="Alladin"></div> + <div role="rowheader" id="rowheader-12" aria-label=""></div> + <div role="rowheader" id="rowheader-13" aria-label=" "></div> + <div role="rowheader" id="rowheader-14" aria-labelledby="columnheader-7-label"></div> + </div> + </div> <label>Slider label: <input type="range" id="slider-1"></label> <input type="range" id="slider-2"> <input type="range" id="slider-3" aria-label="Slider label:"> diff --git a/devtools/server/tests/browser/head.js b/devtools/server/tests/browser/head.js index aba6d578f2..a0363fe6d5 100644 --- a/devtools/server/tests/browser/head.js +++ b/devtools/server/tests/browser/head.js @@ -23,7 +23,9 @@ const { const PATH = "browser/devtools/server/tests/browser/"; const TEST_DOMAIN = "http://test1.example.org"; +const TEST_DOMAIN_HTTPS = "https://test1.example.org"; const MAIN_DOMAIN = `${TEST_DOMAIN}/${PATH}`; +const MAIN_DOMAIN_HTTPS = `${TEST_DOMAIN_HTTPS}/${PATH}`; const ALT_DOMAIN = "http://sectest1.example.org/" + PATH; const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH; diff --git a/devtools/server/tests/xpcshell/head_dbg.js b/devtools/server/tests/xpcshell/head_dbg.js index 7161d5eaea..e8547f15b8 100644 --- a/devtools/server/tests/xpcshell/head_dbg.js +++ b/devtools/server/tests/xpcshell/head_dbg.js @@ -22,8 +22,8 @@ appInfo.updateAppInfo({ const { require, loader } = ChromeUtils.importESModule( "resource://devtools/shared/loader/Loader.sys.mjs" ); -const { worker } = ChromeUtils.import( - "resource://devtools/shared/loader/worker-loader.js" +const { worker } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/worker-loader.sys.mjs" ); const { NetUtil } = ChromeUtils.importESModule( @@ -250,11 +250,6 @@ function waitForNewSource(threadFront, url) { }); } -function attachThread(targetFront, options = {}) { - dump("Attaching to thread.\n"); - return targetFront.attachThread(options); -} - function resume(threadFront) { dump("Resuming thread.\n"); return threadFront.resume(); @@ -445,10 +440,14 @@ async function attachTestTab(client, title) { async function attachTestThread(client, title) { const commands = await attachTestTab(client, title); const targetFront = commands.targetCommand.targetFront; - const threadFront = await targetFront.getFront("thread"); - await targetFront.attachThread({ - autoBlackBox: true, + + // Pass any configuration, in order to ensure starting all the thread actors + // and have them to handle debugger statements. + await commands.threadConfigurationCommand.updateConfiguration({ + skipBreakpoints: false, }); + + const threadFront = await targetFront.getFront("thread"); Assert.equal(threadFront.state, "attached", "Thread front is attached"); return { targetFront, threadFront, commands }; } @@ -856,7 +855,15 @@ async function setupTestFromUrl(url) { const targetFront = await descriptorFront.getTarget(); - const threadFront = await attachThread(targetFront); + const commands = await createCommandsDictionary(descriptorFront); + + // Pass any configuration, in order to ensure starting all the thread actor + // and have it to notify about all sources + await commands.threadConfigurationCommand.updateConfiguration({ + skipBreakpoints: false, + }); + + const threadFront = await targetFront.getFront("thread"); const sourceUrl = getFileUrl(url); const promise = waitForNewSource(threadFront, sourceUrl); diff --git a/devtools/server/tests/xpcshell/test_addon_debugging_connect.js b/devtools/server/tests/xpcshell/test_addon_debugging_connect.js index 221e73d256..f0318378c9 100644 --- a/devtools/server/tests/xpcshell/test_addon_debugging_connect.js +++ b/devtools/server/tests/xpcshell/test_addon_debugging_connect.js @@ -53,7 +53,7 @@ function promiseFrameUpdate(front, matcher = () => true) { add_task( { // This test needs to run only when the extension are running in a separate - // child process, otherwise attachThread would pause the main process and this + // child process, otherwise the thread actor would pause the main process and this // test would get stuck. skip_if: () => !WebExtensionPolicy.useRemoteWebExtensions, }, @@ -90,7 +90,7 @@ add_task( .pop(); ok(backgroundPageFrame, "Found the frame for the background page"); - const threadFront = await addonTarget.attachThread(); + const threadFront = await addonTarget.getFront("thread"); ok(threadFront, "Got a threadFront for the target addon"); equal(threadFront.paused, false, "The addon threadActor isn't paused"); diff --git a/devtools/server/tests/xpcshell/test_getRuleText.js b/devtools/server/tests/xpcshell/test_getRuleText.js index bc89da974c..6e67a5f85b 100644 --- a/devtools/server/tests/xpcshell/test_getRuleText.js +++ b/devtools/server/tests/xpcshell/test_getRuleText.js @@ -39,7 +39,7 @@ const TEST_DATA = [ input: "#id{color:red;background:yellow;}", line: 1, column: 1, - expected: { offset: 4, text: "color:red;background:yellow;" }, + expected: "color:red;background:yellow;", }, { desc: "Multiple rules test case", @@ -48,14 +48,14 @@ const TEST_DATA = [ "{ position:absolute; line-height: 45px}", line: 1, column: 34, - expected: { offset: 56, text: " position:absolute; line-height: 45px" }, + expected: " position:absolute; line-height: 45px", }, { desc: "Unclosed rule", input: "#id{color:red;background:yellow;", line: 1, column: 1, - expected: { offset: 4, text: "color:red;background:yellow;" }, + expected: "color:red;background:yellow;", }, { desc: "Multi-lines CSS", @@ -72,7 +72,7 @@ const TEST_DATA = [ ].join("\n"), line: 7, column: 1, - expected: { offset: 116, text: "\n color: purple;\n" }, + expected: "\n color: purple;\n", }, { desc: "Multi-lines CSS and multi-line rule", @@ -98,75 +98,64 @@ const TEST_DATA = [ ].join("\n"), line: 5, column: 1, - expected: { - offset: 30, - text: "\n margin: 0;\n padding: 15px 15px 2px 15px;\n color: red;\n", - }, + expected: + "\n margin: 0;\n padding: 15px 15px 2px 15px;\n color: red;\n", }, { desc: "Content string containing a } character", input: " #id{border:1px solid red;content: '}';color:red;}", line: 1, column: 4, - expected: { - offset: 7, - text: "border:1px solid red;content: '}';color:red;", - }, + expected: "border:1px solid red;content: '}';color:red;", }, { desc: "Attribute selector containing a { character", input: `div[data-x="{"]{color: gold}`, line: 1, column: 1, - expected: { - offset: 16, - text: "color: gold", - }, + expected: "color: gold", }, { desc: "Rule contains no tokens", input: "div{}", line: 1, column: 1, - expected: { offset: 4, text: "" }, + expected: "", }, { desc: "Rule contains invalid declaration", input: `#id{color;}`, line: 1, column: 1, - expected: { offset: 4, text: "color;" }, + expected: "color;", }, { desc: "Rule contains invalid declaration", input: `#id{-}`, line: 1, column: 1, - expected: { offset: 4, text: "-" }, + expected: "-", }, { desc: "Rule contains nested rule", input: `#id{background: gold; .nested{color:blue;} color: tomato; }`, line: 1, column: 1, - expected: { - offset: 4, - text: "background: gold; .nested{color:blue;} color: tomato; ", - }, + expected: "background: gold; .nested{color:blue;} color: tomato; ", }, { desc: "Rule contains nested rule with invalid declaration", input: `#id{.nested{color;}}`, line: 1, column: 1, - expected: { offset: 4, text: ".nested{color;}" }, + expected: ".nested{color;}", }, { desc: "Rule contains unicode chars", input: `#id /*🙃*/ {content: "☃️";}`, line: 1, column: 1, - expected: { offset: 12, text: `content: "☃️";` }, + expected: `content: "☃️";`, }, ]; @@ -192,7 +181,7 @@ function run_test() { } } if (output) { - deepEqual(output, test.expected); + Assert.equal(output, test.expected); } } } diff --git a/devtools/server/tests/xpcshell/test_sessionDataHelpers.js b/devtools/server/tests/xpcshell/test_sessionDataHelpers.js index e0dcc3b21b..0c17937a69 100644 --- a/devtools/server/tests/xpcshell/test_sessionDataHelpers.js +++ b/devtools/server/tests/xpcshell/test_sessionDataHelpers.js @@ -7,8 +7,9 @@ "use strict"; -const { SessionDataHelpers } = ChromeUtils.import( - "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 { TARGETS } = SUPPORTED_DATA; diff --git a/devtools/server/tests/xpcshell/test_xpcshell_debugging.js b/devtools/server/tests/xpcshell/test_xpcshell_debugging.js index ff54d7390d..12d38b923d 100644 --- a/devtools/server/tests/xpcshell/test_xpcshell_debugging.js +++ b/devtools/server/tests/xpcshell/test_xpcshell_debugging.js @@ -35,11 +35,17 @@ add_task(async function () { ); // Even though we have no tabs, getMainProcess gives us the chrome debugger. - const targetDescriptor = await client.mainRoot.getMainProcess(); - const front = await targetDescriptor.getTarget(); - const watcher = await targetDescriptor.getWatcher(); + const commands = await CommandsFactory.forMainProcess({ client }); + await commands.targetCommand.startListening(); - const threadFront = await front.attachThread(); + // We have to pass at least one valid thread configuration in order to initialize + // the thread actor and make it pause on breakpoint/debugger statements. + await commands.threadConfigurationCommand.updateConfiguration({ + skipBreakpoints: false, + }); + const threadFront = await commands.targetCommand.targetFront.getFront( + "thread" + ); // Checks that the thread actor initializes immediately and that _setupDevToolsServer // callback gets called. @@ -72,7 +78,7 @@ add_task(async function () { ); info("Dynamically add a breakpoint after the debugger statement"); - const breakpointsFront = await watcher.getBreakpointListActor(); + const breakpointsFront = await commands.watcherFront.getBreakpointListActor(); await breakpointsFront.setBreakpoint( { sourceUrl: testFile.path, line: 11, column: 0 }, {} diff --git a/devtools/server/tests/xpcshell/testactors.js b/devtools/server/tests/xpcshell/testactors.js index bbcd8abe6e..6df7f0ce88 100644 --- a/devtools/server/tests/xpcshell/testactors.js +++ b/devtools/server/tests/xpcshell/testactors.js @@ -8,6 +8,9 @@ const { createExtraActors, } = require("resource://devtools/shared/protocol/lazy-pool.js"); const { RootActor } = require("resource://devtools/server/actors/root.js"); +const { + WatcherActor, +} = require("resource://devtools/server/actors/watcher.js"); const { ThreadActor } = require("resource://devtools/server/actors/thread.js"); const { DevToolsServer, @@ -30,6 +33,14 @@ const Targets = require("resource://devtools/server/actors/targets/index.js"); const { createContentProcessSessionContext, } = require("resource://devtools/server/actors/watcher/session-context.js"); +const { TargetActorRegistry } = ChromeUtils.importESModule( + "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs", + { global: "shared" } +); +const { + BaseTargetActor, +} = require("resource://devtools/server/actors/targets/base-target-actor.js"); +const Resources = require("resource://devtools/server/actors/resources/index.js"); var gTestGlobals = new Set(); DevToolsServer.addTestGlobal = function (global) { @@ -78,6 +89,9 @@ function TestTabList(connection) { const actor = new TestTargetActor(connection, global); this._descriptorActorPool.manage(actor); + // Register the target actor, so that the Watcher actor can have access to it. + TargetActorRegistry.registerXpcShellTargetActor(actor); + const descriptorActor = new TestDescriptorActor(connection, actor); this._descriptorActorPool.manage(descriptorActor); @@ -134,7 +148,9 @@ class TestDescriptorActor extends protocol.Actor { form() { const form = { actor: this.actorID, - traits: {}, + traits: { + watcher: true, + }, selected: this.selected, title: this._targetActor.title, url: this._targetActor.url, @@ -143,6 +159,20 @@ class TestDescriptorActor extends protocol.Actor { return form; } + getWatcher() { + const sessionContext = { + type: "all", + supportedTargets: {}, + supportedResources: [ + Resources.TYPES.SOURCE, + Resources.TYPES.CONSOLE_MESSAGE, + Resources.TYPES.THREAD_STATE, + ], + }; + const watcherActor = new WatcherActor(this.conn, sessionContext); + return watcherActor; + } + getFavicon() { return ""; } @@ -152,9 +182,9 @@ class TestDescriptorActor extends protocol.Actor { } } -class TestTargetActor extends protocol.Actor { +class TestTargetActor extends BaseTargetActor { constructor(conn, global) { - super(conn, windowGlobalTargetSpec); + super(conn, Targets.TYPES.FRAME, windowGlobalTargetSpec); this.sessionContext = createContentProcessSessionContext(); this._global = global; diff --git a/devtools/server/tracer/moz.build b/devtools/server/tracer/moz.build index 26f7665018..40ded9ba8a 100644 --- a/devtools/server/tracer/moz.build +++ b/devtools/server/tracer/moz.build @@ -4,7 +4,7 @@ # 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/. -DevToolsModules("tracer.jsm") +DevToolsModules("tracer.sys.mjs") XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"] if CONFIG["MOZ_BUILD_APP"] != "mobile/android": diff --git a/devtools/server/tracer/tests/browser/WorkerDebugger.tracer.js b/devtools/server/tracer/tests/browser/WorkerDebugger.tracer.js index bd6e646b3b..ef15fd5cfb 100644 --- a/devtools/server/tracer/tests/browser/WorkerDebugger.tracer.js +++ b/devtools/server/tracer/tests/browser/WorkerDebugger.tracer.js @@ -1,6 +1,6 @@ "use strict"; -/* global global, loadSubScript */ +/* global global */ try { // For some reason WorkerDebuggerGlobalScope.global doesn't expose JS variables @@ -8,8 +8,11 @@ try { const dbg = new Debugger(global); const [debuggee] = dbg.getDebuggees(); - /* global startTracing, stopTracing, addTracingListener, removeTracingListener */ - loadSubScript("resource://devtools/server/tracer/tracer.jsm"); + const { JSTracer } = ChromeUtils.importESModule( + "resource://devtools/server/tracer/tracer.sys.mjs", + { global: "contextual" } + ); + const frames = []; const listener = { onTracingFrame(args) { @@ -19,13 +22,13 @@ try { return true; }, }; - addTracingListener(listener); - startTracing({ global, prefix: "testWorkerPrefix" }); + JSTracer.addTracingListener(listener); + JSTracer.startTracing({ global, prefix: "testWorkerPrefix" }); debuggee.executeInGlobal("foo()"); - stopTracing(); - removeTracingListener(listener); + JSTracer.stopTracing(); + JSTracer.removeTracingListener(listener); // Send the frames to the main thread to do the assertions there. postMessage(JSON.stringify(frames)); diff --git a/devtools/server/tracer/tests/browser/browser_document_tracer.js b/devtools/server/tracer/tests/browser/browser_document_tracer.js index dcf2c9eb4d..d7d351d6dd 100644 --- a/devtools/server/tracer/tests/browser/browser_document_tracer.js +++ b/devtools/server/tracer/tests/browser/browser_document_tracer.js @@ -17,12 +17,10 @@ add_task(async function testTracingWorker() { const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { - const { - addTracingListener, - removeTracingListener, - startTracing, - stopTracing, - } = ChromeUtils.import("resource://devtools/server/tracer/tracer.jsm"); + const { JSTracer } = ChromeUtils.importESModule( + "resource://devtools/server/tracer/tracer.sys.mjs", + { global: "shared" } + ); // We have to fake opening DevTools otherwise DebuggerNotificationObserver wouldn't work // and the tracer wouldn't be able to trace the DOM events. @@ -35,10 +33,10 @@ add_task(async function testTracingWorker() { }, }; info("Register a tracing listener"); - addTracingListener(listener); + JSTracer.addTracingListener(listener); info("Start tracing the iframe"); - startTracing({ global: content, traceDOMEvents: true }); + JSTracer.startTracing({ global: content, traceDOMEvents: true }); info("Dispatch a click event on the iframe"); EventUtils.synthesizeMouseAtCenter( @@ -58,8 +56,8 @@ add_task(async function testTracingWorker() { is(lastFrame.formatedDisplayName, "λ bar"); is(lastFrame.currentDOMEvent, "setTimeoutCallback"); - stopTracing(); - removeTracingListener(listener); + JSTracer.stopTracing(); + JSTracer.removeTracingListener(listener); ChromeUtils.notifyDevToolsClosed(); }); diff --git a/devtools/server/tracer/tests/xpcshell/test_tracer.js b/devtools/server/tracer/tests/xpcshell/test_tracer.js index 0f38052ba5..8435cb8691 100644 --- a/devtools/server/tracer/tests/xpcshell/test_tracer.js +++ b/devtools/server/tracer/tests/xpcshell/test_tracer.js @@ -3,8 +3,10 @@ "use strict"; -const { addTracingListener, removeTracingListener, startTracing, stopTracing } = - ChromeUtils.import("resource://devtools/server/tracer/tracer.jsm"); +const { JSTracer } = ChromeUtils.importESModule( + "resource://devtools/server/tracer/tracer.sys.mjs", + { global: "shared" } +); add_task(async function () { // Because this test uses evalInSandbox, we need to tweak the following prefs @@ -30,13 +32,13 @@ add_task(async function testTracingContentGlobal() { }; info("Register a tracing listener"); - addTracingListener(listener); + JSTracer.addTracingListener(listener); const sandbox = Cu.Sandbox("https://example.com"); Cu.evalInSandbox("function bar() {}; function foo() {bar()};", sandbox); info("Start tracing"); - startTracing({ global: sandbox, prefix: "testContentPrefix" }); + JSTracer.startTracing({ global: sandbox, prefix: "testContentPrefix" }); Assert.equal(toggles.length, 1); Assert.equal(toggles[0], true); @@ -56,7 +58,7 @@ add_task(async function testTracingContentGlobal() { Assert.ok(lastFrame.frame); info("Stop tracing"); - stopTracing(); + JSTracer.stopTracing(); Assert.equal(toggles.length, 2); Assert.equal(toggles[1], false); @@ -65,19 +67,19 @@ add_task(async function testTracingContentGlobal() { Assert.equal(frames.length, 0); info("Start tracing again, and recall code"); - startTracing({ global: sandbox, prefix: "testContentPrefix" }); + JSTracer.startTracing({ global: sandbox, prefix: "testContentPrefix" }); sandbox.foo(); info("New traces are logged"); Assert.equal(frames.length, 2); info("Unregister the listener and recall code"); - removeTracingListener(listener); + JSTracer.removeTracingListener(listener); sandbox.foo(); info("No more traces are logged"); Assert.equal(frames.length, 2); info("Stop tracing"); - stopTracing(); + JSTracer.stopTracing(); }); add_task(async function testTracingJSMGlobal() { @@ -103,10 +105,10 @@ add_task(async function testTracingJSMGlobal() { ); info("Register a tracing listener"); - addTracingListener(listenerSandbox.listener); + JSTracer.addTracingListener(listenerSandbox.listener); info("Start tracing"); - startTracing({ global: null, prefix: "testPrefix" }); + JSTracer.startTracing({ global: null, prefix: "testPrefix" }); Assert.equal(listenerSandbox.toggles.length, 1); Assert.equal(listenerSandbox.toggles[0], true); @@ -131,11 +133,11 @@ add_task(async function testTracingJSMGlobal() { Assert.ok(lastFrame.frame); info("Stop tracing"); - stopTracing(); + JSTracer.stopTracing(); Assert.equal(listenerSandbox.toggles.length, 2); Assert.equal(listenerSandbox.toggles[1], false); - removeTracingListener(listenerSandbox.listener); + JSTracer.removeTracingListener(listenerSandbox.listener); }); add_task(async function testTracingValues() { @@ -153,7 +155,7 @@ add_task(async function testTracingValues() { } info("Start tracing"); - startTracing({ global: sandbox, traceValues: true, loggingMethod }); + JSTracer.startTracing({ global: sandbox, traceValues: true, loggingMethod }); info("Call some code"); sandbox.foo(); @@ -167,7 +169,7 @@ add_task(async function testTracingValues() { ); info("Stop tracing"); - stopTracing(); + JSTracer.stopTracing(); }); add_task(async function testTracingFunctionReturn() { @@ -185,7 +187,11 @@ add_task(async function testTracingFunctionReturn() { } info("Start tracing"); - startTracing({ global: sandbox, traceFunctionReturn: true, loggingMethod }); + JSTracer.startTracing({ + global: sandbox, + traceFunctionReturn: true, + loggingMethod, + }); info("Call some code"); sandbox.foo(); @@ -198,7 +204,7 @@ add_task(async function testTracingFunctionReturn() { Assert.stringContains(logs[4], "λ foo return"); info("Stop tracing"); - stopTracing(); + JSTracer.stopTracing(); }); add_task(async function testTracingFunctionReturnAndValues() { @@ -216,7 +222,7 @@ add_task(async function testTracingFunctionReturnAndValues() { } info("Start tracing"); - startTracing({ + JSTracer.startTracing({ global: sandbox, traceFunctionReturn: true, traceValues: true, @@ -236,7 +242,7 @@ add_task(async function testTracingFunctionReturnAndValues() { Assert.stringContains(logs[6], "λ foo return undefined"); info("Stop tracing"); - stopTracing(); + JSTracer.stopTracing(); }); add_task(async function testTracingStep() { @@ -246,22 +252,23 @@ add_task(async function testTracingStep() { function foo() { bar(); /* line 3 */ second(); /* line 4 */ + dump("foo\\n"); } function bar() { - let res; /* line 7 */ - if (1 === 1) { /* line 8 */ - res = "string"; /* line 9 */ + let res; /* line 8 */ + if (1 === 1) { /* line 9 */ + res = "string"; /* line 10 */ } else { res = "nope" } - return res; /* line 13 */ + return res; /* line 14 */ }; function second() { - let x = 0; /* line 16 */ - for (let i = 0; i < 2; i++) { /* line 17 */ - x++; /* line 18 */ + let x = 0; /* line 17 */ + for (let i = 0; i < 2; i++) { /* line 18 */ + x++; /* line 19 */ } - return null; /* line 20 */ + return null; /* line 21 */ }; foo();`; Cu.evalInSandbox(source, sandbox, null, "file.js", 1); @@ -273,7 +280,7 @@ foo();`; } info("Start tracing"); - startTracing({ + JSTracer.startTracing({ global: sandbox, traceSteps: true, loggingMethod, @@ -287,47 +294,46 @@ foo();`; Assert.stringContains(logs[1], "λ foo"); Assert.stringContains(logs[1], "file.js:3:3"); - // Each "step" only prints the location and nothing more - Assert.stringContains(logs[2], "file.js:3:3"); - - Assert.stringContains(logs[3], "λ bar"); - Assert.stringContains(logs[3], "file.js:6:16"); + Assert.stringContains(logs[2], "λ bar"); + Assert.stringContains(logs[2], "file.js:7:16"); - Assert.stringContains(logs[4], "file.js:8:7"); + // Each "step" only prints the location and nothing more + Assert.stringContains(logs[3], "file.js:9:7"); - Assert.stringContains(logs[5], "file.js:9:5"); + Assert.stringContains(logs[4], "file.js:10:5"); - Assert.stringContains(logs[6], "file.js:13:3"); + Assert.stringContains(logs[5], "file.js:14:3"); - Assert.stringContains(logs[7], "file.js:4:3"); + Assert.stringContains(logs[6], "file.js:4:3"); - Assert.stringContains(logs[8], "λ second"); - Assert.stringContains(logs[8], "file.js:15:19"); + Assert.stringContains(logs[7], "λ second"); + Assert.stringContains(logs[7], "file.js:16:19"); - Assert.stringContains(logs[9], "file.js:16:11"); + Assert.stringContains(logs[8], "file.js:17:11"); // For loop - Assert.stringContains(logs[10], "file.js:17:16"); + Assert.stringContains(logs[9], "file.js:18:16"); - Assert.stringContains(logs[11], "file.js:17:19"); + Assert.stringContains(logs[10], "file.js:18:19"); - Assert.stringContains(logs[12], "file.js:18:5"); + Assert.stringContains(logs[11], "file.js:19:5"); - Assert.stringContains(logs[13], "file.js:17:26"); + Assert.stringContains(logs[12], "file.js:18:26"); - Assert.stringContains(logs[14], "file.js:17:19"); + Assert.stringContains(logs[13], "file.js:18:19"); - Assert.stringContains(logs[15], "file.js:18:5"); + Assert.stringContains(logs[14], "file.js:19:5"); - Assert.stringContains(logs[16], "file.js:17:26"); + Assert.stringContains(logs[15], "file.js:18:26"); - Assert.stringContains(logs[17], "file.js:17:19"); + Assert.stringContains(logs[16], "file.js:18:19"); // End of for loop - Assert.stringContains(logs[18], "file.js:20:3"); + Assert.stringContains(logs[17], "file.js:21:3"); + Assert.stringContains(logs[18], "file.js:5:3"); info("Stop tracing"); - stopTracing(); + JSTracer.stopTracing(); }); add_task(async function testTracingPauseOnStep() { @@ -348,7 +354,7 @@ add_task(async function testTracingPauseOnStep() { } info("Start tracing without pause"); - startTracing({ + JSTracer.startTracing({ global: sandbox, loggingMethod, }); @@ -366,13 +372,13 @@ add_task(async function testTracingPauseOnStep() { Assert.equal(sandbox.counter, 1); info("Stop tracing"); - stopTracing(); + JSTracer.stopTracing(); logs.length = 0; sandbox.counter = 0; info("Start tracing with 0ms pause"); - startTracing({ + JSTracer.startTracing({ global: sandbox, pauseOnStep: 0, loggingMethod, @@ -415,13 +421,13 @@ add_task(async function testTracingPauseOnStep() { Assert.equal(sandbox.counter, 1); info("Stop tracing"); - stopTracing(); + JSTracer.stopTracing(); logs.length = 0; sandbox.counter = 0; info("Start tracing with 250ms pause"); - startTracing({ + JSTracer.startTracing({ global: sandbox, pauseOnStep: 250, loggingMethod, @@ -463,7 +469,7 @@ add_task(async function testTracingPauseOnStep() { Assert.greater(Cu.now() - startTimestamp, 250); info("Stop tracing"); - stopTracing(); + JSTracer.stopTracing(); }); add_task(async function testTracingFilterSourceUrl() { @@ -485,7 +491,7 @@ add_task(async function testTracingFilterSourceUrl() { } info("Start tracing"); - startTracing({ + JSTracer.startTracing({ global: sandbox, filterFrameSourceUrl: "second", loggingMethod, @@ -500,5 +506,60 @@ add_task(async function testTracingFilterSourceUrl() { Assert.stringContains(logs[1], "second.js:1:18"); info("Stop tracing"); - stopTracing(); + JSTracer.stopTracing(); +}); + +add_task(async function testTracingAllGlobals() { + // Test the `traceAllGlobals` flag + + // Create two distinct globals in order to verify that both are traced + const sandbox1 = Cu.Sandbox("https://example.com"); + const sandbox2 = Cu.Sandbox("https://example.com"); + + const source1 = `function foo() { bar(); }`; + Cu.evalInSandbox(source1, sandbox1, null, "sandbox1.js", 1); + + const source2 = `function bar() { }`; + Cu.evalInSandbox(source2, sandbox2, null, "sandbox2.js", 1); + // Expose `bar` from sandbox2 as global in sandbox1, so that `foo` from sandbox1 can call it. + sandbox1.bar = sandbox2.bar; + + // Pass an override method to catch all strings tentatively logged to stdout + // + // But in this test, we have to evaluate it in a special sandbox which will be ignored by the tracer. + // Otherwise, the tracer would do an infinite loop on this loggingMethod. + const ignoredGlobal = new Cu.Sandbox(null, { invisibleToDebugger: true }); + const loggingMethodString = ` + var logs = []; + function loggingMethod(str) { + logs.push(str); + }; + `; + Cu.evalInSandbox( + loggingMethodString, + ignoredGlobal, + null, + "loggin-method.js", + 1 + ); + const { loggingMethod, logs } = ignoredGlobal; + + info("Start tracing on all globals"); + JSTracer.startTracing({ + traceAllGlobals: true, + loggingMethod, + }); + + // Call some code while being careful to not call anything else which may be traced + sandbox1.foo(); + + JSTracer.stopTracing(); + + Assert.equal(logs.length, 4); + Assert.equal(logs[0], "Start tracing JavaScript\n"); + Assert.stringContains(logs[1], "λ foo"); + Assert.stringContains(logs[1], "sandbox1.js:1:18"); + Assert.stringContains(logs[2], "λ bar"); + Assert.stringContains(logs[2], "sandbox2.js:1:18"); + Assert.equal(logs[3], "Stop tracing JavaScript\n"); }); diff --git a/devtools/server/tracer/tracer.jsm b/devtools/server/tracer/tracer.sys.mjs index 955b25fe3a..6fe1334f2b 100644 --- a/devtools/server/tracer/tracer.jsm +++ b/devtools/server/tracer/tracer.sys.mjs @@ -17,17 +17,6 @@ * `JavaScriptTracer.onEnterFrame` method is hot codepath and should be reviewed accordingly. */ -"use strict"; - -const EXPORTED_SYMBOLS = [ - "startTracing", - "stopTracing", - "addTracingListener", - "removeTracingListener", - "NEXT_INTERACTION_MESSAGE", - "DOM_MUTATIONS", -]; - const NEXT_INTERACTION_MESSAGE = "Waiting for next user interaction before tracing (next mousedown or keydown event)"; @@ -93,7 +82,8 @@ const customLazy = { get DistinctCompartmentDebugger() { const { addDebuggerToGlobal } = ChromeUtils.importESModule( - "resource://gre/modules/jsdebugger.sys.mjs" + "resource://gre/modules/jsdebugger.sys.mjs", + { global: "contextual" } ); const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); const debuggerSandbox = Cu.Sandbox(systemPrincipal, { @@ -117,12 +107,10 @@ const customLazy = { * @param {Object} options.global * The tracer only log traces related to the code executed within this global. * When omitted, it will default to the options object's global. + * @param {Boolean} options.traceAllGlobals + * When set to true, this will trace all the globals running in the current thread. * @param {String} options.prefix * Optional string logged as a prefix to all traces. - * @param {Debugger} options.dbg - * Optional spidermonkey's Debugger instance. - * This allows devtools to pass a custom instance and ease worker support - * where we can't load jsdebugger.sys.mjs. * @param {Boolean} options.loggingMethod * Optional setting to use something else than `dump()` to log traces to stdout. * This is mostly used by tests. @@ -167,10 +155,29 @@ class JavaScriptTracer { this.abortController = new AbortController(); } - // By default, we would trace only JavaScript related to caller's global. - // As there is no way to compute the caller's global default to the global of the - // mandatory options argument. - this.tracedGlobal = options.global || Cu.getGlobalForObject(options); + if (options.traceAllGlobals) { + this.traceAllGlobals = true; + if (options.traceOnNextInteraction) { + throw new Error( + "Tracing all globals and waiting for next user interaction are not yet compatible" + ); + } + if (this.traceDOMEvents) { + throw new Error( + "Tracing all globals and DOM Events are not yet compatible" + ); + } + if (options.global) { + throw new Error( + "'global' option should be omitted when using 'traceAllGlobals'" + ); + } + } else { + // By default, we would trace only JavaScript related to caller's global. + // As there is no way to compute the caller's global default to the global of the + // mandatory options argument. + this.tracedGlobal = options.global || Cu.getGlobalForObject(options); + } // Instantiate a brand new Debugger API so that we can trace independently // of all other DevTools operations. i.e. we can pause while tracing without any interference. @@ -495,6 +502,20 @@ class JavaScriptTracer { * This allows to implement tracing independently of DevTools. */ makeDebugger() { + if (this.traceAllGlobals) { + const dbg = new customLazy.DistinctCompartmentDebugger(); + dbg.addAllGlobalsAsDebuggees(); + + // addAllGlobalAsAdebuggees will also add the global for this module... + // which we have to prevent tracing! + // eslint-disable-next-line mozilla/reject-globalThis-modification + dbg.removeDebuggee(globalThis); + + // Add any future global being created later + dbg.onNewGlobalObject = g => dbg.addDebuggee(g); + return dbg; + } + // When this code runs in the worker thread, Cu isn't available // and we don't have system principal anyway in this context. const { isSystemPrincipal } = @@ -637,6 +658,11 @@ class JavaScriptTracer { currentDOMEvent: this.currentDOMEvent, }); } + // Bail out early if any listener stopped tracing as the Frame object + // will be no longer usable by any other code. + if (!this.isTracing) { + return; + } } } @@ -647,13 +673,26 @@ class JavaScriptTracer { } if (this.traceSteps) { + // Collect the location notified via onTracingFrame to also avoid redundancy between similar location + // between onEnterFrame and onStep notifications. + let { lineNumber: lastLine, columnNumber: lastColumn } = + frame.script.getOffsetMetadata(frame.offset); + frame.onStep = () => { // Spidermonkey steps on many intermediate positions which don't make sense to the user. // `isStepStart` is close to each statement start, which is meaningful to the user. - const { isStepStart } = frame.script.getOffsetMetadata(frame.offset); + const { isStepStart, lineNumber, columnNumber } = + frame.script.getOffsetMetadata(frame.offset); if (!isStepStart) { return; } + // onStep may be called on many instructions related to the same line and colunm. + // Avoid notifying duplicated steps if we stepped on the exact same location. + if (lastLine == lineNumber && lastColumn == columnNumber) { + return; + } + lastLine = lineNumber; + lastColumn = columnNumber; shouldLogToStdout = true; if (listeners.size > 0) { @@ -1039,12 +1078,11 @@ function syncPause(duration) { }); } -// This JSM may be execute as CommonJS when loaded in the worker thread -if (typeof module == "object") { - module.exports = { - startTracing, - stopTracing, - addTracingListener, - removeTracingListener, - }; -} +export const JSTracer = { + startTracing, + stopTracing, + addTracingListener, + removeTracingListener, + NEXT_INTERACTION_MESSAGE, + DOM_MUTATIONS, +}; |