Skip to content

Commit 2ede4bc

Browse files
authored
chore: further unify launching and connection (#2320)
1 parent 9154f4b commit 2ede4bc

File tree

12 files changed

+190
-162
lines changed

12 files changed

+190
-162
lines changed

docs/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3976,6 +3976,7 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
39763976
- `wsEndpoint` <[string]> A browser websocket endpoint to connect to. **required**
39773977
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0.
39783978
- `logger` <[Logger]> Logger sink for Playwright logging.
3979+
- `timeout` <[number]> Maximum time in milliseconds to wait for the connection to be established. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
39793980
- returns: <[Promise]<[Browser]>>
39803981

39813982
This methods attaches Playwright to an existing browser instance.

src/helper.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import * as fs from 'fs';
2121
import * as util from 'util';
2222
import { TimeoutError } from './errors';
2323
import * as types from './types';
24+
import { ChildProcess, execSync } from 'child_process';
2425

2526
// NOTE: update this to point to playwright/lib when moving this file.
2627
const PLAYWRIGHT_LIB_PATH = __dirname;
@@ -332,6 +333,19 @@ class Helper {
332333
static optionsWithUpdatedTimeout<T extends types.TimeoutOptions>(options: T | undefined, deadline: number): T {
333334
return { ...(options || {}) as T, timeout: this.timeUntilDeadline(deadline) };
334335
}
336+
337+
static killProcess(proc: ChildProcess) {
338+
if (proc.pid && !proc.killed) {
339+
try {
340+
if (process.platform === 'win32')
341+
execSync(`taskkill /pid ${proc.pid} /T /F`);
342+
else
343+
process.kill(-proc.pid, 'SIGKILL');
344+
} catch (e) {
345+
// the process might have already stopped
346+
}
347+
}
348+
}
335349
}
336350

337351
export function assert(value: any, message?: string): asserts value {

src/logger.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,16 @@ export class RootLogger implements InnerLogger {
6262
this._logger.add(`launch`, new RecordingLogger('browser'));
6363
}
6464

65-
stopLaunchRecording(): string {
66-
const logger = this._logger.remove(`launch`) as RecordingLogger;
65+
launchRecording(): string {
66+
const logger = this._logger.get(`launch`) as RecordingLogger;
6767
if (logger)
6868
return logger.recording();
6969
return '';
7070
}
71+
72+
stopLaunchRecording() {
73+
this._logger.remove(`launch`);
74+
}
7175
}
7276

7377
const colorMap = new Map<string, number>([
@@ -87,10 +91,12 @@ class MultiplexingLogger implements Logger {
8791
this._loggers.set(id, logger);
8892
}
8993

90-
remove(id: string): Logger | undefined {
91-
const logger = this._loggers.get(id);
94+
get(id: string): Logger | undefined {
95+
return this._loggers.get(id);
96+
}
97+
98+
remove(id: string) {
9299
this._loggers.delete(id);
93-
return logger;
94100
}
95101

96102
isEnabled(name: string, severity: LoggerSeverity): boolean {

src/server/browserServer.ts

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { ChildProcess, execSync } from 'child_process';
17+
import { ChildProcess } from 'child_process';
1818
import { EventEmitter } from 'events';
1919
import { helper } from '../helper';
2020
import { RootLogger } from '../logger';
21-
import { TimeoutSettings } from '../timeoutSettings';
22-
import { LaunchOptions } from './browserType';
21+
import { LaunchOptions, processBrowserArgOptions } from './browserType';
22+
import { ConnectionTransport } from '../transport';
2323

2424
export class WebSocketWrapper {
2525
readonly wsEndpoint: string;
@@ -51,82 +51,52 @@ export class WebSocketWrapper {
5151
}
5252

5353
export class BrowserServer extends EventEmitter {
54-
private _process: ChildProcess | undefined;
55-
private _gracefullyClose: (() => Promise<void>) | undefined;
54+
private _process: ChildProcess;
55+
private _gracefullyClose: (() => Promise<void>);
5656
private _webSocketWrapper: WebSocketWrapper | null = null;
5757
readonly _launchOptions: LaunchOptions;
5858
readonly _logger: RootLogger;
59-
readonly _launchDeadline: number;
59+
readonly _downloadsPath: string;
60+
readonly _transport: ConnectionTransport;
61+
readonly _headful: boolean;
6062

61-
constructor(options: LaunchOptions) {
63+
constructor(options: LaunchOptions, process: ChildProcess, gracefullyClose: () => Promise<void>, transport: ConnectionTransport, downloadsPath: string, webSocketWrapper: WebSocketWrapper | null) {
6264
super();
6365
this._launchOptions = options;
66+
this._headful = !processBrowserArgOptions(options).headless;
6467
this._logger = new RootLogger(options.logger);
65-
this._launchDeadline = TimeoutSettings.computeDeadline(typeof options.timeout === 'number' ? options.timeout : 30000);
66-
}
6768

68-
_initialize(process: ChildProcess, gracefullyClose: () => Promise<void>, webSocketWrapper: WebSocketWrapper | null) {
6969
this._process = process;
7070
this._gracefullyClose = gracefullyClose;
71+
this._transport = transport;
72+
this._downloadsPath = downloadsPath;
7173
this._webSocketWrapper = webSocketWrapper;
7274
}
7375

74-
_isInitialized(): boolean {
75-
return !!this._process;
76-
}
77-
7876
process(): ChildProcess {
79-
return this._process!;
77+
return this._process;
8078
}
8179

8280
wsEndpoint(): string {
8381
return this._webSocketWrapper ? this._webSocketWrapper.wsEndpoint : '';
8482
}
8583

8684
kill() {
87-
if (this._process!.pid && !this._process!.killed) {
88-
try {
89-
if (process.platform === 'win32')
90-
execSync(`taskkill /pid ${this._process!.pid} /T /F`);
91-
else
92-
process.kill(-this._process!.pid, 'SIGKILL');
93-
} catch (e) {
94-
// the process might have already stopped
95-
}
96-
}
85+
helper.killProcess(this._process);
9786
}
9887

9988
async close(): Promise<void> {
100-
await this._gracefullyClose!();
89+
await this._gracefullyClose();
10190
}
10291

10392
async _checkLeaks(): Promise<void> {
10493
if (this._webSocketWrapper)
10594
await this._webSocketWrapper.checkLeaks();
10695
}
10796

108-
async _initializeOrClose<T>(init: () => Promise<T>): Promise<T> {
109-
try {
110-
let promise: Promise<T>;
111-
if ((this._launchOptions as any).__testHookBeforeCreateBrowser)
112-
promise = (this._launchOptions as any).__testHookBeforeCreateBrowser().then(init);
113-
else
114-
promise = init();
115-
const result = await helper.waitWithDeadline(promise, 'the browser to launch', this._launchDeadline, 'pw:browser*');
116-
this._logger.stopLaunchRecording();
117-
return result;
118-
} catch (e) {
119-
e.message += '\n=============== Process output during launch: ===============\n' +
120-
this._logger.stopLaunchRecording() +
121-
'\n=============================================================';
122-
await this._closeOrKill();
123-
throw e;
124-
}
125-
}
126-
127-
private async _closeOrKill(): Promise<void> {
97+
async _closeOrKill(deadline: number): Promise<void> {
12898
try {
129-
await helper.waitWithDeadline(this.close(), '', this._launchDeadline, ''); // The error message is ignored.
99+
await helper.waitWithDeadline(this.close(), '', deadline, ''); // The error message is ignored.
130100
} catch (ignored) {
131101
this.kill();
132102
}

src/server/browserType.ts

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import * as browserPaths from '../install/browserPaths';
2020
import { Logger, RootLogger } from '../logger';
2121
import { ConnectionTransport, WebSocketTransport } from '../transport';
2222
import { BrowserBase, BrowserOptions, Browser } from '../browser';
23-
import { assert } from '../helper';
23+
import { assert, helper } from '../helper';
24+
import { TimeoutSettings } from '../timeoutSettings';
2425

2526
export type BrowserArgOptions = {
2627
headless?: boolean,
@@ -48,6 +49,7 @@ export type ConnectOptions = {
4849
wsEndpoint: string,
4950
slowMo?: number,
5051
logger?: Logger,
52+
timeout?: number,
5153
};
5254
export type LaunchType = 'local' | 'server' | 'persistent';
5355
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
@@ -86,42 +88,88 @@ export abstract class BrowserTypeBase implements BrowserType {
8688

8789
async launch(options: LaunchOptions = {}): Promise<Browser> {
8890
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
89-
const browserServer = new BrowserServer(options);
90-
const { transport, downloadsPath } = await this._launchServer(options, 'local', browserServer);
91-
return await browserServer._initializeOrClose(async () => {
92-
return this._connectToServer(browserServer, false, transport!, downloadsPath);
93-
});
91+
return this._innerLaunch('local', options);
9492
}
9593

9694
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
97-
const browserServer = new BrowserServer(options);
98-
const { transport, downloadsPath } = await this._launchServer(options, 'persistent', browserServer, userDataDir);
95+
const browser = await this._innerLaunch('persistent', options, userDataDir);
96+
return browser._defaultContext!;
97+
}
98+
99+
async _innerLaunch(launchType: LaunchType, options: LaunchOptions, userDataDir?: string): Promise<BrowserBase> {
100+
const deadline = TimeoutSettings.computeDeadline(options.timeout, 30000);
101+
const logger = new RootLogger(options.logger);
102+
logger.startLaunchRecording();
103+
104+
let browserServer: BrowserServer | undefined;
105+
try {
106+
browserServer = await this._launchServer(options, launchType, logger, deadline, userDataDir);
107+
const promise = this._innerLaunchPromise(browserServer, launchType, options);
108+
const browser = await helper.waitWithDeadline(promise, 'the browser to launch', deadline, 'pw:browser*');
109+
return browser;
110+
} catch (e) {
111+
e.message += '\n=============== Process output during launch: ===============\n' +
112+
logger.launchRecording() +
113+
'\n=============================================================';
114+
if (browserServer)
115+
await browserServer._closeOrKill(deadline);
116+
throw e;
117+
} finally {
118+
logger.stopLaunchRecording();
119+
}
120+
}
99121

100-
return await browserServer._initializeOrClose(async () => {
101-
const browser = await this._connectToServer(browserServer, true, transport!, downloadsPath);
122+
async _innerLaunchPromise(browserServer: BrowserServer, launchType: LaunchType, options: LaunchOptions): Promise<BrowserBase> {
123+
if ((options as any).__testHookBeforeCreateBrowser)
124+
await (options as any).__testHookBeforeCreateBrowser();
125+
126+
const browser = await this._connectToServer(browserServer, launchType === 'persistent');
127+
if (launchType === 'persistent' && (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))) {
102128
const context = browser._defaultContext!;
103-
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
104-
await context._loadDefaultContext();
105-
return context;
106-
});
129+
await context._loadDefaultContext();
130+
}
131+
return browser;
107132
}
108133

109134
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
110-
const browserServer = new BrowserServer(options);
111-
await this._launchServer(options, 'server', browserServer);
112-
return browserServer;
135+
const logger = new RootLogger(options.logger);
136+
return this._launchServer(options, 'server', logger, TimeoutSettings.computeDeadline(options.timeout, 30000));
113137
}
114138

115139
async connect(options: ConnectOptions): Promise<Browser> {
140+
const deadline = TimeoutSettings.computeDeadline(options.timeout, 30000);
116141
const logger = new RootLogger(options.logger);
117-
return await WebSocketTransport.connect(options.wsEndpoint, async transport => {
118-
if ((options as any).__testHookBeforeCreateBrowser)
119-
await (options as any).__testHookBeforeCreateBrowser();
120-
return this._connectToTransport(transport, { slowMo: options.slowMo, logger, downloadsPath: '' });
121-
}, logger);
142+
logger.startLaunchRecording();
143+
144+
let transport: ConnectionTransport | undefined;
145+
try {
146+
transport = await WebSocketTransport.connect(options.wsEndpoint, logger, deadline);
147+
const promise = this._innerConnectPromise(transport, options, logger);
148+
const browser = await helper.waitWithDeadline(promise, 'connect to browser', deadline, 'pw:browser*');
149+
logger.stopLaunchRecording();
150+
return browser;
151+
} catch (e) {
152+
e.message += '\n=============== Process output during connect: ===============\n' +
153+
logger.launchRecording() +
154+
'\n=============================================================';
155+
try {
156+
if (transport)
157+
transport.close();
158+
} catch (e) {
159+
}
160+
throw e;
161+
} finally {
162+
logger.stopLaunchRecording();
163+
}
164+
}
165+
166+
async _innerConnectPromise(transport: ConnectionTransport, options: ConnectOptions, logger: RootLogger): Promise<Browser> {
167+
if ((options as any).__testHookBeforeCreateBrowser)
168+
await (options as any).__testHookBeforeCreateBrowser();
169+
return this._connectToTransport(transport, { slowMo: options.slowMo, logger, downloadsPath: '' });
122170
}
123171

124-
abstract _launchServer(options: LaunchServerOptions, launchType: LaunchType, browserServer: BrowserServer, userDataDir?: string): Promise<{ transport?: ConnectionTransport, downloadsPath: string }>;
125-
abstract _connectToServer(browserServer: BrowserServer, persistent: boolean, transport: ConnectionTransport, downloadsPath: string): Promise<BrowserBase>;
172+
abstract _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer>;
173+
abstract _connectToServer(browserServer: BrowserServer, persistent: boolean): Promise<BrowserBase>;
126174
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
127175
}

0 commit comments

Comments
 (0)