UNPKG

chrome-devtools-frontend

Version:
326 lines (289 loc) • 11.4 kB
// Copyright 2021 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import type * as Protocol from '../../generated/protocol.js'; import * as Common from '../common/common.js'; import * as Platform from '../platform/platform.js'; import * as ProtocolClient from '../protocol_client/protocol_client.js'; import {SDKModel} from './SDKModel.js'; import type {TargetManager} from './TargetManager.js'; export class Target extends ProtocolClient.InspectorBackend.TargetBase { readonly #targetManager: TargetManager; #name: string; #inspectedURL: Platform.DevToolsPath.UrlString = Platform.DevToolsPath.EmptyUrlString; #inspectedURLName = ''; readonly #capabilitiesMask: number; #type: Type; readonly #parentTarget: Target|null; #id: Protocol.Target.TargetID|'main'; #modelByConstructor = new Map<new(arg1: Target) => SDKModel, SDKModel>(); #isSuspended: boolean; /** * Generally when a target crashes we don't need to know, with one exception. * If a target crashes during the recording of a performance trace, after the * trace when we try to resume() it, it will fail because it has crashed. This * causes the performance panel to freeze (see crbug.com/333989070). So we * mark the target as crashed so we can exit without trying to resume it. In * `ChildTargetManager` we will mark a target as "un-crashed" when we get the * `targetInfoChanged` event. This helps ensure we can deal with cases where * the page crashes, but a reload fixes it and the targets get restored (see * crbug.com/387258086). */ #hasCrashed = false; #targetInfo: Protocol.Target.TargetInfo|undefined; #creatingModels?: boolean; constructor( targetManager: TargetManager, id: Protocol.Target.TargetID|'main', name: string, type: Type, parentTarget: Target|null, sessionId: string, suspended: boolean, connection: ProtocolClient.InspectorBackend.Connection|null, targetInfo?: Protocol.Target.TargetInfo) { const needsNodeJSPatching = type === Type.NODE; super(needsNodeJSPatching, parentTarget, sessionId, connection); this.#targetManager = targetManager; this.#name = name; this.#capabilitiesMask = 0; switch (type) { case Type.FRAME: this.#capabilitiesMask = Capability.BROWSER | Capability.STORAGE | Capability.DOM | Capability.JS | Capability.LOG | Capability.NETWORK | Capability.TARGET | Capability.TRACING | Capability.EMULATION | Capability.INPUT | Capability.INSPECTOR | Capability.AUDITS | Capability.WEB_AUTHN | Capability.IO | Capability.MEDIA | Capability.EVENT_BREAKPOINTS; if (parentTarget?.type() !== Type.FRAME) { // This matches backend exposing certain capabilities only for the main frame. this.#capabilitiesMask |= Capability.DEVICE_EMULATION | Capability.SCREEN_CAPTURE | Capability.SECURITY | Capability.SERVICE_WORKER; if (Common.ParsedURL.schemeIs(targetInfo?.url as Platform.DevToolsPath.UrlString, 'chrome-extension:')) { this.#capabilitiesMask &= ~Capability.SECURITY; } // TODO(dgozman): we report service workers for the whole frame tree on the main frame, // while we should be able to only cover the subtree corresponding to the target. } break; case Type.ServiceWorker: this.#capabilitiesMask = Capability.JS | Capability.LOG | Capability.NETWORK | Capability.TARGET | Capability.INSPECTOR | Capability.IO | Capability.EVENT_BREAKPOINTS; if (parentTarget?.type() !== Type.FRAME) { this.#capabilitiesMask |= Capability.BROWSER; } break; case Type.SHARED_WORKER: this.#capabilitiesMask = Capability.JS | Capability.LOG | Capability.NETWORK | Capability.TARGET | Capability.IO | Capability.MEDIA | Capability.INSPECTOR | Capability.EVENT_BREAKPOINTS; break; case Type.SHARED_STORAGE_WORKLET: this.#capabilitiesMask = Capability.JS | Capability.LOG | Capability.INSPECTOR | Capability.EVENT_BREAKPOINTS; break; case Type.Worker: this.#capabilitiesMask = Capability.JS | Capability.LOG | Capability.NETWORK | Capability.TARGET | Capability.IO | Capability.MEDIA | Capability.EMULATION | Capability.EVENT_BREAKPOINTS; break; case Type.WORKLET: this.#capabilitiesMask = Capability.JS | Capability.LOG | Capability.EVENT_BREAKPOINTS | Capability.NETWORK; break; case Type.NODE: this.#capabilitiesMask = Capability.JS | Capability.NETWORK | Capability.TARGET | Capability.IO; break; case Type.AUCTION_WORKLET: this.#capabilitiesMask = Capability.JS | Capability.EVENT_BREAKPOINTS; break; case Type.BROWSER: this.#capabilitiesMask = Capability.TARGET | Capability.IO; break; case Type.TAB: this.#capabilitiesMask = Capability.TARGET | Capability.TRACING; break; case Type.NODE_WORKER: this.#capabilitiesMask = Capability.JS | Capability.NETWORK | Capability.TARGET | Capability.IO; } this.#type = type; this.#parentTarget = parentTarget; this.#id = id; this.#isSuspended = suspended; this.#targetInfo = targetInfo; } createModels(required: Set<new(arg1: Target) => SDKModel>): void { this.#creatingModels = true; const registeredModels = Array.from(SDKModel.registeredModels.entries()); // Create early models. for (const [modelClass, info] of registeredModels) { if (info.early) { this.model(modelClass); } } // Create autostart and required models. for (const [modelClass, info] of registeredModels) { if (info.autostart || required.has(modelClass)) { this.model(modelClass); } } this.#creatingModels = false; } id(): Protocol.Target.TargetID|'main' { return this.#id; } name(): string { return this.#name || this.#inspectedURLName; } setName(name: string): void { if (this.#name === name) { return; } this.#name = name; this.#targetManager.onNameChange(this); } type(): Type { return this.#type; } override markAsNodeJSForTest(): void { super.markAsNodeJSForTest(); this.#type = Type.NODE; } targetManager(): TargetManager { return this.#targetManager; } hasAllCapabilities(capabilitiesMask: number): boolean { // TODO(dgozman): get rid of this method, once we never observe targets with // capability mask. return (this.#capabilitiesMask & capabilitiesMask) === capabilitiesMask; } decorateLabel(label: string): string { return (this.#type === Type.Worker || this.#type === Type.ServiceWorker) ? '\u2699 ' + label : label; } parentTarget(): Target|null { return this.#parentTarget; } outermostTarget(): Target|null { let lastTarget: Target|null = null; let currentTarget: Target|null = this; do { if (currentTarget.type() !== Type.TAB && currentTarget.type() !== Type.BROWSER) { lastTarget = currentTarget; } currentTarget = currentTarget.parentTarget(); } while (currentTarget); return lastTarget; } override dispose(reason: string): void { super.dispose(reason); this.#targetManager.removeTarget(this); for (const model of this.#modelByConstructor.values()) { model.dispose(); } } model<T extends SDKModel>(modelClass: new(arg1: Target) => T): T|null { if (!this.#modelByConstructor.get(modelClass)) { const info = SDKModel.registeredModels.get(modelClass); if (info === undefined) { throw new Error('Model class is not registered'); } if ((this.#capabilitiesMask & info.capabilities) === info.capabilities) { const model = new modelClass(this); this.#modelByConstructor.set(modelClass, model); if (!this.#creatingModels) { this.#targetManager.modelAdded(modelClass, model, this.#targetManager.isInScope(this)); } } } return (this.#modelByConstructor.get(modelClass) as T) || null; } models(): Map<new(arg1: Target) => SDKModel, SDKModel> { return this.#modelByConstructor; } inspectedURL(): Platform.DevToolsPath.UrlString { return this.#inspectedURL; } setInspectedURL(inspectedURL: Platform.DevToolsPath.UrlString): void { this.#inspectedURL = inspectedURL; const parsedURL = Common.ParsedURL.ParsedURL.fromString(inspectedURL); this.#inspectedURLName = parsedURL ? parsedURL.lastPathComponentWithFragment() : '#' + this.#id; this.#targetManager.onInspectedURLChange(this); if (!this.#name) { this.#targetManager.onNameChange(this); } } hasCrashed(): boolean { return this.#hasCrashed; } setHasCrashed(isCrashed: boolean): void { const wasCrashed = this.#hasCrashed; this.#hasCrashed = isCrashed; // If the target has now been restored, check to see if it needs resuming. // This ensures that if a target crashes whilst suspended, it is resumed // when it is recovered. // If the target is not suspended, resume() is a no-op, so it's safe to call. if (wasCrashed && !isCrashed) { void this.resume(); } } async suspend(reason?: string): Promise<void> { if (this.#isSuspended) { return; } this.#isSuspended = true; // If the target has crashed, we will not attempt to suspend all the // models, but we still mark it as suspended so we correctly track the // state. if (this.#hasCrashed) { return; } await Promise.all(Array.from(this.models().values(), m => m.preSuspendModel(reason))); await Promise.all(Array.from(this.models().values(), m => m.suspendModel(reason))); } async resume(): Promise<void> { if (!this.#isSuspended) { return; } this.#isSuspended = false; if (this.#hasCrashed) { return; } await Promise.all(Array.from(this.models().values(), m => m.resumeModel())); await Promise.all(Array.from(this.models().values(), m => m.postResumeModel())); } suspended(): boolean { return this.#isSuspended; } updateTargetInfo(targetInfo: Protocol.Target.TargetInfo): void { this.#targetInfo = targetInfo; } targetInfo(): Protocol.Target.TargetInfo|undefined { return this.#targetInfo; } } export enum Type { FRAME = 'frame', // eslint-disable-next-line @typescript-eslint/naming-convention -- Used by web_tests. ServiceWorker = 'service-worker', // eslint-disable-next-line @typescript-eslint/naming-convention -- Used by web_tests. Worker = 'worker', SHARED_WORKER = 'shared-worker', SHARED_STORAGE_WORKLET = 'shared-storage-worklet', NODE = 'node', BROWSER = 'browser', AUCTION_WORKLET = 'auction-worklet', WORKLET = 'worklet', TAB = 'tab', NODE_WORKER = 'node-worker', } export const enum Capability { BROWSER = 1 << 0, DOM = 1 << 1, JS = 1 << 2, LOG = 1 << 3, NETWORK = 1 << 4, TARGET = 1 << 5, SCREEN_CAPTURE = 1 << 6, TRACING = 1 << 7, EMULATION = 1 << 8, SECURITY = 1 << 9, INPUT = 1 << 10, INSPECTOR = 1 << 11, DEVICE_EMULATION = 1 << 12, STORAGE = 1 << 13, SERVICE_WORKER = 1 << 14, AUDITS = 1 << 15, WEB_AUTHN = 1 << 16, IO = 1 << 17, MEDIA = 1 << 18, EVENT_BREAKPOINTS = 1 << 19, NONE = 0, }