chrome-devtools-frontend
Version:
Chrome DevTools UI
326 lines (289 loc) • 11.4 kB
text/typescript
// 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,
}