feat(rpc): gracefully close browsers in server process on disconnect (#3005)
This commit is contained in:
parent
9d9801192e
commit
91e1a25f34
|
|
@ -20,12 +20,19 @@ import { Playwright } from '../server/playwright';
|
||||||
import { PlaywrightDispatcher } from './server/playwrightDispatcher';
|
import { PlaywrightDispatcher } from './server/playwrightDispatcher';
|
||||||
import { Electron } from '../server/electron';
|
import { Electron } from '../server/electron';
|
||||||
import { setUseApiName } from '../progress';
|
import { setUseApiName } from '../progress';
|
||||||
|
import { gracefullyCloseAll } from '../server/processLauncher';
|
||||||
|
|
||||||
setUseApiName(false);
|
setUseApiName(false);
|
||||||
|
|
||||||
const dispatcherConnection = new DispatcherConnection();
|
const dispatcherConnection = new DispatcherConnection();
|
||||||
const transport = new Transport(process.stdout, process.stdin);
|
const transport = new Transport(process.stdout, process.stdin);
|
||||||
transport.onclose = () => process.exit(0);
|
transport.onclose = async () => {
|
||||||
|
// Force exit after 30 seconds.
|
||||||
|
setTimeout(() => process.exit(0), 30000);
|
||||||
|
// Meanwhile, try to gracefully close all browsers.
|
||||||
|
await gracefullyCloseAll();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
transport.onmessage = message => dispatcherConnection.dispatch(JSON.parse(message));
|
transport.onmessage = message => dispatcherConnection.dispatch(JSON.parse(message));
|
||||||
dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message));
|
dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ type LaunchResult = {
|
||||||
kill: () => Promise<void>,
|
kill: () => Promise<void>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const gracefullyCloseSet = new Set<() => Promise<void>>();
|
||||||
|
|
||||||
|
export async function gracefullyCloseAll() {
|
||||||
|
await Promise.all(Array.from(gracefullyCloseSet).map(gracefullyClose => gracefullyClose().catch(e => {})));
|
||||||
|
}
|
||||||
|
|
||||||
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
|
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
|
||||||
const cleanup = () => helper.removeFolders(options.tempDirectories);
|
const cleanup = () => helper.removeFolders(options.tempDirectories);
|
||||||
|
|
||||||
|
|
@ -97,6 +103,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
progress.logger.info(`<process did exit: exitCode=${exitCode}, signal=${signal}>`);
|
progress.logger.info(`<process did exit: exitCode=${exitCode}, signal=${signal}>`);
|
||||||
processClosed = true;
|
processClosed = true;
|
||||||
helper.removeEventListeners(listeners);
|
helper.removeEventListeners(listeners);
|
||||||
|
gracefullyCloseSet.delete(gracefullyClose);
|
||||||
options.onExit(exitCode, signal);
|
options.onExit(exitCode, signal);
|
||||||
fulfillClose();
|
fulfillClose();
|
||||||
// Cleanup as process exits.
|
// Cleanup as process exits.
|
||||||
|
|
@ -113,9 +120,11 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
listeners.push(helper.addEventListener(process, 'SIGTERM', gracefullyClose));
|
listeners.push(helper.addEventListener(process, 'SIGTERM', gracefullyClose));
|
||||||
if (options.handleSIGHUP)
|
if (options.handleSIGHUP)
|
||||||
listeners.push(helper.addEventListener(process, 'SIGHUP', gracefullyClose));
|
listeners.push(helper.addEventListener(process, 'SIGHUP', gracefullyClose));
|
||||||
|
gracefullyCloseSet.add(gracefullyClose);
|
||||||
|
|
||||||
let gracefullyClosing = false;
|
let gracefullyClosing = false;
|
||||||
async function gracefullyClose(): Promise<void> {
|
async function gracefullyClose(): Promise<void> {
|
||||||
|
gracefullyCloseSet.delete(gracefullyClose);
|
||||||
// We keep listeners until we are done, to handle 'exit' and 'SIGINT' while
|
// We keep listeners until we are done, to handle 'exit' and 'SIGINT' while
|
||||||
// asynchronously closing to prevent zombie processes. This might introduce
|
// asynchronously closing to prevent zombie processes. This might introduce
|
||||||
// reentrancy to this function, for example user sends SIGINT second time.
|
// reentrancy to this function, for example user sends SIGINT second time.
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ describe.skip(!CHROMIUM)('launcher', function() {
|
||||||
const browser = await browserType.launchServer(options);
|
const browser = await browserType.launchServer(options);
|
||||||
await browser.close();
|
await browser.close();
|
||||||
});
|
});
|
||||||
it.skip(USES_HOOKS).fail(CHROMIUM && WIN)('should open devtools when "devtools: true" option is given', async({browserType, defaultBrowserOptions}) => {
|
it.fail(USES_HOOKS || WIN)('should open devtools when "devtools: true" option is given', async({browserType, defaultBrowserOptions}) => {
|
||||||
let devtoolsCallback;
|
let devtoolsCallback;
|
||||||
const devtoolsPromise = new Promise(f => devtoolsCallback = f);
|
const devtoolsPromise = new Promise(f => devtoolsCallback = f);
|
||||||
const __testHookForDevTools = devtools => devtools.__testHookOnBinding = parsed => {
|
const __testHookForDevTools = devtools => devtools.__testHookOnBinding = parsed => {
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ class PlaywrightEnvironment {
|
||||||
constructor(playwright) {
|
constructor(playwright) {
|
||||||
this._playwright = playwright;
|
this._playwright = playwright;
|
||||||
this.spawnedProcess = undefined;
|
this.spawnedProcess = undefined;
|
||||||
this.expectExit = false;
|
this.onExit = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
name() { return 'Playwright'; };
|
name() { return 'Playwright'; };
|
||||||
|
|
@ -173,13 +173,13 @@ class PlaywrightEnvironment {
|
||||||
if (process.env.PWCHANNEL === 'wire') {
|
if (process.env.PWCHANNEL === 'wire') {
|
||||||
this.spawnedProcess = childProcess.fork(path.join(__dirname, '..', 'lib', 'rpc', 'server'), [], {
|
this.spawnedProcess = childProcess.fork(path.join(__dirname, '..', 'lib', 'rpc', 'server'), [], {
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
detached: process.platform !== 'win32',
|
detached: true,
|
||||||
});
|
|
||||||
this.spawnedProcess.once('exit', (exitCode, signal) => {
|
|
||||||
this.spawnedProcess = undefined;
|
|
||||||
if (!this.expectExit)
|
|
||||||
throw new Error(`Server closed with exitCode=${exitCode} signal=${signal}`);
|
|
||||||
});
|
});
|
||||||
|
this.spawnedProcess.unref();
|
||||||
|
this.onExit = (exitCode, signal) => {
|
||||||
|
throw new Error(`Server closed with exitCode=${exitCode} signal=${signal}`);
|
||||||
|
};
|
||||||
|
this.spawnedProcess.once('exit', this.onExit);
|
||||||
const transport = new Transport(this.spawnedProcess.stdin, this.spawnedProcess.stdout);
|
const transport = new Transport(this.spawnedProcess.stdin, this.spawnedProcess.stdout);
|
||||||
connection.onmessage = message => transport.send(JSON.stringify(message));
|
connection.onmessage = message => transport.send(JSON.stringify(message));
|
||||||
transport.onmessage = message => connection.dispatch(JSON.parse(message));
|
transport.onmessage = message => connection.dispatch(JSON.parse(message));
|
||||||
|
|
@ -205,10 +205,10 @@ class PlaywrightEnvironment {
|
||||||
|
|
||||||
async afterAll(state) {
|
async afterAll(state) {
|
||||||
if (this.spawnedProcess) {
|
if (this.spawnedProcess) {
|
||||||
const exited = new Promise(f => this.spawnedProcess.once('exit', f));
|
this.spawnedProcess.removeListener('exit', this.onExit);
|
||||||
this.expectExit = true;
|
this.spawnedProcess.stdin.destroy();
|
||||||
this.spawnedProcess.kill();
|
this.spawnedProcess.stdout.destroy();
|
||||||
await exited;
|
this.spawnedProcess.stderr.destroy();
|
||||||
}
|
}
|
||||||
delete state.playwright;
|
delete state.playwright;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ module.exports = function registerFixtures(global) {
|
||||||
server.PREFIX = `http://localhost:${port}`;
|
server.PREFIX = `http://localhost:${port}`;
|
||||||
server.CROSS_PROCESS_PREFIX = `http://127.0.0.1:${port}`;
|
server.CROSS_PROCESS_PREFIX = `http://127.0.0.1:${port}`;
|
||||||
server.EMPTY_PAGE = `http://localhost:${port}/empty.html`;
|
server.EMPTY_PAGE = `http://localhost:${port}/empty.html`;
|
||||||
|
|
||||||
const httpsPort = port + 1;
|
const httpsPort = port + 1;
|
||||||
const httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort);
|
const httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort);
|
||||||
httpsServer.enableHTTPCache(cachedPath);
|
httpsServer.enableHTTPCache(cachedPath);
|
||||||
|
|
@ -48,13 +48,13 @@ module.exports = function registerFixtures(global) {
|
||||||
httpsServer.PREFIX = `https://localhost:${httpsPort}`;
|
httpsServer.PREFIX = `https://localhost:${httpsPort}`;
|
||||||
httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`;
|
httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`;
|
||||||
httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`;
|
httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`;
|
||||||
|
|
||||||
await test({server, httpsServer});
|
await test({server, httpsServer});
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
server.stop(),
|
server.stop(),
|
||||||
httpsServer.stop(),
|
httpsServer.stop(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
global.registerWorkerFixture('defaultBrowserOptions', async({}, test) => {
|
global.registerWorkerFixture('defaultBrowserOptions', async({}, test) => {
|
||||||
|
|
@ -72,17 +72,17 @@ module.exports = function registerFixtures(global) {
|
||||||
const connection = new Connection();
|
const connection = new Connection();
|
||||||
let toImpl;
|
let toImpl;
|
||||||
let spawnedProcess;
|
let spawnedProcess;
|
||||||
let expectExit;
|
let onExit;
|
||||||
if (process.env.PWCHANNEL === 'wire') {
|
if (process.env.PWCHANNEL === 'wire') {
|
||||||
spawnedProcess = childProcess.fork(path.join(__dirname, '..', '..', 'lib', 'rpc', 'server'), [], {
|
spawnedProcess = childProcess.fork(path.join(__dirname, '..', '..', 'lib', 'rpc', 'server'), [], {
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
detached: process.platform !== 'win32',
|
detached: true,
|
||||||
});
|
|
||||||
spawnedProcess.once('exit', (exitCode, signal) => {
|
|
||||||
spawnedProcess = undefined;
|
|
||||||
if (!expectExit)
|
|
||||||
throw new Error(`Server closed with exitCode=${exitCode} signal=${signal}`);
|
|
||||||
});
|
});
|
||||||
|
spawnedProcess.unref();
|
||||||
|
onExit = (exitCode, signal) => {
|
||||||
|
throw new Error(`Server closed with exitCode=${exitCode} signal=${signal}`);
|
||||||
|
};
|
||||||
|
spawnedProcess.on('exit', onExit);
|
||||||
const transport = new Transport(spawnedProcess.stdin, spawnedProcess.stdout);
|
const transport = new Transport(spawnedProcess.stdin, spawnedProcess.stdout);
|
||||||
connection.onmessage = message => transport.send(JSON.stringify(message));
|
connection.onmessage = message => transport.send(JSON.stringify(message));
|
||||||
transport.onmessage = message => connection.dispatch(JSON.parse(message));
|
transport.onmessage = message => connection.dispatch(JSON.parse(message));
|
||||||
|
|
@ -103,17 +103,16 @@ module.exports = function registerFixtures(global) {
|
||||||
const playwrightObject = await connection.waitForObjectWithKnownName('playwright');
|
const playwrightObject = await connection.waitForObjectWithKnownName('playwright');
|
||||||
playwrightObject.toImpl = toImpl;
|
playwrightObject.toImpl = toImpl;
|
||||||
await test(playwrightObject);
|
await test(playwrightObject);
|
||||||
|
|
||||||
if (spawnedProcess) {
|
if (spawnedProcess) {
|
||||||
const exited = new Promise(f => spawnedProcess.once('exit', f));
|
spawnedProcess.removeListener('exit', onExit);
|
||||||
expectExit = true;
|
spawnedProcess.stdin.destroy();
|
||||||
spawnedProcess.kill();
|
spawnedProcess.stdout.destroy();
|
||||||
await exited;
|
spawnedProcess.stderr.destroy();
|
||||||
}
|
}
|
||||||
return;
|
} else {
|
||||||
|
playwright.toImpl = x => x;
|
||||||
|
await test(playwright);
|
||||||
}
|
}
|
||||||
playwright.toImpl = x => x;
|
|
||||||
await test(playwright);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
global.registerFixture('toImpl', async ({playwright}, test) => {
|
global.registerFixture('toImpl', async ({playwright}, test) => {
|
||||||
|
|
@ -123,7 +122,7 @@ module.exports = function registerFixtures(global) {
|
||||||
global.registerWorkerFixture('browserType', async ({playwright}, test) => {
|
global.registerWorkerFixture('browserType', async ({playwright}, test) => {
|
||||||
await test(playwright[process.env.BROWSER || 'chromium']);
|
await test(playwright[process.env.BROWSER || 'chromium']);
|
||||||
});
|
});
|
||||||
|
|
||||||
global.registerWorkerFixture('browser', async ({browserType, defaultBrowserOptions}, test) => {
|
global.registerWorkerFixture('browser', async ({browserType, defaultBrowserOptions}, test) => {
|
||||||
const browser = await browserType.launch(defaultBrowserOptions);
|
const browser = await browserType.launch(defaultBrowserOptions);
|
||||||
try {
|
try {
|
||||||
|
|
@ -136,7 +135,7 @@ module.exports = function registerFixtures(global) {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
global.registerFixture('context', async ({browser}, test) => {
|
global.registerFixture('context', async ({browser}, test) => {
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
try {
|
try {
|
||||||
|
|
@ -145,7 +144,7 @@ module.exports = function registerFixtures(global) {
|
||||||
await context.close();
|
await context.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
global.registerFixture('page', async ({context}, test) => {
|
global.registerFixture('page', async ({context}, test) => {
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
await test(page);
|
await test(page);
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ class PlaywrightEnvironment extends NodeEnvironment {
|
||||||
this.global.describe.fail = this.global.describe.skip;
|
this.global.describe.fail = this.global.describe.skip;
|
||||||
|
|
||||||
const itSkip = this.global.it.skip;
|
const itSkip = this.global.it.skip;
|
||||||
|
itSkip.slow = () => itSkip;
|
||||||
this.global.it.skip = (...args) => {
|
this.global.it.skip = (...args) => {
|
||||||
if (args.length = 1)
|
if (args.length = 1)
|
||||||
return args[0] ? itSkip : this.global.it;
|
return args[0] ? itSkip : this.global.it;
|
||||||
|
|
@ -199,7 +200,7 @@ class FixturePool {
|
||||||
await this.setupFixture(name);
|
await this.setupFixture(name);
|
||||||
const params = {};
|
const params = {};
|
||||||
for (const n of names)
|
for (const n of names)
|
||||||
params[n] = this.instances.get(n).value;
|
params[n] = this.instances.get(n).value;
|
||||||
return fn(params);
|
return fn(params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -301,7 +301,7 @@ describe('browserType.connect', function() {
|
||||||
await browserServer._checkLeaks();
|
await browserServer._checkLeaks();
|
||||||
await browserServer.close();
|
await browserServer.close();
|
||||||
});
|
});
|
||||||
it.skip(USES_HOOKS).slow().fail(CHROMIUM && WIN)('should handle exceptions during connect', async({browserType, defaultBrowserOptions, server}) => {
|
it.fail(USES_HOOKS || (CHROMIUM && WIN)).slow()('should handle exceptions during connect', async({browserType, defaultBrowserOptions, server}) => {
|
||||||
const browserServer = await browserType.launchServer(defaultBrowserOptions);
|
const browserServer = await browserType.launchServer(defaultBrowserOptions);
|
||||||
const __testHookBeforeCreateBrowser = () => { throw new Error('Dummy') };
|
const __testHookBeforeCreateBrowser = () => { throw new Error('Dummy') };
|
||||||
const error = await browserType.connect({ wsEndpoint: browserServer.wsEndpoint(), __testHookBeforeCreateBrowser }).catch(e => e);
|
const error = await browserType.connect({ wsEndpoint: browserServer.wsEndpoint(), __testHookBeforeCreateBrowser }).catch(e => e);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue