Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(chromium): connect to a browser over cdp #5207

Merged
merged 2 commits into from
Feb 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/src/api/class-browsertype.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,27 @@ This methods attaches Playwright to an existing browser instance.
- `timeout` <[float]> Maximum time in milliseconds to wait for the connection to be established. Defaults to
`30000` (30 seconds). Pass `0` to disable timeout.

## async method: BrowserType.connectOverCDP
* langs: js
- returns: <[Browser]>

This methods attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.

The default browser context is accessible via [`method: Browser.contexts`].

:::note
Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
:::

### param: BrowserType.connectOverCDP.params
- `params` <[Object]>
- `wsEndpoint` <[string]> A CDP websocket endpoint to connect to.
- `slowMo` <[float]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you
can see what is going on. Defaults to 0.
- `logger` <[Logger]> Logger sink for Playwright logging. Optional.
- `timeout` <[float]> Maximum time in milliseconds to wait for the connection to be established. Defaults to
`30000` (30 seconds). Pass `0` to disable timeout.

## method: BrowserType.executablePath
- returns: <[string]>

Expand Down
19 changes: 19 additions & 0 deletions src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,25 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
});
}, logger);
}

async connectOverCDP(params: ConnectOptions): Promise<Browser> {
if (this.name() !== 'chromium')
throw new Error('Connecting over CDP is only supported in Chromium.');
const logger = params.logger;
return this._wrapApiCall('browserType.connectOverCDP', async () => {
const result = await this._channel.connectOverCDP({
wsEndpoint: params.wsEndpoint,
slowMo: params.slowMo,
timeout: params.timeout
});
const browser = Browser.from(result.browser);
if (result.defaultContext)
browser._contexts.add(BrowserContext.from(result.defaultContext));
browser._isRemote = true;
browser._logger = logger;
return browser;
}, logger);
}
}

export class RemoteBrowser extends ChannelOwner<channels.RemoteBrowserChannel, channels.RemoteBrowserInitializer> {
Expand Down
8 changes: 8 additions & 0 deletions src/dispatchers/browserTypeDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,12 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
const browserContext = await this._object.launchPersistentContext(metadata, params.userDataDir, params);
return { context: new BrowserContextDispatcher(this._scope, browserContext) };
}

