feat(browserApp): kill and onclose (#641)
This commit is contained in:
parent
f1d1dfb081
commit
be19ae5e67
|
|
@ -20,6 +20,10 @@ export const Events = {
|
||||||
Disconnected: 'disconnected'
|
Disconnected: 'disconnected'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
BrowserApp: {
|
||||||
|
Close: 'close',
|
||||||
|
},
|
||||||
|
|
||||||
Page: {
|
Page: {
|
||||||
Close: 'close',
|
Close: 'close',
|
||||||
Console: 'console',
|
Console: 'console',
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,17 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ChildProcess } from 'child_process';
|
import { ChildProcess, execSync } from 'child_process';
|
||||||
import { ConnectOptions } from '../browser';
|
import { ConnectOptions } from '../browser';
|
||||||
|
import * as platform from '../platform';
|
||||||
|
|
||||||
export class BrowserApp {
|
export class BrowserApp extends platform.EventEmitter {
|
||||||
private _process: ChildProcess;
|
private _process: ChildProcess;
|
||||||
private _gracefullyClose: () => Promise<void>;
|
private _gracefullyClose: () => Promise<void>;
|
||||||
private _connectOptions: ConnectOptions;
|
private _connectOptions: ConnectOptions;
|
||||||
|
|
||||||
constructor(process: ChildProcess, gracefullyClose: () => Promise<void>, connectOptions: ConnectOptions) {
|
constructor(process: ChildProcess, gracefullyClose: () => Promise<void>, connectOptions: ConnectOptions) {
|
||||||
|
super();
|
||||||
this._process = process;
|
this._process = process;
|
||||||
this._gracefullyClose = gracefullyClose;
|
this._gracefullyClose = gracefullyClose;
|
||||||
this._connectOptions = connectOptions;
|
this._connectOptions = connectOptions;
|
||||||
|
|
@ -40,6 +42,19 @@ export class BrowserApp {
|
||||||
return this._connectOptions;
|
return this._connectOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kill() {
|
||||||
|
if (this._process.pid && !this._process.killed) {
|
||||||
|
try {
|
||||||
|
if (process.platform === 'win32')
|
||||||
|
execSync(`taskkill /pid ${this._process.pid} /T /F`);
|
||||||
|
else
|
||||||
|
process.kill(-this._process.pid, 'SIGKILL');
|
||||||
|
} catch (e) {
|
||||||
|
// the process might have already stopped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
await this._gracefullyClose();
|
await this._gracefullyClose();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import { PipeTransport } from './pipeTransport';
|
||||||
import { LaunchOptions, BrowserArgOptions, BrowserType } from './browserType';
|
import { LaunchOptions, BrowserArgOptions, BrowserType } from './browserType';
|
||||||
import { createTransport, ConnectOptions } from '../browser';
|
import { createTransport, ConnectOptions } from '../browser';
|
||||||
import { BrowserApp } from './browserApp';
|
import { BrowserApp } from './browserApp';
|
||||||
|
import { Events } from '../events';
|
||||||
|
|
||||||
export class Chromium implements BrowserType {
|
export class Chromium implements BrowserType {
|
||||||
private _projectRoot: string;
|
private _projectRoot: string;
|
||||||
|
|
@ -94,6 +95,7 @@ export class Chromium implements BrowserType {
|
||||||
if (usePipe && webSocket)
|
if (usePipe && webSocket)
|
||||||
throw new Error(`Argument "--remote-debugging-pipe" is not compatible with "webSocket" launch option.`);
|
throw new Error(`Argument "--remote-debugging-pipe" is not compatible with "webSocket" launch option.`);
|
||||||
|
|
||||||
|
let browserApp: BrowserApp | undefined = undefined;
|
||||||
const { launchedProcess, gracefullyClose } = await launchProcess({
|
const { launchedProcess, gracefullyClose } = await launchProcess({
|
||||||
executablePath: chromeExecutable!,
|
executablePath: chromeExecutable!,
|
||||||
args: chromeArguments,
|
args: chromeArguments,
|
||||||
|
|
@ -105,19 +107,22 @@ export class Chromium implements BrowserType {
|
||||||
pipe: usePipe,
|
pipe: usePipe,
|
||||||
tempDir: temporaryUserDataDir || undefined,
|
tempDir: temporaryUserDataDir || undefined,
|
||||||
attemptToGracefullyClose: async () => {
|
attemptToGracefullyClose: async () => {
|
||||||
if (!connectOptions)
|
if (!browserApp)
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
|
|
||||||
// We try to gracefully close to prevent crash reporting and core dumps.
|
// We try to gracefully close to prevent crash reporting and core dumps.
|
||||||
// Note that it's fine to reuse the pipe transport, since
|
// Note that it's fine to reuse the pipe transport, since
|
||||||
// our connection ignores kBrowserCloseMessageId.
|
// our connection ignores kBrowserCloseMessageId.
|
||||||
const transport = await createTransport(connectOptions);
|
const transport = await createTransport(browserApp.connectOptions());
|
||||||
const message = { method: 'Browser.close', id: kBrowserCloseMessageId };
|
const message = { method: 'Browser.close', id: kBrowserCloseMessageId };
|
||||||
transport.send(JSON.stringify(message));
|
transport.send(JSON.stringify(message));
|
||||||
},
|
},
|
||||||
|
onkill: () => {
|
||||||
|
if (browserApp)
|
||||||
|
browserApp.emit(Events.BrowserApp.Close);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let connectOptions: ConnectOptions | undefined;
|
let connectOptions: ConnectOptions;
|
||||||
if (!usePipe) {
|
if (!usePipe) {
|
||||||
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}`);
|
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}`);
|
||||||
const match = await waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, timeout, timeoutError);
|
const match = await waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, timeout, timeoutError);
|
||||||
|
|
@ -127,7 +132,8 @@ export class Chromium implements BrowserType {
|
||||||
const transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
|
const transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
|
||||||
connectOptions = { slowMo, transport };
|
connectOptions = { slowMo, transport };
|
||||||
}
|
}
|
||||||
return new BrowserApp(launchedProcess, gracefullyClose, connectOptions);
|
browserApp = new BrowserApp(launchedProcess, gracefullyClose, connectOptions);
|
||||||
|
return browserApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(options: ConnectOptions & { browserURL?: string }): Promise<CRBrowser> {
|
async connect(options: ConnectOptions & { browserURL?: string }): Promise<CRBrowser> {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { assert } from '../helper';
|
||||||
import { LaunchOptions, BrowserArgOptions, BrowserType } from './browserType';
|
import { LaunchOptions, BrowserArgOptions, BrowserType } from './browserType';
|
||||||
import { createTransport, ConnectOptions } from '../browser';
|
import { createTransport, ConnectOptions } from '../browser';
|
||||||
import { BrowserApp } from './browserApp';
|
import { BrowserApp } from './browserApp';
|
||||||
|
import { Events } from '../events';
|
||||||
|
|
||||||
export class Firefox implements BrowserType {
|
export class Firefox implements BrowserType {
|
||||||
private _projectRoot: string;
|
private _projectRoot: string;
|
||||||
|
|
@ -89,8 +90,7 @@ export class Firefox implements BrowserType {
|
||||||
firefoxExecutable = executablePath;
|
firefoxExecutable = executablePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
let connectOptions: ConnectOptions | undefined = undefined;
|
let browserApp: BrowserApp | undefined = undefined;
|
||||||
|
|
||||||
const { launchedProcess, gracefullyClose } = await launchProcess({
|
const { launchedProcess, gracefullyClose } = await launchProcess({
|
||||||
executablePath: firefoxExecutable,
|
executablePath: firefoxExecutable,
|
||||||
args: firefoxArguments,
|
args: firefoxArguments,
|
||||||
|
|
@ -106,27 +106,33 @@ export class Firefox implements BrowserType {
|
||||||
pipe: false,
|
pipe: false,
|
||||||
tempDir: temporaryProfileDir || undefined,
|
tempDir: temporaryProfileDir || undefined,
|
||||||
attemptToGracefullyClose: async () => {
|
attemptToGracefullyClose: async () => {
|
||||||
if (!connectOptions)
|
if (!browserApp)
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
// We try to gracefully close to prevent crash reporting and core dumps.
|
// We try to gracefully close to prevent crash reporting and core dumps.
|
||||||
// Note that it's fine to reuse the pipe transport, since
|
// Note that it's fine to reuse the pipe transport, since
|
||||||
// our connection ignores kBrowserCloseMessageId.
|
// our connection ignores kBrowserCloseMessageId.
|
||||||
const transport = await createTransport(connectOptions);
|
const transport = await createTransport(browserApp.connectOptions());
|
||||||
const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId };
|
const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId };
|
||||||
transport.send(JSON.stringify(message));
|
transport.send(JSON.stringify(message));
|
||||||
},
|
},
|
||||||
|
onkill: () => {
|
||||||
|
if (browserApp)
|
||||||
|
browserApp.emit(Events.BrowserApp.Close);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`);
|
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`);
|
||||||
const match = await waitForLine(launchedProcess, launchedProcess.stdout, /^Juggler listening on (ws:\/\/.*)$/, timeout, timeoutError);
|
const match = await waitForLine(launchedProcess, launchedProcess.stdout, /^Juggler listening on (ws:\/\/.*)$/, timeout, timeoutError);
|
||||||
const browserWSEndpoint = match[1];
|
const browserWSEndpoint = match[1];
|
||||||
|
let connectOptions: ConnectOptions;
|
||||||
if (webSocket) {
|
if (webSocket) {
|
||||||
connectOptions = { browserWSEndpoint, slowMo };
|
connectOptions = { browserWSEndpoint, slowMo };
|
||||||
} else {
|
} else {
|
||||||
const transport = await platform.createWebSocketTransport(browserWSEndpoint);
|
const transport = await platform.createWebSocketTransport(browserWSEndpoint);
|
||||||
connectOptions = { transport, slowMo };
|
connectOptions = { transport, slowMo };
|
||||||
}
|
}
|
||||||
return new BrowserApp(launchedProcess, gracefullyClose, connectOptions);
|
browserApp = new BrowserApp(launchedProcess, gracefullyClose, connectOptions);
|
||||||
|
return browserApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(options: ConnectOptions & { browserURL?: string }): Promise<FFBrowser> {
|
async connect(options: ConnectOptions & { browserURL?: string }): Promise<FFBrowser> {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export type LaunchProcessOptions = {
|
||||||
|
|
||||||
// Note: attemptToGracefullyClose should reject if it does not close the browser.
|
// Note: attemptToGracefullyClose should reject if it does not close the browser.
|
||||||
attemptToGracefullyClose: () => Promise<any>,
|
attemptToGracefullyClose: () => Promise<any>,
|
||||||
|
onkill: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
type LaunchResult = { launchedProcess: childProcess.ChildProcess, gracefullyClose: () => Promise<void> };
|
type LaunchResult = { launchedProcess: childProcess.ChildProcess, gracefullyClose: () => Promise<void> };
|
||||||
|
|
@ -97,6 +98,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
} else {
|
} else {
|
||||||
fulfill();
|
fulfill();
|
||||||
}
|
}
|
||||||
|
options.onkill();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import * as ws from 'ws';
|
||||||
import * as uuidv4 from 'uuid/v4';
|
import * as uuidv4 from 'uuid/v4';
|
||||||
import { ConnectOptions } from '../browser';
|
import { ConnectOptions } from '../browser';
|
||||||
import { BrowserApp } from './browserApp';
|
import { BrowserApp } from './browserApp';
|
||||||
|
import { Events } from '../events';
|
||||||
|
|
||||||
export class WebKit implements BrowserType {
|
export class WebKit implements BrowserType {
|
||||||
private _projectRoot: string;
|
private _projectRoot: string;
|
||||||
|
|
@ -94,8 +95,9 @@ export class WebKit implements BrowserType {
|
||||||
throw new Error(missingText);
|
throw new Error(missingText);
|
||||||
webkitExecutable = executablePath;
|
webkitExecutable = executablePath;
|
||||||
}
|
}
|
||||||
let transport: PipeTransport | undefined = undefined;
|
|
||||||
|
|
||||||
|
let transport: PipeTransport | undefined = undefined;
|
||||||
|
let browserApp: BrowserApp | undefined = undefined;
|
||||||
const { launchedProcess, gracefullyClose } = await launchProcess({
|
const { launchedProcess, gracefullyClose } = await launchProcess({
|
||||||
executablePath: webkitExecutable!,
|
executablePath: webkitExecutable!,
|
||||||
args: webkitArguments,
|
args: webkitArguments,
|
||||||
|
|
@ -115,6 +117,10 @@ export class WebKit implements BrowserType {
|
||||||
const message = JSON.stringify({method: 'Browser.close', params: {}, id: kBrowserCloseMessageId});
|
const message = JSON.stringify({method: 'Browser.close', params: {}, id: kBrowserCloseMessageId});
|
||||||
transport.send(message);
|
transport.send(message);
|
||||||
},
|
},
|
||||||
|
onkill: () => {
|
||||||
|
if (browserApp)
|
||||||
|
browserApp.emit(Events.BrowserApp.Close);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
|
transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
|
||||||
|
|
@ -126,7 +132,8 @@ export class WebKit implements BrowserType {
|
||||||
} else {
|
} else {
|
||||||
connectOptions = { transport, slowMo };
|
connectOptions = { transport, slowMo };
|
||||||
}
|
}
|
||||||
return new BrowserApp(launchedProcess, gracefullyClose, connectOptions);
|
browserApp = new BrowserApp(launchedProcess, gracefullyClose, connectOptions);
|
||||||
|
return browserApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(options: ConnectOptions & { browserURL?: string }): Promise<WKBrowser> {
|
async connect(options: ConnectOptions & { browserURL?: string }): Promise<WKBrowser> {
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,14 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||||
expect(message).not.toContain('Timeout');
|
expect(message).not.toContain('Timeout');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
it('should be able to close remote browser', async({server}) => {
|
||||||
|
const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true});
|
||||||
|
const remote = await playwright.connect(browserApp.connectOptions());
|
||||||
|
await Promise.all([
|
||||||
|
new Promise(f => browserApp.once('close', f)),
|
||||||
|
remote.close(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Playwright.launch |webSocket| option', function() {
|
describe('Playwright.launch |webSocket| option', function() {
|
||||||
|
|
@ -244,8 +252,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||||
const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions);
|
const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions);
|
||||||
const browser = await playwright.connect(browserApp.connectOptions());
|
const browser = await playwright.connect(browserApp.connectOptions());
|
||||||
const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve));
|
const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve));
|
||||||
// Emulate user exiting browser.
|
browserApp.kill();
|
||||||
process.kill(-browserApp.process().pid, 'SIGKILL');
|
|
||||||
await disconnectedEventPromise;
|
await disconnectedEventPromise;
|
||||||
});
|
});
|
||||||
it('should fire "disconnected" when closing with webSocket', async() => {
|
it('should fire "disconnected" when closing with webSocket', async() => {
|
||||||
|
|
@ -253,8 +260,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
|
||||||
const browserApp = await playwright.launchBrowserApp(options);
|
const browserApp = await playwright.launchBrowserApp(options);
|
||||||
const browser = await playwright.connect(browserApp.connectOptions());
|
const browser = await playwright.connect(browserApp.connectOptions());
|
||||||
const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve));
|
const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve));
|
||||||
// Emulate user exiting browser.
|
browserApp.kill();
|
||||||
process.kill(-browserApp.process().pid, 'SIGKILL');
|
|
||||||
await disconnectedEventPromise;
|
await disconnectedEventPromise;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue