puppeteer/packages/puppeteer-core/src/cdp/Browser.ts

524 lines
14 KiB
TypeScript
Raw Normal View History

/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {ChildProcess} from 'child_process';
import type {Protocol} from 'devtools-protocol';
import {
Browser as BrowserBase,
BrowserEvent,
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
type BrowserCloseCallback,
type BrowserContextOptions,
type IsPageTargetCallback,
type Permission,
type TargetFilterCallback,
type WaitForTargetOptions,
} from '../api/Browser.js';
2023-09-13 13:47:55 +00:00
import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
import type {Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js';
import {ChromeTargetManager} from './ChromeTargetManager.js';
import type {Connection} from './Connection.js';
import {FirefoxTargetManager} from './FirefoxTargetManager.js';
import {
2023-09-13 13:47:55 +00:00
DevToolsTarget,
InitializationStatus,
OtherTarget,
PageTarget,
WorkerTarget,
type CdpTarget,
} from './Target.js';
import {TargetManagerEvent, type TargetManager} from './TargetManager.js';
/**
* @internal
*/
2023-09-13 19:57:26 +00:00
export class CdpBrowser extends BrowserBase {
2022-06-13 09:16:25 +00:00
static async _create(
product: 'firefox' | 'chrome' | undefined,
2020-05-07 10:54:55 +00:00
connection: Connection,
contextIds: string[],
ignoreHTTPSErrors: boolean,
defaultViewport?: Viewport | null,
2020-05-07 10:54:55 +00:00
process?: ChildProcess,
closeCallback?: BrowserCloseCallback,
targetFilterCallback?: TargetFilterCallback,
isPageTargetCallback?: IsPageTargetCallback,
2023-10-09 11:49:27 +00:00
waitForInitiallyDiscoveredTargets = true
2023-09-13 19:57:26 +00:00
): Promise<CdpBrowser> {
const browser = new CdpBrowser(
product,
2020-05-07 10:54:55 +00:00
connection,
contextIds,
ignoreHTTPSErrors,
defaultViewport,
process,
closeCallback,
targetFilterCallback,
isPageTargetCallback,
2023-10-09 11:49:27 +00:00
waitForInitiallyDiscoveredTargets
2020-05-07 10:54:55 +00:00
);
await browser._attach();
return browser;
}
2022-06-13 09:16:25 +00:00
#ignoreHTTPSErrors: boolean;
#defaultViewport?: Viewport | null;
#process?: ChildProcess;
#connection: Connection;
#closeCallback: BrowserCloseCallback;
#targetFilterCallback: TargetFilterCallback;
#isPageTargetCallback!: IsPageTargetCallback;
2023-09-13 19:57:26 +00:00
#defaultContext: CdpBrowserContext;
#contexts = new Map<string, CdpBrowserContext>();
#targetManager: TargetManager;
2022-06-13 09:16:25 +00:00
2020-05-07 10:54:55 +00:00
constructor(
product: 'chrome' | 'firefox' | undefined,
2020-05-07 10:54:55 +00:00
connection: Connection,
contextIds: string[],
ignoreHTTPSErrors: boolean,
defaultViewport?: Viewport | null,
2020-05-07 10:54:55 +00:00
process?: ChildProcess,
closeCallback?: BrowserCloseCallback,
targetFilterCallback?: TargetFilterCallback,
isPageTargetCallback?: IsPageTargetCallback,
2023-10-09 11:49:27 +00:00
waitForInitiallyDiscoveredTargets = true
2020-05-07 10:54:55 +00:00
) {
super();
product = product || 'chrome';
2022-06-13 09:16:25 +00:00
this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
this.#defaultViewport = defaultViewport;
this.#process = process;
this.#connection = connection;
this.#closeCallback = closeCallback || function (): void {};
this.#targetFilterCallback =
targetFilterCallback ||
((): boolean => {
return true;
});
2022-06-13 09:16:25 +00:00
this.#setIsPageTargetCallback(isPageTargetCallback);
if (product === 'firefox') {
this.#targetManager = new FirefoxTargetManager(
connection,
this.#createTarget,
this.#targetFilterCallback
);
} else {
this.#targetManager = new ChromeTargetManager(
connection,
this.#createTarget,
this.#targetFilterCallback,
2023-10-09 11:49:27 +00:00
waitForInitiallyDiscoveredTargets
);
}
2023-09-13 19:57:26 +00:00
this.#defaultContext = new CdpBrowserContext(this.#connection, this);
2022-06-14 11:55:35 +00:00
for (const contextId of contextIds) {
2022-06-13 09:16:25 +00:00
this.#contexts.set(
2020-05-07 10:54:55 +00:00
contextId,
2023-09-13 19:57:26 +00:00
new CdpBrowserContext(this.#connection, this, contextId)
2020-05-07 10:54:55 +00:00
);
2022-06-14 11:55:35 +00:00
}
}
2020-05-07 10:54:55 +00:00
#emitDisconnected = () => {
2023-09-13 13:47:55 +00:00
this.emit(BrowserEvent.Disconnected, undefined);
};
async _attach(): Promise<void> {
2023-09-13 13:47:55 +00:00
this.#connection.on(CDPSessionEvent.Disconnected, this.#emitDisconnected);
this.#targetManager.on(
2023-09-13 13:47:55 +00:00
TargetManagerEvent.TargetAvailable,
this.#onAttachedToTarget
);
this.#targetManager.on(
2023-09-13 13:47:55 +00:00
TargetManagerEvent.TargetGone,
this.#onDetachedFromTarget
);
this.#targetManager.on(
2023-09-13 13:47:55 +00:00
TargetManagerEvent.TargetChanged,
this.#onTargetChanged
);
this.#targetManager.on(
2023-09-13 13:47:55 +00:00
TargetManagerEvent.TargetDiscovered,
this.#onTargetDiscovered
);
await this.#targetManager.initialize();
}
_detach(): void {
2023-09-13 13:47:55 +00:00
this.#connection.off(CDPSessionEvent.Disconnected, this.#emitDisconnected);
this.#targetManager.off(
2023-09-13 13:47:55 +00:00
TargetManagerEvent.TargetAvailable,
this.#onAttachedToTarget
);
this.#targetManager.off(
2023-09-13 13:47:55 +00:00
TargetManagerEvent.TargetGone,
this.#onDetachedFromTarget
);
this.#targetManager.off(
2023-09-13 13:47:55 +00:00
TargetManagerEvent.TargetChanged,
this.#onTargetChanged
);
this.#targetManager.off(
2023-09-13 13:47:55 +00:00
TargetManagerEvent.TargetDiscovered,
this.#onTargetDiscovered
2020-05-07 10:54:55 +00:00
);
}
override process(): ChildProcess | null {
2022-06-13 09:16:25 +00:00
return this.#process ?? null;
2020-05-07 10:54:55 +00:00
}
_targetManager(): TargetManager {
return this.#targetManager;
}
2022-06-13 09:16:25 +00:00
#setIsPageTargetCallback(isPageTargetCallback?: IsPageTargetCallback): void {
this.#isPageTargetCallback =
isPageTargetCallback ||
((target: Target): boolean => {
return (
target.type() === 'page' ||
target.type() === 'background_page' ||
target.type() === 'webview'
);
});
}
_getIsPageTargetCallback(): IsPageTargetCallback | undefined {
2022-06-13 09:16:25 +00:00
return this.#isPageTargetCallback;
}
override async createIncognitoBrowserContext(
options: BrowserContextOptions = {}
2023-09-13 19:57:26 +00:00
): Promise<CdpBrowserContext> {
const {proxyServer, proxyBypassList} = options;
const {browserContextId} = await this.#connection.send(
'Target.createBrowserContext',
{
proxyServer,
proxyBypassList: proxyBypassList && proxyBypassList.join(','),
}
2020-05-07 10:54:55 +00:00
);
2023-09-13 19:57:26 +00:00
const context = new CdpBrowserContext(
2022-06-13 09:16:25 +00:00
this.#connection,
2020-05-07 10:54:55 +00:00
this,
browserContextId
);
2022-06-13 09:16:25 +00:00
this.#contexts.set(browserContextId, context);
2020-05-07 10:54:55 +00:00
return context;
}
2023-09-13 19:57:26 +00:00
override browserContexts(): CdpBrowserContext[] {
2022-06-13 09:16:25 +00:00
return [this.#defaultContext, ...Array.from(this.#contexts.values())];
2020-05-07 10:54:55 +00:00
}
2023-09-13 19:57:26 +00:00
override defaultBrowserContext(): CdpBrowserContext {
2022-06-13 09:16:25 +00:00
return this.#defaultContext;
2020-05-07 10:54:55 +00:00
}
async _disposeContext(contextId?: string): Promise<void> {
if (!contextId) {
return;
}
2022-06-13 09:16:25 +00:00
await this.#connection.send('Target.disposeBrowserContext', {
browserContextId: contextId,
2020-05-07 10:54:55 +00:00
});
2022-06-13 09:16:25 +00:00
this.#contexts.delete(contextId);
2020-05-07 10:54:55 +00:00
}
#createTarget = (
targetInfo: Protocol.Target.TargetInfo,
session?: CDPSession
) => {
const {browserContextId} = targetInfo;
2020-05-07 10:54:55 +00:00
const context =
2022-06-13 09:16:25 +00:00
browserContextId && this.#contexts.has(browserContextId)
? this.#contexts.get(browserContextId)
: this.#defaultContext;
2020-05-07 10:54:55 +00:00
if (!context) {
throw new Error('Missing browser context');
}
const createSession = (isAutoAttachEmulated: boolean) => {
return this.#connection._createSession(targetInfo, isAutoAttachEmulated);
};
const otherTarget = new OtherTarget(
targetInfo,
session,
context,
this.#targetManager,
createSession
);
if (targetInfo.url?.startsWith('devtools://')) {
return new DevToolsTarget(
targetInfo,
session,
context,
this.#targetManager,
createSession,
this.#ignoreHTTPSErrors,
this.#defaultViewport ?? null
);
}
if (this.#isPageTargetCallback(otherTarget)) {
return new PageTarget(
targetInfo,
session,
context,
this.#targetManager,
createSession,
this.#ignoreHTTPSErrors,
this.#defaultViewport ?? null
);
}
if (
targetInfo.type === 'service_worker' ||
targetInfo.type === 'shared_worker'
) {
return new WorkerTarget(
targetInfo,
session,
context,
this.#targetManager,
createSession
);
}
return otherTarget;
};
2020-05-07 10:54:55 +00:00
2023-09-13 19:57:26 +00:00
#onAttachedToTarget = async (target: CdpTarget) => {
if (
target._isTargetExposed() &&
(await target._initializedDeferred.valueOrThrow()) ===
InitializationStatus.SUCCESS
) {
2023-09-13 13:47:55 +00:00
this.emit(BrowserEvent.TargetCreated, target);
target.browserContext().emit(BrowserContextEvent.TargetCreated, target);
2020-05-07 10:54:55 +00:00
}
};
2020-05-07 10:54:55 +00:00
2023-09-13 19:57:26 +00:00
#onDetachedFromTarget = async (target: CdpTarget): Promise<void> => {
target._initializedDeferred.resolve(InitializationStatus.ABORTED);
target._isClosedDeferred.resolve();
if (
target._isTargetExposed() &&
(await target._initializedDeferred.valueOrThrow()) ===
InitializationStatus.SUCCESS
) {
2023-09-13 13:47:55 +00:00
this.emit(BrowserEvent.TargetDestroyed, target);
target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target);
2020-05-07 10:54:55 +00:00
}
};
2023-09-13 19:57:26 +00:00
#onTargetChanged = ({target}: {target: CdpTarget}): void => {
2023-09-13 13:47:55 +00:00
this.emit(BrowserEvent.TargetChanged, target);
target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
};
#onTargetDiscovered = (targetInfo: Protocol.Target.TargetInfo): void => {
2023-09-13 13:47:55 +00:00
this.emit(BrowserEvent.TargetDiscovered, targetInfo);
};
2020-05-07 10:54:55 +00:00
override wsEndpoint(): string {
2022-06-13 09:16:25 +00:00
return this.#connection.url();
2020-05-07 10:54:55 +00:00
}
override async newPage(): Promise<Page> {
2023-09-01 07:49:33 +00:00
return await this.#defaultContext.newPage();
2020-05-07 10:54:55 +00:00
}
async _createPageInContext(contextId?: string): Promise<Page> {
const {targetId} = await this.#connection.send('Target.createTarget', {
2020-05-07 10:54:55 +00:00
url: 'about:blank',
browserContextId: contextId || undefined,
});
2023-08-28 06:20:57 +00:00
const target = (await this.waitForTarget(t => {
2023-09-13 19:57:26 +00:00
return (t as CdpTarget)._targetId === targetId;
})) as CdpTarget;
if (!target) {
throw new Error(`Missing target for page (id = ${targetId})`);
}
const initialized =
(await target._initializedDeferred.valueOrThrow()) ===
InitializationStatus.SUCCESS;
if (!initialized) {
throw new Error(`Failed to create target for page (id = ${targetId})`);
}
2020-05-07 10:54:55 +00:00
const page = await target.page();
if (!page) {
throw new Error(
`Failed to create a page for context (id = ${contextId})`
);
}
2020-05-07 10:54:55 +00:00
return page;
}
2023-09-13 19:57:26 +00:00
override targets(): CdpTarget[] {
return Array.from(
this.#targetManager.getAvailableTargets().values()
).filter(target => {
return (
target._isTargetExposed() &&
target._initializedDeferred.value() === InitializationStatus.SUCCESS
);
});
2020-05-07 10:54:55 +00:00
}
2023-09-13 19:57:26 +00:00
override target(): CdpTarget {
const browserTarget = this.targets().find(target => {
return target.type() === 'browser';
});
if (!browserTarget) {
throw new Error('Browser target is not found');
}
return browserTarget;
2020-05-07 10:54:55 +00:00
}
override async version(): Promise<string> {
2022-06-13 09:16:25 +00:00
const version = await this.#getVersion();
2020-05-07 10:54:55 +00:00
return version.product;
}
override async userAgent(): Promise<string> {
2022-06-13 09:16:25 +00:00
const version = await this.#getVersion();
2020-05-07 10:54:55 +00:00
return version.userAgent;
}
override async close(): Promise<void> {
2022-06-13 09:16:25 +00:00
await this.#closeCallback.call(null);
2020-05-07 10:54:55 +00:00
this.disconnect();
}
override disconnect(): void {
this.#targetManager.dispose();
2022-06-13 09:16:25 +00:00
this.#connection.dispose();
this._detach();
2020-05-07 10:54:55 +00:00
}
override get connected(): boolean {
2022-06-13 09:16:25 +00:00
return !this.#connection._closed;
2020-05-07 10:54:55 +00:00
}
2022-06-13 09:16:25 +00:00
#getVersion(): Promise<Protocol.Browser.GetVersionResponse> {
return this.#connection.send('Browser.getVersion');
2020-05-07 10:54:55 +00:00
}
}
/**
* @internal
*/
2023-09-13 19:57:26 +00:00
export class CdpBrowserContext extends BrowserContext {
2022-06-13 09:16:25 +00:00
#connection: Connection;
2023-09-13 19:57:26 +00:00
#browser: CdpBrowser;
2022-06-13 09:16:25 +00:00
#id?: string;
2023-09-13 19:57:26 +00:00
constructor(connection: Connection, browser: CdpBrowser, contextId?: string) {
super();
2022-06-13 09:16:25 +00:00
this.#connection = connection;
this.#browser = browser;
this.#id = contextId;
}
override get id(): string | undefined {
return this.#id;
}
2023-09-13 19:57:26 +00:00
override targets(): CdpTarget[] {
return this.#browser.targets().filter(target => {
return target.browserContext() === this;
});
}
override waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options: WaitForTargetOptions = {}
): Promise<Target> {
return this.#browser.waitForTarget(target => {
return target.browserContext() === this && predicate(target);
}, options);
}
override async pages(): Promise<Page[]> {
const pages = await Promise.all(
2020-05-07 10:54:55 +00:00
this.targets()
.filter(target => {
return (
target.type() === 'page' ||
(target.type() === 'other' &&
this.#browser._getIsPageTargetCallback()?.(target))
);
})
.map(target => {
return target.page();
})
);
return pages.filter((page): page is Page => {
return !!page;
});
}
override isIncognito(): boolean {
2022-06-13 09:16:25 +00:00
return !!this.#id;
}
override async overridePermissions(
2020-05-07 10:54:55 +00:00
origin: string,
permissions: Permission[]
2020-05-07 10:54:55 +00:00
): Promise<void> {
const protocolPermissions = permissions.map(permission => {
const protocolPermission =
WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission);
2022-06-14 11:55:35 +00:00
if (!protocolPermission) {
throw new Error('Unknown permission: ' + permission);
2022-06-14 11:55:35 +00:00
}
return protocolPermission;
});
2022-06-13 09:16:25 +00:00
await this.#connection.send('Browser.grantPermissions', {
2020-05-07 10:54:55 +00:00
origin,
2022-06-13 09:16:25 +00:00
browserContextId: this.#id || undefined,
permissions: protocolPermissions,
2020-05-07 10:54:55 +00:00
});
}
override async clearPermissionOverrides(): Promise<void> {
2022-06-13 09:16:25 +00:00
await this.#connection.send('Browser.resetPermissions', {
browserContextId: this.#id || undefined,
2020-05-07 10:54:55 +00:00
});
}
override newPage(): Promise<Page> {
2022-06-13 09:16:25 +00:00
return this.#browser._createPageInContext(this.#id);
}
2023-09-13 19:57:26 +00:00
override browser(): CdpBrowser {
2022-06-13 09:16:25 +00:00
return this.#browser;
}
override async close(): Promise<void> {
2022-06-13 09:16:25 +00:00
assert(this.#id, 'Non-incognito profiles cannot be closed!');
await this.#browser._disposeContext(this.#id);
}
}