chrome-devtools-frontend
Version:
Chrome DevTools UI
286 lines (255 loc) • 9.91 kB
text/typescript
// Copyright 2021 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 * as Platform from '../platform/platform.js';
import * as ProtocolClient from '../protocol_client/protocol_client.js';
import type * as Protocol from '../../generated/protocol.js';
import {type TargetManager} from './TargetManager.js';
import {SDKModel} from './SDKModel.js';
export class Target extends ProtocolClient.InspectorBackend.TargetBase {
readonly #targetManagerInternal: TargetManager;
#nameInternal: string;
#inspectedURLInternal: Platform.DevToolsPath.UrlString;
#inspectedURLName: string;
readonly #capabilitiesMask: number;
#typeInternal: Type;
readonly #parentTargetInternal: Target|null;
#idInternal: Protocol.Target.TargetID|'main';
#modelByConstructor: Map<new(arg1: Target) => SDKModel, SDKModel>;
#isSuspended: boolean;
#targetInfoInternal: 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.#targetManagerInternal = targetManager;
this.#nameInternal = name;
this.#inspectedURLInternal = Platform.DevToolsPath.EmptyUrlString;
this.#inspectedURLName = '';
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.WebAuthn | Capability.IO |
Capability.Media;
if (parentTarget?.type() !== Type.Frame) {
// This matches backend exposing certain capabilities only for the main frame.
this.#capabilitiesMask |=
Capability.DeviceEmulation | Capability.ScreenCapture | Capability.Security | Capability.ServiceWorker;
if (targetInfo?.url.startsWith('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;
if (parentTarget?.type() !== Type.Frame) {
this.#capabilitiesMask |= Capability.Browser;
}
break;
case Type.SharedWorker:
this.#capabilitiesMask = Capability.JS | Capability.Log | Capability.Network | Capability.Target |
Capability.IO | Capability.Media | Capability.Inspector;
break;
case Type.Worker:
this.#capabilitiesMask = Capability.JS | Capability.Log | Capability.Network | Capability.Target |
Capability.IO | Capability.Media | Capability.Emulation;
break;
case Type.Node:
this.#capabilitiesMask = Capability.JS;
break;
case Type.AuctionWorklet:
this.#capabilitiesMask = Capability.JS | Capability.EventBreakpoints;
break;
case Type.Browser:
this.#capabilitiesMask = Capability.Target | Capability.IO;
break;
case Type.Tab:
this.#capabilitiesMask = Capability.Target;
break;
}
this.#typeInternal = type;
this.#parentTargetInternal = parentTarget;
this.#idInternal = id;
/* } */
this.#modelByConstructor = new Map();
this.#isSuspended = suspended;
this.#targetInfoInternal = 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.#idInternal;
}
name(): string {
return this.#nameInternal || this.#inspectedURLName;
}
setName(name: string): void {
if (this.#nameInternal === name) {
return;
}
this.#nameInternal = name;
this.#targetManagerInternal.onNameChange(this);
}
type(): Type {
return this.#typeInternal;
}
override markAsNodeJSForTest(): void {
super.markAsNodeJSForTest();
this.#typeInternal = Type.Node;
}
targetManager(): TargetManager {
return this.#targetManagerInternal;
}
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.#typeInternal === Type.Worker || this.#typeInternal === Type.ServiceWorker) ? '\u2699 ' + label :
label;
}
parentTarget(): Target|null {
return this.#parentTargetInternal;
}
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.#targetManagerInternal.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 'Model class is not registered @' + new Error().stack;
}
if ((this.#capabilitiesMask & info.capabilities) === info.capabilities) {
const model = new modelClass(this);
this.#modelByConstructor.set(modelClass, model);
if (!this.#creatingModels) {
this.#targetManagerInternal.modelAdded(this, modelClass, model, this.#targetManagerInternal.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.#inspectedURLInternal;
}
setInspectedURL(inspectedURL: Platform.DevToolsPath.UrlString): void {
this.#inspectedURLInternal = inspectedURL;
const parsedURL = Common.ParsedURL.ParsedURL.fromString(inspectedURL);
this.#inspectedURLName = parsedURL ? parsedURL.lastPathComponentWithFragment() : '#' + this.#idInternal;
if (this.parentTarget()?.type() !== Type.Frame) {
Host.InspectorFrontendHost.InspectorFrontendHostInstance.inspectedURLChanged(
inspectedURL || Platform.DevToolsPath.EmptyUrlString);
}
this.#targetManagerInternal.onInspectedURLChange(this);
if (!this.#nameInternal) {
this.#targetManagerInternal.onNameChange(this);
}
}
async suspend(reason?: string): Promise<void> {
if (this.#isSuspended) {
return;
}
this.#isSuspended = true;
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;
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.#targetInfoInternal = targetInfo;
}
targetInfo(): Protocol.Target.TargetInfo|undefined {
return this.#targetInfoInternal;
}
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Type {
Frame = 'frame',
ServiceWorker = 'service-worker',
Worker = 'worker',
SharedWorker = 'shared-worker',
Node = 'node',
Browser = 'browser',
AuctionWorklet = 'auction-worklet',
Tab = 'tab',
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Capability {
Browser = 1 << 0,
DOM = 1 << 1,
JS = 1 << 2,
Log = 1 << 3,
Network = 1 << 4,
Target = 1 << 5,
ScreenCapture = 1 << 6,
Tracing = 1 << 7,
Emulation = 1 << 8,
Security = 1 << 9,
Input = 1 << 10,
Inspector = 1 << 11,
DeviceEmulation = 1 << 12,
Storage = 1 << 13,
ServiceWorker = 1 << 14,
Audits = 1 << 15,
WebAuthn = 1 << 16,
IO = 1 << 17,
Media = 1 << 18,
EventBreakpoints = 1 << 19,
None = 0,
}