chrome-devtools-frontend
Version:
Chrome DevTools UI
110 lines (96 loc) • 4.63 kB
text/typescript
// Copyright 2022 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 ProtocolClient from '../../core/protocol_client/protocol_client.js';
import type {ProtocolMapping} from '../../generated/protocol-mapping.js';
import type * as Protocol from '../../generated/protocol.js';
import * as puppeteer from '../../third_party/puppeteer/puppeteer.js';
/**
* This class serves as a puppeteer.Connection while sending/receiving CDP messages
* over DevTools' own SessionRouter.
*
* The only oddity is that we attached to a concrete target with a sessionId but make
* it look to puppeteer like it's the default session (no session ID).
*
* Since we see all CDPEvents, we filter out the ones whose session we don't know about.
*/
class PuppeteerConnectionAdapter extends puppeteer.Connection implements
ProtocolClient.CDPConnection.CDPConnectionObserver {
readonly #connection: ProtocolClient.CDPConnection.CDPConnection;
readonly #sessionId: Protocol.Target.SessionID;
constructor(connection: ProtocolClient.CDPConnection.CDPConnection, sessionId: Protocol.Target.SessionID) {
// url is an empty string in this case parallel to:
// https://github.com/puppeteer/puppeteer/blob/f63a123ecef86693e6457b07437a96f108f3e3c5/src/common/BrowserConnector.ts#L72
// Pass a 'null' transport, it should never actually be used, otherwise we do something wrong overwriting connection.
super('', {close: () => undefined} as puppeteer.ConnectionTransport);
this.#connection = connection;
this.#connection.observe(this);
this.#sessionId = sessionId;
}
// eslint-disable-next-line @devtools/no-underscored-properties
override _rawSend(
_callbacks: unknown, method: string|number|symbol, params: unknown, sessionId?: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_options?: unknown): Promise<any> {
return this.#connection
.send(
method as ProtocolClient.CDPConnection.Command,
params as ProtocolClient.CDPConnection.CommandParams<ProtocolClient.CDPConnection.Command>,
sessionId ?? this.#sessionId)
.then(response => 'result' in response ? response.result : {});
}
onEvent<T extends keyof ProtocolMapping.Events>(event: ProtocolClient.CDPConnection.CDPEvent<T>): void {
const {sessionId} = event;
if (sessionId === this.#sessionId) {
// Puppeteer is expecting to use the default session, but we give it a non-default session in #connection.
// Replace that sessionId with undefined so Puppeteer treats it as default.
event.sessionId = undefined;
} else if (!sessionId || !this._sessions.has(sessionId)) {
// Ignore the root session, or sessions puppeteer doesn't know about.
return;
}
void super.onMessage(JSON.stringify(event));
}
onDisconnect(): void {
this.dispose();
}
override dispose(): void {
super.dispose();
this.#connection.unobserve(this);
void this.#connection.send('Target.detachFromTarget', {sessionId: this.#sessionId}, this.#sessionId);
}
}
export class PuppeteerConnectionHelper {
static async connectPuppeteerToConnectionViaTab(options: {
connection: ProtocolClient.CDPConnection.CDPConnection,
targetId: Protocol.Target.TargetID,
sessionId: Protocol.Target.SessionID,
isPageTargetCallback: (targetInfo: Protocol.Target.TargetInfo) => boolean,
}): Promise<{
page: puppeteer.Page | null,
browser: puppeteer.Browser,
puppeteerConnection: puppeteer.Connection,
}> {
const {connection, targetId, sessionId, isPageTargetCallback} = options;
const puppeteerConnection = new PuppeteerConnectionAdapter(connection, sessionId);
const browserPromise = puppeteer.Browser._create(
puppeteerConnection,
[] /* contextIds */,
false /* ignoreHTTPSErrors */,
undefined /* defaultViewport */,
undefined /* DownloadBehavior */,
undefined /* process */,
undefined /* closeCallback */,
undefined /* targetFilterCallback */,
target => isPageTargetCallback((target as puppeteer.Target)._getTargetInfo()),
false /* waitForInitiallyDiscoveredTargets */,
);
const [, browser] = await Promise.all([
puppeteerConnection._createSession({targetId}, /* emulateAutoAttach= */ true),
browserPromise,
]);
await browser.waitForTarget(t => t.type() === 'page');
const pages = await browser.pages();
return {page: pages[0] as puppeteer.Page, browser, puppeteerConnection};
}
}