| // 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, type SDKModelConstructor} 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.CDPConnection.CDPConnection|null, targetInfo?: Protocol.Target.TargetInfo) { |
| super(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) { |
| // TODO(crbug.com/406991275): This should also grant the `STORAGE` capability, but first the |
| // crashers in https://crbug.com/466134219 have to be resolved. |
| 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; |
| if (parentTarget?.type() !== Type.FRAME) { |
| this.#capabilitiesMask |= Capability.STORAGE; |
| } |
| 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; |
| if (parentTarget?.type() !== Type.FRAME) { |
| this.#capabilitiesMask |= Capability.STORAGE; |
| } |
| 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; |
| } |
| |
| /** Creates the models in the order in which they are provided */ |
| createModels(models: SDKModelConstructor[]): void { |
| this.#creatingModels = true; |
| for (const model of models) { |
| this.model(model); |
| } |
| 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; |
| } |
| |
| markAsNodeJSForTest(): void { |
| 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, |
| } |