| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as i18n from '../../core/i18n/i18n.js'; |
| import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; |
| import type * as Protocol from '../../generated/protocol.js'; |
| import * as Common from '../common/common.js'; |
| import * as Host from '../host/host.js'; |
| |
| import {PrimaryPageChangeType, ResourceTreeModel} from './ResourceTreeModel.js'; |
| import {SDKModel} from './SDKModel.js'; |
| import {SecurityOriginManager} from './SecurityOriginManager.js'; |
| import {StorageKeyManager} from './StorageKeyManager.js'; |
| import {Capability, type Target, Type} from './Target.js'; |
| import {Events as TargetManagerEvents, type TargetManager} from './TargetManager.js'; |
| |
| const UIStrings = { |
| /** |
| * @description Text that refers to the main target. The main target is the primary webpage that |
| * DevTools is connected to. This text is used in various places in the UI as a label/name to inform |
| * the user which target/webpage they are currently connected to, as DevTools may connect to multiple |
| * targets at the same time in some scenarios. |
| */ |
| main: 'Main', |
| } as const; |
| const str_ = i18n.i18n.registerUIStrings('core/sdk/ChildTargetManager.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| export class ChildTargetManager extends SDKModel<EventTypes> implements ProtocolProxyApi.TargetDispatcher { |
| readonly #targetManager: TargetManager; |
| #parentTarget: Target; |
| readonly #targetAgent: ProtocolProxyApi.TargetApi; |
| readonly #targetInfos = new Map<Protocol.Target.TargetID, Protocol.Target.TargetInfo>(); |
| readonly #childTargetsBySessionId = new Map<Protocol.Target.SessionID, Target>(); |
| readonly #childTargetsById = new Map<Protocol.Target.TargetID|'main', Target>(); |
| #parentTargetId: Protocol.Target.TargetID|null = null; |
| |
| constructor(parentTarget: Target) { |
| super(parentTarget); |
| this.#targetManager = parentTarget.targetManager(); |
| this.#parentTarget = parentTarget; |
| this.#targetAgent = parentTarget.targetAgent(); |
| parentTarget.registerTargetDispatcher(this); |
| const browserTarget = this.#targetManager.browserTarget(); |
| if (browserTarget) { |
| if (browserTarget !== parentTarget) { |
| void browserTarget.targetAgent().invoke_autoAttachRelated( |
| {targetId: parentTarget.id() as Protocol.Target.TargetID, waitForDebuggerOnStart: true}); |
| } |
| } else if (parentTarget.type() === Type.NODE) { |
| void this.#targetAgent.invoke_setAutoAttach({autoAttach: true, waitForDebuggerOnStart: true, flatten: false}); |
| } else { |
| void this.#targetAgent.invoke_setAutoAttach({autoAttach: true, waitForDebuggerOnStart: true, flatten: true}); |
| } |
| |
| if (parentTarget.parentTarget()?.type() !== Type.FRAME && !Host.InspectorFrontendHost.isUnderTest()) { |
| void this.#targetAgent.invoke_setDiscoverTargets({discover: true}); |
| void this.#targetAgent.invoke_setRemoteLocations({locations: [{host: 'localhost', port: 9229}]}); |
| } |
| } |
| |
| static install(attachCallback?: ((arg0: { |
| target: Target, |
| waitingForDebugger: boolean, |
| }) => Promise<void>)): void { |
| ChildTargetManager.attachCallback = attachCallback; |
| SDKModel.register(ChildTargetManager, {capabilities: Capability.TARGET, autostart: true}); |
| } |
| |
| childTargets(): Target[] { |
| return Array.from(this.#childTargetsBySessionId.values()); |
| } |
| |
| override async suspendModel(): Promise<void> { |
| await this.#targetAgent.invoke_setAutoAttach({autoAttach: true, waitForDebuggerOnStart: false, flatten: true}); |
| } |
| |
| override async resumeModel(): Promise<void> { |
| await this.#targetAgent.invoke_setAutoAttach({autoAttach: true, waitForDebuggerOnStart: true, flatten: true}); |
| } |
| |
| override dispose(): void { |
| for (const sessionId of this.#childTargetsBySessionId.keys()) { |
| this.detachedFromTarget({sessionId, targetId: undefined}); |
| } |
| } |
| |
| targetCreated({targetInfo}: Protocol.Target.TargetCreatedEvent): void { |
| this.#targetInfos.set(targetInfo.targetId, targetInfo); |
| this.fireAvailableTargetsChanged(); |
| this.dispatchEventToListeners(Events.TARGET_CREATED, targetInfo); |
| } |
| |
| targetInfoChanged({targetInfo}: Protocol.Target.TargetInfoChangedEvent): void { |
| this.#targetInfos.set(targetInfo.targetId, targetInfo); |
| const target = this.#childTargetsById.get(targetInfo.targetId); |
| if (target) { |
| void target.setHasCrashed(false); |
| if (target.targetInfo()?.subtype === 'prerender' && !targetInfo.subtype) { |
| const resourceTreeModel = target.model(ResourceTreeModel); |
| target.updateTargetInfo(targetInfo); |
| if (resourceTreeModel?.mainFrame) { |
| resourceTreeModel.primaryPageChanged(resourceTreeModel.mainFrame, PrimaryPageChangeType.ACTIVATION); |
| } |
| target.setName(i18nString(UIStrings.main)); |
| } else { |
| target.updateTargetInfo(targetInfo); |
| } |
| } |
| this.fireAvailableTargetsChanged(); |
| this.dispatchEventToListeners(Events.TARGET_INFO_CHANGED, targetInfo); |
| } |
| |
| targetDestroyed({targetId}: Protocol.Target.TargetDestroyedEvent): void { |
| this.#targetInfos.delete(targetId); |
| this.fireAvailableTargetsChanged(); |
| this.dispatchEventToListeners(Events.TARGET_DESTROYED, targetId); |
| } |
| |
| targetCrashed({targetId}: Protocol.Target.TargetCrashedEvent): void { |
| const target = this.#childTargetsById.get(targetId); |
| if (target) { |
| target.setHasCrashed(true); |
| } |
| } |
| |
| private fireAvailableTargetsChanged(): void { |
| this.#targetManager.dispatchEventToListeners( |
| TargetManagerEvents.AVAILABLE_TARGETS_CHANGED, [...this.#targetInfos.values()]); |
| } |
| |
| async getParentTargetId(): Promise<Protocol.Target.TargetID> { |
| if (!this.#parentTargetId) { |
| this.#parentTargetId = (await this.#parentTarget.targetAgent().invoke_getTargetInfo({})).targetInfo.targetId; |
| } |
| return this.#parentTargetId; |
| } |
| |
| async getTargetInfo(): Promise<Protocol.Target.TargetInfo> { |
| return (await this.#parentTarget.targetAgent().invoke_getTargetInfo({})).targetInfo; |
| } |
| |
| async attachedToTarget({sessionId, targetInfo, waitingForDebugger}: Protocol.Target.AttachedToTargetEvent): |
| Promise<void> { |
| if (this.#parentTargetId === targetInfo.targetId) { |
| return; |
| } |
| let type = Type.BROWSER; |
| let targetName = ''; |
| if (targetInfo.type === 'worker' && targetInfo.title && targetInfo.title !== targetInfo.url) { |
| targetName = targetInfo.title; |
| } else if (!['page', 'iframe', 'webview'].includes(targetInfo.type)) { |
| const KNOWN_FRAME_PATTERNS = [ |
| '^chrome://print/$', |
| '^chrome://file-manager/', |
| '^chrome://feedback/', |
| '^chrome://.*\\.top-chrome/$', |
| '^chrome://view-cert/$', |
| '^devtools://', |
| ]; |
| if (KNOWN_FRAME_PATTERNS.some(p => targetInfo.url.match(p))) { |
| type = Type.FRAME; |
| } else { |
| const parsedURL = Common.ParsedURL.ParsedURL.fromString(targetInfo.url); |
| targetName = |
| parsedURL ? parsedURL.lastPathComponentWithFragment() : '#' + (++ChildTargetManager.lastAnonymousTargetId); |
| } |
| } |
| |
| if (targetInfo.type === 'iframe' || targetInfo.type === 'webview') { |
| type = Type.FRAME; |
| } else if (targetInfo.type === 'background_page' || targetInfo.type === 'app' || targetInfo.type === 'popup_page') { |
| type = Type.FRAME; |
| } else if (targetInfo.type === 'page') { |
| type = Type.FRAME; |
| } else if (targetInfo.type === 'browser_ui') { |
| type = Type.FRAME; |
| } else if (targetInfo.type === 'worker') { |
| type = Type.Worker; |
| } else if (targetInfo.type === 'worklet') { |
| type = Type.WORKLET; |
| } else if (targetInfo.type === 'shared_worker') { |
| type = Type.SHARED_WORKER; |
| } else if (targetInfo.type === 'shared_storage_worklet') { |
| type = Type.SHARED_STORAGE_WORKLET; |
| } else if (targetInfo.type === 'service_worker') { |
| type = Type.ServiceWorker; |
| } else if (targetInfo.type === 'auction_worklet') { |
| type = Type.AUCTION_WORKLET; |
| } else if (targetInfo.type === 'node_worker') { |
| type = Type.NODE_WORKER; |
| } |
| const target = this.#targetManager.createTarget( |
| targetInfo.targetId, targetName, type, this.#parentTarget, sessionId, undefined, undefined, targetInfo); |
| this.#childTargetsBySessionId.set(sessionId, target); |
| this.#childTargetsById.set(target.id(), target); |
| |
| if (ChildTargetManager.attachCallback) { |
| await ChildTargetManager.attachCallback({target, waitingForDebugger}); |
| } |
| |
| // [crbug/1423096] Invoking this on a worker session that is not waiting for the debugger can force the worker |
| // to resume even if there is another session waiting for the debugger. |
| if (waitingForDebugger) { |
| void target.runtimeAgent().invoke_runIfWaitingForDebugger(); |
| } |
| |
| // For top-level workers (those not attached to a frame), we need to |
| // initialize their storage context manually. The `Capability.STORAGE` is |
| // only granted in `Target.ts` to workers that are not parented by a frame, |
| // which makes this check safe. Frame-associated workers have their storage |
| // managed by ResourceTreeModel. |
| if (type !== Type.FRAME && target.hasAllCapabilities(Capability.STORAGE)) { |
| await this.initializeStorage(target); |
| } |
| } |
| |
| private async initializeStorage(target: Target): Promise<void> { |
| const storageAgent = target.storageAgent(); |
| const response = await storageAgent.invoke_getStorageKey({}); |
| |
| const storageKey = response.storageKey; |
| if (response.getError() || !storageKey) { |
| console.error(`Failed to get storage key for target ${target.id()}: ${response.getError()}`); |
| return; |
| } |
| |
| const storageKeyManager = target.model(StorageKeyManager); |
| if (storageKeyManager) { |
| storageKeyManager.setMainStorageKey(storageKey); |
| storageKeyManager.updateStorageKeys(new Set([storageKey])); |
| } |
| |
| const securityOriginManager = target.model(SecurityOriginManager); |
| if (securityOriginManager) { |
| const origin = new URL(storageKey).origin; |
| securityOriginManager.setMainSecurityOrigin(origin, ''); |
| securityOriginManager.updateSecurityOrigins(new Set([origin])); |
| } |
| } |
| |
| detachedFromTarget({sessionId}: Protocol.Target.DetachedFromTargetEvent): void { |
| const target = this.#childTargetsBySessionId.get(sessionId); |
| if (target) { |
| target.dispose('target terminated'); |
| this.#childTargetsBySessionId.delete(sessionId); |
| this.#childTargetsById.delete(target.id()); |
| } |
| } |
| |
| receivedMessageFromTarget({}: Protocol.Target.ReceivedMessageFromTargetEvent): void { |
| // We use flatten protocol. |
| } |
| |
| targetInfos(): Protocol.Target.TargetInfo[] { |
| return Array.from(this.#targetInfos.values()); |
| } |
| |
| private static lastAnonymousTargetId = 0; |
| |
| private static attachCallback?: ((arg0: { |
| target: Target, |
| waitingForDebugger: boolean, |
| }) => Promise<void>); |
| } |
| |
| export const enum Events { |
| TARGET_CREATED = 'TargetCreated', |
| TARGET_DESTROYED = 'TargetDestroyed', |
| TARGET_INFO_CHANGED = 'TargetInfoChanged', |
| } |
| |
| export interface EventTypes { |
| [Events.TARGET_CREATED]: Protocol.Target.TargetInfo; |
| [Events.TARGET_DESTROYED]: Protocol.Target.TargetID; |
| [Events.TARGET_INFO_CHANGED]: Protocol.Target.TargetInfo; |
| } |