Skip to content

Commit 3d6d9db

Browse files
fix: wait for the process to close when closing the browser (#1629)
1 parent b1580a3 commit 3d6d9db

File tree

11 files changed

+57
-36
lines changed

11 files changed

+57
-36
lines changed

src/browser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface Browser extends EventEmitter {
2424
newPage(options?: BrowserContextOptions): Promise<Page>;
2525
isConnected(): boolean;
2626
close(): Promise<void>;
27+
_disconnect(): Promise<void>;
2728
_setDebugFunction(debugFunction: (message: string) => void): void;
2829
}
2930

src/chromium/crBrowser.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { Events } from './events';
3030
import { Protocol } from './protocol';
3131
import { CRExecutionContext } from './crExecutionContext';
3232
import { EventEmitter } from 'events';
33+
import type { BrowserServer } from '../server/browserServer';
3334

3435
export class CRBrowser extends EventEmitter implements Browser {
3536
readonly _connection: CRConnection;
@@ -46,6 +47,7 @@ export class CRBrowser extends EventEmitter implements Browser {
4647
private _tracingRecording = false;
4748
private _tracingPath: string | null = '';
4849
private _tracingClient: CRSession | undefined;
50+
_ownedServer: BrowserServer | null = null;
4951

5052
static async connect(transport: ConnectionTransport, isPersistent: boolean, slowMo?: number): Promise<CRBrowser> {
5153
const connection = new CRConnection(SlowMoTransport.wrap(transport, slowMo));
@@ -183,13 +185,20 @@ export class CRBrowser extends EventEmitter implements Browser {
183185
await this._session.send('Target.closeTarget', { targetId: crPage._targetId });
184186
}
185187

186-
async close() {
188+
async _disconnect() {
187189
const disconnected = new Promise(f => this._connection.once(ConnectionEvents.Disconnected, f));
188190
await Promise.all(this.contexts().map(context => context.close()));
189191
this._connection.close();
190192
await disconnected;
191193
}
192194

195+
async close() {
196+
if (this._ownedServer)
197+
await this._ownedServer.close();
198+
else
199+
await this._disconnect();
200+
}
201+
193202
async newBrowserCDPSession(): Promise<CRSession> {
194203
return await this._connection.createBrowserSession();
195204
}

src/firefox/ffBrowser.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { headersArray } from './ffNetworkManager';
2828
import { FFPage } from './ffPage';
2929
import { Protocol } from './protocol';
3030
import { EventEmitter } from 'events';
31+
import type { BrowserServer } from '../server/browserServer';
3132

3233
export class FFBrowser extends EventEmitter implements Browser {
3334
_connection: FFConnection;
@@ -37,6 +38,7 @@ export class FFBrowser extends EventEmitter implements Browser {
3738
private _eventListeners: RegisteredListener[];
3839
readonly _firstPagePromise: Promise<void>;
3940
private _firstPageCallback = () => {};
41+
_ownedServer: BrowserServer | null = null;
4042

4143
static async connect(transport: ConnectionTransport, attachToDefaultContext: boolean, slowMo?: number): Promise<FFBrowser> {
4244
const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo));
@@ -140,14 +142,21 @@ export class FFBrowser extends EventEmitter implements Browser {
140142
});
141143
}
142144

143-
async close() {
145+
async _disconnect() {
144146
await Promise.all(this.contexts().map(context => context.close()));
145147
helper.removeEventListeners(this._eventListeners);
146148
const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f));
147149
this._connection.close();
148150
await disconnected;
149151
}
150152

153+
async close() {
154+
if (this._ownedServer)
155+
await this._ownedServer.close();
156+
else
157+
await this._disconnect();
158+
}
159+
151160
_setDebugFunction(debugFunction: debug.IDebugger) {
152161
this._connection._debugProtocol = debugFunction;
153162
}

src/server/chromium.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import * as util from 'util';
2222
import { debugError, helper, assert } from '../helper';
2323
import { CRBrowser } from '../chromium/crBrowser';
2424
import * as ws from 'ws';
25-
import { launchProcess } from '../server/processLauncher';
25+
import { launchProcess } from './processLauncher';
2626
import { kBrowserCloseMessageId } from '../chromium/crConnection';
2727
import { PipeTransport } from './pipeTransport';
2828
import { LaunchOptions, BrowserArgOptions, BrowserType, ConnectOptions, LaunchServerOptions } from './browserType';
@@ -49,7 +49,7 @@ export class Chromium implements BrowserType<CRBrowser> {
4949
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
5050
const { browserServer, transport } = await this._launchServer(options, 'local');
5151
const browser = await CRBrowser.connect(transport!, false, options.slowMo);
52-
(browser as any)['__server__'] = browserServer;
52+
browser._ownedServer = browserServer;
5353
return browser;
5454
}
5555

@@ -62,8 +62,9 @@ export class Chromium implements BrowserType<CRBrowser> {
6262
timeout = 30000,
6363
slowMo = 0
6464
} = options;
65-
const { transport } = await this._launchServer(options, 'persistent', userDataDir);
65+
const { transport, browserServer } = await this._launchServer(options, 'persistent', userDataDir);
6666
const browser = await CRBrowser.connect(transport!, true, slowMo);
67+
browser._ownedServer = browserServer;
6768
await helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout);
6869
return browser._defaultContext;
6970
}
@@ -110,8 +111,7 @@ export class Chromium implements BrowserType<CRBrowser> {
110111
pipe: true,
111112
tempDir: temporaryUserDataDir || undefined,
112113
attemptToGracefullyClose: async () => {
113-
if (!browserServer)
114-
return Promise.reject();
114+
assert(browserServer);
115115
// We try to gracefully close to prevent crash reporting and core dumps.
116116
// Note that it's fine to reuse the pipe transport, since
117117
// our connection ignores kBrowserCloseMessageId.
@@ -127,7 +127,7 @@ export class Chromium implements BrowserType<CRBrowser> {
127127

128128
let transport: PipeTransport | undefined = undefined;
129129
let browserServer: BrowserServer | undefined = undefined;
130-
transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream, () => browserServer!.close());
130+
transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
131131
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, port) : null);
132132
return { browserServer, transport };
133133
}

