Skip to content

Commit 0518625

Browse files
authored
feat(launch): introduce client, server & persistent launch modes (1) (#838)
1 parent bdf8e39 commit 0518625

File tree

19 files changed

+161
-290
lines changed

19 files changed

+161
-290
lines changed

docs/api.md

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,10 @@ An example of launching a browser executable and connecting to a [Browser] later
149149
const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
150150

151151
(async () => {
152-
const browserApp = await webkit.launchBrowserApp({ webSocket: true });
153-
const connectOptions = browserApp.connectOptions();
154-
// Use connect options later to establish a connection.
155-
const browser = await webkit.connect(connectOptions);
152+
const browserApp = await webkit.launchBrowserApp();
153+
const wsEndpoint = browserApp.wsEndpoint();
154+
// Use web socket endpoint later to establish a connection.
155+
const browser = await webkit.connect({ wsEndpoint });
156156
// Close browser instance.
157157
await browserApp.close();
158158
})();
@@ -3413,7 +3413,6 @@ If the function passed to the `worker.evaluateHandle` returns a [Promise], then
34133413
<!-- GEN:toc -->
34143414
- [event: 'close'](#event-close-2)
34153415
- [browserApp.close()](#browserappclose)
3416-
- [browserApp.connectOptions()](#browserappconnectoptions)
34173416
- [browserApp.kill()](#browserappkill)
34183417
- [browserApp.process()](#browserappprocess)
34193418
- [browserApp.wsEndpoint()](#browserappwsendpoint)
@@ -3428,14 +3427,6 @@ Emitted when the browser app closes.
34283427

34293428
Closes the browser gracefully and makes sure the process is terminated.
34303429

3431-
#### browserApp.connectOptions()
3432-
- returns: <[Object]>
3433-
- `browserWSEndpoint` <?[string]> a browser websocket endpoint to connect to.
3434-
- `slowMo` <[number]>
3435-
- `transport` <[ConnectionTransport]> **Experimental** A custom transport object which should be used to connect.
3436-
3437-
This options object can be passed to [browserType.connect(options)](#browsertypeconnectoptions) to establish connection to the browser.
3438-
34393430
#### browserApp.kill()
34403431

34413432
Kills the browser process.
@@ -3444,10 +3435,9 @@ Kills the browser process.
34443435
- returns: <?[ChildProcess]> Spawned browser application process.
34453436

34463437
#### browserApp.wsEndpoint()
3447-
- returns: <?[string]> Browser websocket url.
3448-
3449-
Browser websocket endpoint which can be used as an argument to [browserType.connect(options)] to establish connection to the browser. Requires browser app to be launched with `browserType.launchBrowserApp({ webSocket: true, ... })`.
3438+
- returns: <[string]> Browser websocket url.
34503439

3440+
Browser websocket endpoint which can be used as an argument to [browserType.connect(options)](#browsertypeconnectoptions) to establish connection to the browser.
34513441

34523442
### class: BrowserType
34533443

@@ -3478,10 +3468,8 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
34783468

34793469
#### browserType.connect(options)
34803470
- `options` <[Object]>
3481-
- `browserWSEndpoint` <?[string]> A browser websocket endpoint to connect to.
3471+
- `wsEndpoint` <?[string]> A browser websocket endpoint to connect to.
34823472
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
3483-
- `browserURL` <?[string]> **Chromium-only** A browser url to connect to, in format `http://${host}:${port}`. Use interchangeably with `browserWSEndpoint` to let Playwright fetch it from [metadata endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target).
3484-
- `transport` <[ConnectionTransport]> **Experimental** Specify a custom transport object for Playwright to use.
34853473
- returns: <[Promise]<[Browser]>>
34863474

34873475
This methods attaches Playwright to an existing browser instance.
@@ -3547,7 +3535,6 @@ try {
35473535
- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields:
35483536
- `headless` <[boolean]> Whether to run browser in headless mode. More details for [Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the `devtools` option is `true`.
35493537
- `executablePath` <[string]> Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Playwright is only [guaranteed to work](https://github.com/Microsoft/playwright/#q-why-doesnt-playwright-vxxx-work-with-chromium-vyyy) with the bundled Chromium, Firefox or WebKit, use at your own risk.
3550-
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
35513538
- `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/).
35523539
- `ignoreDefaultArgs` <[boolean]|[Array]<[string]>> If `true`, then do not use [`browserType.defaultArgs()`](#browsertypedefaultargsoptions). If an array is given, then filter out the given default arguments. Dangerous option; use with care. Defaults to `false`.
35533540
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
@@ -3557,7 +3544,6 @@ try {
35573544
- `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
35583545
- `userDataDir` <[string]> Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for [Chromium](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md) and [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile).
35593546
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
3560-
- `webSocket` <[boolean]> Connects to the browser over a WebSocket instead of a pipe. Defaults to `false`.
35613547
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
35623548
- returns: <[Promise]<[Browser]>> Promise which resolves to browser instance.
35633549

@@ -3591,7 +3577,6 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'.
35913577
- `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
35923578
- `userDataDir` <[string]> Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for [Chromium](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md) and [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile).
35933579
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
3594-
- `webSocket` <[boolean]> Connects to the browser over a WebSocket instead of a pipe. Defaults to `false`.
35953580
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
35963581
- returns: <[Promise]<[BrowserApp]>> Promise which resolves to the browser app instance.
35973582

@@ -3898,7 +3883,6 @@ const { chromium } = require('playwright');
38983883
[ChromiumBrowser]: #class-chromiumbrowser "ChromiumBrowser"
38993884
[ChromiumSession]: #class-chromiumsession "ChromiumSession"
39003885
[ChromiumTarget]: #class-chromiumtarget "ChromiumTarget"
3901-
[ConnectionTransport]: ../lib/WebSocketTransport.js "ConnectionTransport"
39023886
[ConsoleMessage]: #class-consolemessage "ConsoleMessage"
39033887
[Coverage]: #class-coverage "Coverage"
39043888
[Dialog]: #class-dialog "Dialog"

src/browser.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515
*/
1616

1717
import { BrowserContext, BrowserContextOptions } from './browserContext';
18-
import { ConnectionTransport, SlowMoTransport } from './transport';
1918
import * as platform from './platform';
20-
import { assert } from './helper';
2119

2220
export interface Browser extends platform.EventEmitterType {
2321
newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
@@ -31,16 +29,5 @@ export interface Browser extends platform.EventEmitterType {
3129

3230
export type ConnectOptions = {
3331
slowMo?: number,
34-
browserWSEndpoint?: string;
35-
transport?: ConnectionTransport;
32+
wsEndpoint: string
3633
};
37-
38-
export async function createTransport(options: ConnectOptions): Promise<ConnectionTransport> {
39-
assert(Number(!!options.browserWSEndpoint) + Number(!!options.transport) === 1, 'Exactly one of browserWSEndpoint or transport must be passed to connect');
40-
let transport: ConnectionTransport | undefined;
41-
if (options.transport)
42-
transport = options.transport;
43-
else if (options.browserWSEndpoint)
44-
transport = await platform.createWebSocketTransport(options.browserWSEndpoint);
45-
return SlowMoTransport.wrap(transport!, options.slowMo);
46-
}

src/chromium/crBrowser.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ import { Page, Worker } from '../page';
2424
import { CRTarget } from './crTarget';
2525
import { Protocol } from './protocol';
2626
import { CRPage } from './crPage';
27-
import { Browser, createTransport, ConnectOptions } from '../browser';
27+
import { Browser } from '../browser';
2828
import * as network from '../network';
2929
import * as types from '../types';
3030
import * as platform from '../platform';
3131
import { readProtocolStream } from './crProtocolHelper';
32+
import { ConnectionTransport, SlowMoTransport } from '../transport';
3233

3334
export class CRBrowser extends platform.EventEmitter implements Browser {
3435
_connection: CRConnection;
@@ -41,9 +42,8 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
4142
private _tracingPath: string | null = '';
4243
private _tracingClient: CRSession | undefined;
4344

44-
static async connect(options: ConnectOptions): Promise<CRBrowser> {
45-
const transport = await createTransport(options);
46-
const connection = new CRConnection(transport);
45+
static async connect(transport: ConnectionTransport, slowMo?: number): Promise<CRBrowser> {
46+
const connection = new CRConnection(SlowMoTransport.wrap(transport, slowMo));
4747
const { browserContextIds } = await connection.rootSession.send('Target.getBrowserContexts');
4848
const browser = new CRBrowser(connection, browserContextIds);
4949
await connection.rootSession.send('Target.setDiscoverTargets', { discover: true });

src/firefox/ffBrowser.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { Browser, createTransport, ConnectOptions } from '../browser';
18+
import { Browser } from '../browser';
1919
import { BrowserContext, BrowserContextOptions } from '../browserContext';
2020
import { Events } from '../events';
2121
import { assert, helper, RegisteredListener } from '../helper';
@@ -26,6 +26,7 @@ import { ConnectionEvents, FFConnection, FFSessionEvents } from './ffConnection'
2626
import { FFPage } from './ffPage';
2727
import * as platform from '../platform';
2828
import { Protocol } from './protocol';
29+
import { ConnectionTransport, SlowMoTransport } from '../transport';
2930

3031
export class FFBrowser extends platform.EventEmitter implements Browser {
3132
_connection: FFConnection;
@@ -34,10 +35,9 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
3435
private _contexts: Map<string, BrowserContext>;
3536
private _eventListeners: RegisteredListener[];
3637

37-
static async connect(options: ConnectOptions): Promise<FFBrowser> {
38-
const transport = await createTransport(options);
39-
const connection = new FFConnection(transport);
40-
const {browserContextIds} = await connection.send('Target.getBrowserContexts');
38+
static async connect(transport: ConnectionTransport, slowMo?: number): Promise<FFBrowser> {
39+
const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo));
40+
const { browserContextIds } = await connection.send('Target.getBrowserContexts');
4141
const browser = new FFBrowser(connection, browserContextIds);
4242
await connection.send('Target.enable');
4343
await browser._waitForTarget(t => t.type() === 'page');

src/server/browserApp.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,26 @@
1515
*/
1616

1717
import { ChildProcess, execSync } from 'child_process';
18-
import { ConnectOptions } from '../browser';
1918
import * as platform from '../platform';
2019

2120
export class BrowserApp extends platform.EventEmitter {
2221
private _process: ChildProcess;
2322
private _gracefullyClose: () => Promise<void>;
24-
private _connectOptions: ConnectOptions;
23+
private _browserWSEndpoint: string | null = null;
2524

26-
constructor(process: ChildProcess, gracefullyClose: () => Promise<void>, connectOptions: ConnectOptions) {
25+
constructor(process: ChildProcess, gracefullyClose: () => Promise<void>, wsEndpoint: string | null) {
2726
super();
2827
this._process = process;
2928
this._gracefullyClose = gracefullyClose;
30-
this._connectOptions = connectOptions;
29+
this._browserWSEndpoint = wsEndpoint;
3130
}
3231

3332
process(): ChildProcess {
3433
return this._process;
3534
}
3635

3736
wsEndpoint(): string | null {
38-
return this._connectOptions.browserWSEndpoint || null;
39-
}
40-
41-
connectOptions(): ConnectOptions {
42-
return this._connectOptions;
37+
return this._browserWSEndpoint;
4338
}
4439

4540
kill() {

src/server/browserType.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,16 @@ export type LaunchOptions = BrowserArgOptions & {
3434
handleSIGHUP?: boolean,
3535
timeout?: number,
3636
dumpio?: boolean,
37-
env?: {[key: string]: string} | undefined,
38-
webSocket?: boolean,
39-
slowMo?: number, // TODO: we probably don't want this in launchBrowserApp.
37+
env?: {[key: string]: string} | undefined
4038
};
4139

4240
export interface BrowserType {
4341
executablePath(): string;
4442
name(): string;
4543
launchBrowserApp(options?: LaunchOptions): Promise<BrowserApp>;
46-
launch(options?: LaunchOptions): Promise<Browser>;
44+
launch(options?: LaunchOptions & { slowMo?: number }): Promise<Browser>;
4745
defaultArgs(options?: BrowserArgOptions): string[];
48-
connect(options: ConnectOptions & { browserURL?: string }): Promise<Browser>;
46+
connect(options: ConnectOptions): Promise<Browser>;
4947
devices: types.Devices;
5048
errors: { TimeoutError: typeof TimeoutError };
5149
}

src/server/chromium.ts

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ import { launchProcess, waitForLine } from '../server/processLauncher';
3030
import { kBrowserCloseMessageId } from '../chromium/crConnection';
3131
import { PipeTransport } from './pipeTransport';
3232
import { LaunchOptions, BrowserArgOptions, BrowserType } from './browserType';
33-
import { createTransport, ConnectOptions } from '../browser';
33+
import { ConnectOptions } from '../browser';
3434
import { BrowserApp } from './browserApp';
3535
import { Events } from '../events';
36+
import { ConnectionTransport } from '../transport';
3637

3738
export class Chromium implements BrowserType {
3839
private _projectRoot: string;
@@ -47,26 +48,29 @@ export class Chromium implements BrowserType {
4748
return 'chromium';
4849
}
4950

50-
async launch(options?: LaunchOptions): Promise<CRBrowser> {
51-
const app = await this.launchBrowserApp(options);
52-
const browser = await CRBrowser.connect(app.connectOptions());
51+
async launch(options?: LaunchOptions & { slowMo?: number }): Promise<CRBrowser> {
52+
const { browserApp, transport } = await this._launchBrowserApp(options, false);
53+
const browser = await CRBrowser.connect(transport!, options && options.slowMo);
5354
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
54-
browser.close = () => app.close();
55+
browser.close = () => browserApp.close();
56+
(browser as any)['__app__'] = browserApp;
5557
return browser;
5658
}
5759

58-
async launchBrowserApp(options: LaunchOptions = {}): Promise<BrowserApp> {
60+
async launchBrowserApp(options?: LaunchOptions): Promise<BrowserApp> {
61+
return (await this._launchBrowserApp(options, true)).browserApp;
62+
}
63+
64+
async _launchBrowserApp(options: LaunchOptions = {}, isServer: boolean): Promise<{ browserApp: BrowserApp, transport?: ConnectionTransport }> {
5965
const {
6066
ignoreDefaultArgs = false,
6167
args = [],
6268
dumpio = false,
6369
executablePath = null,
64-
webSocket = false,
6570
env = process.env,
6671
handleSIGINT = true,
6772
handleSIGTERM = true,
6873
handleSIGHUP = true,
69-
slowMo = 0,
7074
timeout = 30000
7175
} = options;
7276

@@ -81,7 +85,7 @@ export class Chromium implements BrowserType {
8185
let temporaryUserDataDir: string | null = null;
8286

8387
if (!chromeArguments.some(argument => argument.startsWith('--remote-debugging-')))
84-
chromeArguments.push(webSocket ? '--remote-debugging-port=0' : '--remote-debugging-pipe');
88+
chromeArguments.push(isServer ? '--remote-debugging-port=0' : '--remote-debugging-pipe');
8589
if (!chromeArguments.some(arg => arg.startsWith('--user-data-dir'))) {
8690
temporaryUserDataDir = await mkdtempAsync(CHROMIUM_PROFILE_PATH);
8791
chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);
@@ -96,8 +100,8 @@ export class Chromium implements BrowserType {
96100
}
97101

98102
const usePipe = chromeArguments.includes('--remote-debugging-pipe');
99-
if (usePipe && webSocket)
100-
throw new Error(`Argument "--remote-debugging-pipe" is not compatible with "webSocket" launch option.`);
103+
if (usePipe && isServer)
104+
throw new Error(`Argument "--remote-debugging-pipe" is not compatible with the launchBrowserApp.`);
101105

102106
let browserApp: BrowserApp | undefined = undefined;
103107
const { launchedProcess, gracefullyClose } = await launchProcess({
@@ -116,47 +120,33 @@ export class Chromium implements BrowserType {
116120
// We try to gracefully close to prevent crash reporting and core dumps.
117121
// Note that it's fine to reuse the pipe transport, since
118122
// our connection ignores kBrowserCloseMessageId.
119-
const transport = await createTransport(browserApp.connectOptions());
123+
const t = transport || await platform.createWebSocketTransport(browserWSEndpoint!);
120124
const message = { method: 'Browser.close', id: kBrowserCloseMessageId };
121-
transport.send(JSON.stringify(message));
125+
t.send(JSON.stringify(message));
122126
},
123127
onkill: (exitCode, signal) => {
124128
if (browserApp)
125129
browserApp.emit(Events.BrowserApp.Close, exitCode, signal);
126130
},
127131
});
128132

129-
let connectOptions: ConnectOptions;
130-
if (!usePipe) {
133+
let transport: ConnectionTransport | undefined;
134+
let browserWSEndpoint: string | null;
135+
if (isServer) {
131136
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chromium! The only Chromium revision guaranteed to work is r${this._revision}`);
132137
const match = await waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, timeout, timeoutError);
133-
const browserWSEndpoint = match[1];
134-
connectOptions = { browserWSEndpoint, slowMo };
138+
browserWSEndpoint = match[1];
135139
} else {
136-
const transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
137-
connectOptions = { slowMo, transport };
140+
transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
141+
browserWSEndpoint = null;
138142
}
139-
browserApp = new BrowserApp(launchedProcess, gracefullyClose, connectOptions);
140-
return browserApp;
143+
browserApp = new BrowserApp(launchedProcess, gracefullyClose, browserWSEndpoint);
144+
return { browserApp, transport };
141145
}
142146

143-
async connect(options: ConnectOptions & { browserURL?: string }): Promise<CRBrowser> {
144-
if (options.transport && options.transport.onmessage)
145-
throw new Error('Transport is already in use');
146-
if (options.browserURL) {
147-
assert(!options.browserWSEndpoint && !options.transport, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to connect');
148-
let connectionURL: string;
149-
try {
150-
const data = await platform.fetchUrl(new URL('/json/version', options.browserURL).href);
151-
connectionURL = JSON.parse(data).webSocketDebuggerUrl;
152-
} catch (e) {
153-
e.message = `Failed to fetch browser webSocket url from ${options.browserURL}: ` + e.message;
154-
throw e;
155-
}
156-
const transport = await platform.createWebSocketTransport(connectionURL);
157-
options = { ...options, transport };
158-
}
159-
return CRBrowser.connect(options);
147+
async connect(options: ConnectOptions): Promise<CRBrowser> {
148+
const transport = await platform.createWebSocketTransport(options.wsEndpoint);
149+
return CRBrowser.connect(transport, options.slowMo);
160150
}
161151

162152
executablePath(): string {

0 commit comments

Comments
 (0)