UNPKG

chrome-devtools-frontend

Version:
279 lines (244 loc) • 11.6 kB
// 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; }