UNPKG

chrome-devtools-frontend

Version:
249 lines (218 loc) • 10.7 kB
// Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../common/common.js'; import * as Host from '../host/host.js'; import type * as ProtocolClient from '../protocol_client/protocol_client.js'; import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; import type * as Protocol from '../../generated/protocol.js'; import {ParallelConnection} from './Connections.js'; import {Capability, Type, type Target} from './Target.js'; import {SDKModel} from './SDKModel.js'; import {Events as TargetManagerEvents, TargetManager} from './TargetManager.js'; import {PrimaryPageChangeType, ResourceTreeModel} from './ResourceTreeModel.js'; export class ChildTargetManager extends SDKModel<EventTypes> implements ProtocolProxyApi.TargetDispatcher { readonly #targetManager: TargetManager; #parentTarget: Target; readonly #targetAgent: ProtocolProxyApi.TargetApi; readonly #targetInfosInternal: Map<Protocol.Target.TargetID, Protocol.Target.TargetInfo> = new Map(); readonly #childTargetsBySessionId: Map<Protocol.Target.SessionID, Target> = new Map(); readonly #childTargetsById: Map<Protocol.Target.TargetID|'main', Target> = new Map(); readonly #parallelConnections: Map<string, ProtocolClient.InspectorBackend.Connection> = new Map(); #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 { 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.#targetInfosInternal.set(targetInfo.targetId, targetInfo); this.fireAvailableTargetsChanged(); this.dispatchEventToListeners(Events.TargetCreated, targetInfo); } targetInfoChanged({targetInfo}: Protocol.Target.TargetInfoChangedEvent): void { this.#targetInfosInternal.set(targetInfo.targetId, targetInfo); const target = this.#childTargetsById.get(targetInfo.targetId); if (target) { if (target.targetInfo()?.subtype === 'prerender' && !targetInfo.subtype) { const resourceTreeModel = target.model(ResourceTreeModel); target.updateTargetInfo(targetInfo); if (resourceTreeModel && resourceTreeModel.mainFrame) { resourceTreeModel.primaryPageChanged(resourceTreeModel.mainFrame, PrimaryPageChangeType.Activation); } } else { target.updateTargetInfo(targetInfo); } } this.fireAvailableTargetsChanged(); this.dispatchEventToListeners(Events.TargetInfoChanged, targetInfo); } targetDestroyed({targetId}: Protocol.Target.TargetDestroyedEvent): void { this.#targetInfosInternal.delete(targetId); this.fireAvailableTargetsChanged(); this.dispatchEventToListeners(Events.TargetDestroyed, targetId); } // eslint-disable-next-line @typescript-eslint/no-unused-vars targetCrashed({targetId, status, errorCode}: Protocol.Target.TargetCrashedEvent): void { } private fireAvailableTargetsChanged(): void { TargetManager.instance().dispatchEventToListeners( TargetManagerEvents.AvailableTargetsChanged, [...this.#targetInfosInternal.values()]); } async getParentTargetId(): Promise<Protocol.Target.TargetID> { if (!this.#parentTargetId) { this.#parentTargetId = (await this.#parentTarget.targetAgent().invoke_getTargetInfo({})).targetInfo.targetId; } return this.#parentTargetId; } 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)) { if (targetInfo.url === 'chrome://print/' || (targetInfo.url.startsWith('chrome://') && targetInfo.url.endsWith('.top-chrome/'))) { type = Type.Frame; } else { const parsedURL = Common.ParsedURL.ParsedURL.fromString(targetInfo.url); targetName = parsedURL ? parsedURL.lastPathComponentWithFragment() : '#' + (++ChildTargetManager.lastAnonymousTargetId); if (parsedURL?.scheme === 'devtools' && targetInfo.type === 'other') { type = Type.Frame; } } } 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; } // TODO(lfg): ensure proper capabilities for child pages (e.g. portals). else if (targetInfo.type === 'page') { type = Type.Frame; } else if (targetInfo.type === 'worker') { type = Type.Worker; } else if (targetInfo.type === 'shared_worker') { type = Type.SharedWorker; } else if (targetInfo.type === 'service_worker') { type = Type.ServiceWorker; } else if (targetInfo.type === 'auction_worklet') { type = Type.AuctionWorklet; } 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(); } } detachedFromTarget({sessionId}: Protocol.Target.DetachedFromTargetEvent): void { if (this.#parallelConnections.has(sessionId)) { this.#parallelConnections.delete(sessionId); } else { 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. } async createParallelConnection(onMessage: (arg0: (Object|string)) => void): Promise<{connection: ProtocolClient.InspectorBackend.Connection, sessionId: string}> { // The main Target id is actually just `main`, instead of the real targetId. // Get the real id (requires an async operation) so that it can be used synchronously later. const targetId = await this.getParentTargetId(); const {connection, sessionId} = await this.createParallelConnectionAndSessionForTarget(this.#parentTarget, targetId); connection.setOnMessage(onMessage); this.#parallelConnections.set(sessionId, connection); return {connection, sessionId}; } private async createParallelConnectionAndSessionForTarget(target: Target, targetId: Protocol.Target.TargetID): Promise<{ connection: ProtocolClient.InspectorBackend.Connection, sessionId: string, }> { const targetAgent = target.targetAgent(); const targetRouter = (target.router() as ProtocolClient.InspectorBackend.SessionRouter); const sessionId = (await targetAgent.invoke_attachToTarget({targetId, flatten: true})).sessionId; const connection = new ParallelConnection(targetRouter.connection(), sessionId); targetRouter.registerSession(target, sessionId, connection); connection.setOnDisconnect(() => { targetRouter.unregisterSession(sessionId); void targetAgent.invoke_detachFromTarget({sessionId}); }); return {connection, sessionId}; } targetInfos(): Protocol.Target.TargetInfo[] { return Array.from(this.#targetInfosInternal.values()); } private static lastAnonymousTargetId = 0; private static attachCallback?: ((arg0: { target: Target, waitingForDebugger: boolean, }) => Promise<void>); } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Events { TargetCreated = 'TargetCreated', TargetDestroyed = 'TargetDestroyed', TargetInfoChanged = 'TargetInfoChanged', } export type EventTypes = { [Events.TargetCreated]: Protocol.Target.TargetInfo, [Events.TargetDestroyed]: Protocol.Target.TargetID, [Events.TargetInfoChanged]: Protocol.Target.TargetInfo, };