async connectOverCDP(params: channels.BrowserTypeConnectOverCDPParams, metadata: CallMetadata): Promise<channels.BrowserTypeConnectOverCDPResult> {
const browser = await this._object.connectOverCDP(metadata, params.wsEndpoint, params, params.timeout);
return {
browser: new BrowserDispatcher(this._scope, browser),
defaultContext: browser._defaultContext ? new BrowserContextDispatcher(this._scope, browser._defaultContext) : undefined,
};
}
}
14 changes: 14 additions & 0 deletions src/protocol/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export type BrowserTypeInitializer = {
export interface BrowserTypeChannel extends Channel {
launch(params: BrowserTypeLaunchParams, metadata?: Metadata): Promise<BrowserTypeLaunchResult>;
launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, metadata?: Metadata): Promise<BrowserTypeLaunchPersistentContextResult>;
connectOverCDP(params: BrowserTypeConnectOverCDPParams, metadata?: Metadata): Promise<BrowserTypeConnectOverCDPResult>;
}
export type BrowserTypeLaunchParams = {
executablePath?: string,
Expand Down Expand Up @@ -377,6 +378,19 @@ export type BrowserTypeLaunchPersistentContextOptions = {
export type BrowserTypeLaunchPersistentContextResult = {
context: BrowserContextChannel,
};
export type BrowserTypeConnectOverCDPParams = {
wsEndpoint: string,
slowMo?: number,
timeout?: number,
};
export type BrowserTypeConnectOverCDPOptions = {
slowMo?: number,
timeout?: number,
};
export type BrowserTypeConnectOverCDPResult = {
browser: BrowserChannel,
defaultContext?: BrowserContextChannel,
};

// ----------- Browser -----------
export type BrowserInitializer = {
Expand Down
10 changes: 9 additions & 1 deletion src/protocol/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,14 @@ BrowserType:
returns:
context: BrowserContext

connectOverCDP:
parameters:
wsEndpoint: string
slowMo: number?
timeout: number?
returns:
browser: Browser
defaultContext: BrowserContext?

Browser:
type: interface
Expand Down Expand Up @@ -1147,7 +1155,7 @@ Frame:
evaluateExpression:
parameters:
expression: string
isFunction: boolean?
isFunction: boolean?
arg: SerializedArgument
world:
type: enum?
Expand Down
5 changes: 5 additions & 0 deletions src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
path: tString,
})),
});
scheme.BrowserTypeConnectOverCDPParams = tObject({
wsEndpoint: tString,
slowMo: tOptional(tNumber),
timeout: tOptional(tNumber),
});
scheme.BrowserCloseParams = tOptional(tObject({}));
scheme.BrowserNewContextParams = tObject({
noDefaultViewport: tOptional(tBoolean),
Expand Down
5 changes: 2 additions & 3 deletions src/server/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import * as registry from '../utils/registry';
import { SdkObject } from './instrumentation';

export interface BrowserProcess {
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
onclose?: ((exitCode: number | null, signal: string | null) => void);
process?: ChildProcess;
kill(): Promise<void>;
close(): Promise<void>;
Expand All @@ -37,7 +37,7 @@ export type PlaywrightOptions = {
rootSdkObject: SdkObject,
};

export type BrowserOptions = PlaywrightOptions & {
export type BrowserOptions = PlaywrightOptions & types.UIOptions & {
name: string,
isChromium: boolean,
downloadsPath?: string,
Expand All @@ -47,7 +47,6 @@ export type BrowserOptions = PlaywrightOptions & {
proxy?: ProxySettings,
protocolLogger: types.ProtocolLogger,
browserLogsCollector: RecentLogsCollector,
slowMo?: number,
};

export abstract class Browser extends SdkObject {
Expand Down
4 changes: 4 additions & 0 deletions src/server/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ export abstract class BrowserType extends SdkObject {
return { browserProcess, downloadsPath, transport };
}

async connectOverCDP(metadata: CallMetadata, wsEndpoint: string, uiOptions: types.UIOptions, timeout?: number): Promise<Browser> {
throw new Error('CDP connections are only supported by Chromium');
}

abstract _defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[];
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<Browser>;
abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
Expand Down
37 changes: 35 additions & 2 deletions src/server/chromium/chromium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ import { Env } from '../processLauncher';
import { kBrowserCloseMessageId } from './crConnection';
import { rewriteErrorMessage } from '../../utils/stackTrace';
import { BrowserType } from '../browserType';
import { ConnectionTransport, ProtocolRequest } from '../transport';
import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../transport';
import { CRDevTools } from './crDevTools';
import { BrowserOptions, PlaywrightOptions } from '../browser';
import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
import * as types from '../types';
import { isDebugMode } from '../../utils/utils';
import { RecentLogsCollector } from '../../utils/debugLogger';
import { ProgressController } from '../progress';
import { TimeoutSettings } from '../../utils/timeoutSettings';
import { helper } from '../helper';
import { CallMetadata } from '../instrumentation';

export class Chromium extends BrowserType {
private _devtools: CRDevTools | undefined;
Expand All @@ -37,6 +42,34 @@ export class Chromium extends BrowserType {
this._devtools = this._createDevTools();
}

async connectOverCDP(metadata: CallMetadata, wsEndpoint: string, uiOptions: types.UIOptions, timeout?: number) {
const controller = new ProgressController(metadata, this);
controller.setLogName('browser');
const browserLogsCollector = new RecentLogsCollector();
return controller.run(async progress => {
const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint);
const browserProcess: BrowserProcess = {
close: async () => {
await chromeTransport.closeAndWait();
},
kill: async () => {
await chromeTransport.closeAndWait();
}
};
const browserOptions: BrowserOptions = {
...this._playwrightOptions,
...uiOptions,
name: 'chromium',
isChromium: true,
persistent: { noDefaultViewport: true },
browserProcess,
protocolLogger: helper.debugProtocolLogger(),
browserLogsCollector,
};
return await CRBrowser.connect(chromeTransport, browserOptions);
}, TimeoutSettings.timeout({timeout}));
}

private _createDevTools() {
return new CRDevTools(path.join(this._registry.browserDirectory('chromium'), 'devtools-preferences.json'));
}
Expand Down
5 changes: 3 additions & 2 deletions src/server/chromium/crBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,9 @@ export class CRBrowser extends Browser {
if (targetInfo.type === 'background_page') {
const backgroundPage = new CRPage(session, targetInfo.targetId, context, null, false);
this._backgroundPages.set(targetInfo.targetId, backgroundPage);
backgroundPage.pageOrError().then(() => {
context!.emit(CRBrowserContext.CREvents.BackgroundPage, backgroundPage._page);
backgroundPage.pageOrError().then(pageOrError => {
if (pageOrError instanceof Page)
context!.emit(CRBrowserContext.CREvents.BackgroundPage, backgroundPage._page);
});
return;
}
Expand Down
4 changes: 2 additions & 2 deletions src/server/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ export class WebSocketTransport implements ConnectionTransport {
}

async closeAndWait() {
const promise = new Promise(f => this.onclose = f);
const promise = new Promise(f => this._ws.once('close', f));
this.close();
return promise; // Make sure to await the actual disconnect.
await promise; // Make sure to await the actual disconnect.
}
}
7 changes: 5 additions & 2 deletions src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export type BrowserContextOptions = {

export type EnvArray = { name: string, value: string }[];

type LaunchOptionsBase = {
type LaunchOptionsBase = UIOptions & {
executablePath?: string,
args?: string[],
ignoreDefaultArgs?: string[],
Expand All @@ -269,7 +269,6 @@ type LaunchOptionsBase = {
proxy?: ProxySettings,
downloadsPath?: string,
chromiumSandbox?: boolean,
slowMo?: number,
};
export type LaunchOptions = LaunchOptionsBase & {
firefoxUserPrefs?: { [key: string]: string | number | boolean },
Expand Down Expand Up @@ -345,3 +344,7 @@ export type SetStorageState = {
cookies?: SetNetworkCookieParam[],
origins?: OriginStorage[]
}

export type UIOptions = {
slowMo?: number;
}
29 changes: 29 additions & 0 deletions test/chromium/chromium.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,6 +16,7 @@
*/
import { it, expect, describe } from '../fixtures';
import type { ChromiumBrowserContext } from '../..';
import http from 'http';

describe('chromium', (suite, { browserName }) => {
suite.skip(browserName !== 'chromium');
Expand Down Expand Up @@ -88,4 +90,31 @@ describe('chromium', (suite, { browserName }) => {
// make it work with Edgium.
expect(serverRequest.headers.intervention).toContain('feature/5718547946799104');
});

it('should connect to an existing cdp session 2', (test, {headful}) => {
test.skip(headful, 'Chromium currently doesn\'t support --remote-debugging-port and --remote-debugging-pipe at the same time.');
}, async ({browserType, testWorkerIndex, browserOptions, createUserDataDir }) => {
const port = 9339 + testWorkerIndex;
const browserServer = await browserType.launch({
...browserOptions,
args: ['--remote-debugging-port=' + port]
});
try {
const json = await new Promise<string>((resolve, reject) => {
http.get(`http://localhost:${port}/json/version/`, resp => {
let data = '';
resp.on('data', chunk => data += chunk);
resp.on('end', () => resolve(data));
}).on('error', reject);
});
const cdpBrowser = await browserType.connectOverCDP({
wsEndpoint: JSON.parse(json).webSocketDebuggerUrl,
});
const contexts = cdpBrowser.contexts();
expect(contexts.length).toBe(1);
await cdpBrowser.close();
} finally {
await browserServer.close();
}
});
});
33 changes: 33 additions & 0 deletions types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6262,6 +6262,39 @@ export interface BrowserType<Browser> {
*/
connect(params: ConnectOptions): Promise<Browser>;

/**
* This methods attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
*
* The default browser context is accessible via
* [browser.contexts()](https://playwright.dev/docs/api/class-browser#browsercontexts).
*
* > NOTE: Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
* @param params
*/
connectOverCDP(params: {
/**
* A CDP websocket endpoint to connect to.
*/
wsEndpoint: string;

/**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0.
*/
slowMo?: number;

/**
* Logger sink for Playwright logging. Optional.
*/
logger?: Logger;

/**
* Maximum time in milliseconds to wait for the connection to be established. Defaults to `30000` (30 seconds). Pass `0` to
* disable timeout.
*/
timeout?: number;
}): Promise<Browser>;

/**
* A path where Playwright expects to find a bundled browser executable.
*/
Expand Down