src/server/firefox.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,7 @@ export class Firefox implements BrowserType<FFBrowser> {
5353
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
5454
return FFBrowser.connect(transport, false, options.slowMo);
5555
});
56-
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
57-
browser.close = () => browserServer.close();
58-
(browser as any)['__server__'] = browserServer;
56+
browser._ownedServer = browserServer;
5957
return browser;
6058
}
6159

@@ -72,10 +70,9 @@ export class Firefox implements BrowserType<FFBrowser> {
7270
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
7371
return FFBrowser.connect(transport, true, slowMo);
7472
});
73+
browser._ownedServer = browserServer;
7574
await helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout);
76-
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
7775
const browserContext = browser._defaultContext;
78-
browserContext.close = () => browserServer.close();
7976
return browserContext;
8077
}
8178

@@ -128,11 +125,8 @@ export class Firefox implements BrowserType<FFBrowser> {
128125
pipe: false,
129126
tempDir: temporaryProfileDir || undefined,
130127
attemptToGracefullyClose: async () => {
131-
if (!browserServer)
132-
return Promise.reject();
128+
assert(browserServer);
133129
// We try to gracefully close to prevent crash reporting and core dumps.
134-
// Note that it's fine to reuse the pipe transport, since
135-
// our connection ignores kBrowserCloseMessageId.
136130
const transport = await WebSocketTransport.connect(browserWSEndpoint!, async transport => transport);
137131
const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId };
138132
await transport.send(message);

src/server/pipeTransport.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,12 @@ export class PipeTransport implements ConnectionTransport {
2323
private _pendingMessage = '';
2424
private _eventListeners: RegisteredListener[];
2525
private _waitForNextTask = helper.makeWaitForNextTask();
26-
private readonly _closeCallback: () => void;
2726

2827
onmessage?: (message: ProtocolResponse) => void;
2928
onclose?: () => void;
3029

31-
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream, closeCallback: () => void) {
30+
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream) {
3231
this._pipeWrite = pipeWrite;
33-
this._closeCallback = closeCallback;
3432
this._eventListeners = [
3533
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
3634
helper.addEventListener(pipeRead, 'close', () => {
@@ -51,7 +49,7 @@ export class PipeTransport implements ConnectionTransport {
5149
}
5250

5351
close() {
54-
this._closeCallback();
52+
throw new Error('unimplemented');
5553
}
5654

5755
_dispatch(buffer: Buffer) {

src/server/processLauncher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
135135
}
136136
gracefullyClosing = true;
137137
debugBrowser(`<gracefully close start>`);
138-
options.attemptToGracefullyClose().catch(() => killProcess());
138+
await options.attemptToGracefullyClose().catch(() => killProcess());
139139
await waitForProcessToClose;
140140
debugBrowser(`<gracefully close end>`);
141141
}

src/server/webkit.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ export class WebKit implements BrowserType<WKBrowser> {
4848
async launch(options: LaunchOptions = {}): Promise<WKBrowser> {
4949
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
5050
const { browserServer, transport } = await this._launchServer(options, 'local');
51-
const browser = await WKBrowser.connect(transport!, options.slowMo);
52-
(browser as any)['__server__'] = browserServer;
51+
const browser = await WKBrowser.connect(transport!, options.slowMo, false, () => browserServer.close());
52+
browser._ownedServer = browserServer;
5353
return browser;
5454
}
5555

@@ -62,8 +62,8 @@ export class WebKit implements BrowserType<WKBrowser> {
6262
timeout = 30000,
6363
slowMo = 0,
6464
} = options;
65-
const { transport } = await this._launchServer(options, 'persistent', userDataDir);
66-
const browser = await WKBrowser.connect(transport!, slowMo, true);
65+
const { transport, browserServer } = await this._launchServer(options, 'persistent', userDataDir);
66+
const browser = await WKBrowser.connect(transport!, slowMo, true, () => browserServer.close());
6767
await helper.waitWithTimeout(browser._waitForFirstPageTarget(), 'first page', timeout);
6868
return browser._defaultContext;
6969
}
@@ -111,12 +111,11 @@ export class WebKit implements BrowserType<WKBrowser> {
111111
pipe: true,
112112
tempDir: temporaryUserDataDir || undefined,
113113
attemptToGracefullyClose: async () => {
114-
if (!transport)
115-
return Promise.reject();
114+
assert(transport);
116115
// We try to gracefully close to prevent crash reporting and core dumps.
117116
// Note that it's fine to reuse the pipe transport, since
118117
// our connection ignores kBrowserCloseMessageId.
119-
transport.send({method: 'Playwright.close', params: {}, id: kBrowserCloseMessageId});
118+
await transport.send({method: 'Playwright.close', params: {}, id: kBrowserCloseMessageId});
120119
},
121120
onkill: (exitCode, signal) => {
122121
if (browserServer)
@@ -127,7 +126,7 @@ export class WebKit implements BrowserType<WKBrowser> {
127126
// For local launch scenario close will terminate the browser process.
128127
let transport: ConnectionTransport | undefined = undefined;
129128
let browserServer: BrowserServer | undefined = undefined;
130-
transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream, () => browserServer!.close());
129+
transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
131130
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, port || 0) : null);
132131
return { browserServer, transport };
133132
}

src/webkit/wkBrowser.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { Protocol } from './protocol';
2727
import { kPageProxyMessageReceived, PageProxyMessageReceivedPayload, WKConnection, WKSession } from './wkConnection';
2828
import { WKPage } from './wkPage';
2929
import { EventEmitter } from 'events';
30+
import type { BrowserServer } from '../server/browserServer';
3031

3132
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15';
3233

@@ -39,19 +40,22 @@ export class WKBrowser extends EventEmitter implements Browser {
3940
readonly _wkPages = new Map<string, WKPage>();
4041
private readonly _eventListeners: RegisteredListener[];
4142
private _popupOpeners: string[] = [];
43+
private _closeOverride?: () => Promise<void>;
4244

4345
private _firstPageCallback: () => void = () => {};
4446
private readonly _firstPagePromise: Promise<void>;
47+
_ownedServer: BrowserServer | null = null;
4548

46-
static async connect(transport: ConnectionTransport, slowMo: number = 0, attachToDefaultContext: boolean = false): Promise<WKBrowser> {
47-
const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo), attachToDefaultContext);
49+
static async connect(transport: ConnectionTransport, slowMo: number = 0, attachToDefaultContext: boolean = false, closeOverride?: () => Promise<void>): Promise<WKBrowser> {
50+
const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo), attachToDefaultContext, closeOverride);
4851
return browser;
4952
}
5053

51-
constructor(transport: ConnectionTransport, attachToDefaultContext: boolean) {
54+
constructor(transport: ConnectionTransport, attachToDefaultContext: boolean, closeOverride?: () => Promise<void>) {
5255
super();
5356
this._connection = new WKConnection(transport, this._onDisconnect.bind(this));
5457
this._attachToDefaultContext = attachToDefaultContext;
58+
this._closeOverride = closeOverride;
5559
this._browserSession = this._connection.browserSession;
5660

5761
this._defaultContext = new WKBrowserContext(this, undefined, validateBrowserContextOptions({}));
@@ -178,14 +182,21 @@ export class WKBrowser extends EventEmitter implements Browser {
178182
return !this._connection.isClosed();
179183
}
180184

181-
async close() {
185+
async _disconnect() {
182186
helper.removeEventListeners(this._eventListeners);
183187
const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f));
184188
await Promise.all(this.contexts().map(context => context.close()));
185189
this._connection.close();
186190
await disconnected;
187191
}
188192

193+
async close() {
194+
if (this._closeOverride)
195+
await this._closeOverride();
196+
else
197+
await this._disconnect();
198+
}
199+
189200
_setDebugFunction(debugFunction: debug.IDebugger) {
190201
this._connection._debugProtocol = debugFunction;
191202
}

test/playwright.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ module.exports.addPlaywrightTests = ({testRunner, platform, products, playwright
160160
describe('', function() {
161161
beforeAll(async state => {
162162
state.browser = await browserType.launch(defaultBrowserOptions);
163-
state.browserServer = state.browser.__server__;
163+
state.browserServer = state.browser._ownedServer;
164164
state._stdout = readline.createInterface({ input: state.browserServer.process().stdout });
165165
state._stderr = readline.createInterface({ input: state.browserServer.process().stderr });
166166
});

0 commit comments

Comments
 (0)