chore: replace sigint handler per browser with a single one (#23317)
Otherwise, multiple sigint handlers (one from each browser) would try to `process.exit(130)` each.
This commit is contained in:
parent
13f70b6d89
commit
6db6498565
|
|
@ -18,7 +18,6 @@
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { eventsHelper } from './eventsHelper';
|
|
||||||
import { isUnderTest } from './';
|
import { isUnderTest } from './';
|
||||||
import { removeFolders } from './fileUtils';
|
import { removeFolders } from './fileUtils';
|
||||||
|
|
||||||
|
|
@ -51,16 +50,63 @@ type LaunchResult = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gracefullyCloseSet = new Set<() => Promise<void>>();
|
export const gracefullyCloseSet = new Set<() => Promise<void>>();
|
||||||
|
const killSet = new Set<() => void>();
|
||||||
|
|
||||||
export async function gracefullyCloseAll() {
|
export async function gracefullyCloseAll() {
|
||||||
await Promise.all(Array.from(gracefullyCloseSet).map(gracefullyClose => gracefullyClose().catch(e => {})));
|
await Promise.all(Array.from(gracefullyCloseSet).map(gracefullyClose => gracefullyClose().catch(e => {})));
|
||||||
}
|
}
|
||||||
|
|
||||||
// We currently spawn a process per page when recording video in Chromium.
|
function exitHandler() {
|
||||||
// This triggers "too many listeners" on the process object once you have more than 10 pages open.
|
for (const kill of killSet)
|
||||||
const maxListeners = process.getMaxListeners();
|
kill();
|
||||||
if (maxListeners !== 0)
|
}
|
||||||
process.setMaxListeners(Math.max(maxListeners || 0, 100));
|
|
||||||
|
let sigintHandlerCalled = false;
|
||||||
|
function sigintHandler() {
|
||||||
|
const exitWithCode130 = () => {
|
||||||
|
// Give tests a chance to see that launched process did exit and dispatch any async calls.
|
||||||
|
if (isUnderTest())
|
||||||
|
setTimeout(() => process.exit(130), 1000);
|
||||||
|
else
|
||||||
|
process.exit(130);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sigintHandlerCalled) {
|
||||||
|
// Resort to default handler from this point on, just in case we hang/stall.
|
||||||
|
process.off('SIGINT', sigintHandler);
|
||||||
|
|
||||||
|
// Upon second Ctrl+C, immediately kill browsers and exit.
|
||||||
|
// This prevents hanging in the case where closing the browser takes a lot of time or is buggy.
|
||||||
|
for (const kill of killSet)
|
||||||
|
kill();
|
||||||
|
exitWithCode130();
|
||||||
|
} else {
|
||||||
|
sigintHandlerCalled = true;
|
||||||
|
gracefullyCloseAll().then(() => exitWithCode130());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sigtermHandler() {
|
||||||
|
gracefullyCloseAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sighupHandler() {
|
||||||
|
gracefullyCloseAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
const installedHandlers = new Set<'exit' | 'SIGINT' | 'SIGTERM' | 'SIGHUP'>();
|
||||||
|
const processHandlers = {
|
||||||
|
exit: exitHandler,
|
||||||
|
SIGINT: sigintHandler,
|
||||||
|
SIGTERM: sigtermHandler,
|
||||||
|
SIGHUP: sighupHandler,
|
||||||
|
};
|
||||||
|
function addProcessHandlerIfNeeded(name: 'exit' | 'SIGINT' | 'SIGTERM' | 'SIGHUP') {
|
||||||
|
if (!installedHandlers.has(name)) {
|
||||||
|
installedHandlers.add(name);
|
||||||
|
process.on(name, processHandlers[name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
|
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
|
||||||
const stdio: ('ignore' | 'pipe')[] = options.stdio === 'pipe' ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'];
|
const stdio: ('ignore' | 'pipe')[] = options.stdio === 'pipe' ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'];
|
||||||
|
|
@ -116,34 +162,25 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
spawnedProcess.once('exit', (exitCode, signal) => {
|
spawnedProcess.once('exit', (exitCode, signal) => {
|
||||||
options.log(`[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`);
|
options.log(`[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`);
|
||||||
processClosed = true;
|
processClosed = true;
|
||||||
eventsHelper.removeEventListeners(listeners);
|
|
||||||
gracefullyCloseSet.delete(gracefullyClose);
|
gracefullyCloseSet.delete(gracefullyClose);
|
||||||
|
killSet.delete(killProcessAndCleanup);
|
||||||
options.onExit(exitCode, signal);
|
options.onExit(exitCode, signal);
|
||||||
// Cleanup as process exits.
|
// Cleanup as process exits.
|
||||||
cleanup().then(fulfillCleanup);
|
cleanup().then(fulfillCleanup);
|
||||||
});
|
});
|
||||||
|
|
||||||
const listeners = [eventsHelper.addEventListener(process, 'exit', killProcessAndCleanup)];
|
addProcessHandlerIfNeeded('exit');
|
||||||
if (options.handleSIGINT) {
|
if (options.handleSIGINT)
|
||||||
listeners.push(eventsHelper.addEventListener(process, 'SIGINT', () => {
|
addProcessHandlerIfNeeded('SIGINT');
|
||||||
gracefullyClose().then(() => {
|
|
||||||
// Give tests a chance to dispatch any async calls.
|
|
||||||
if (isUnderTest())
|
|
||||||
setTimeout(() => process.exit(130), 0);
|
|
||||||
else
|
|
||||||
process.exit(130);
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (options.handleSIGTERM)
|
if (options.handleSIGTERM)
|
||||||
listeners.push(eventsHelper.addEventListener(process, 'SIGTERM', gracefullyClose));
|
addProcessHandlerIfNeeded('SIGTERM');
|
||||||
if (options.handleSIGHUP)
|
if (options.handleSIGHUP)
|
||||||
listeners.push(eventsHelper.addEventListener(process, 'SIGHUP', gracefullyClose));
|
addProcessHandlerIfNeeded('SIGHUP');
|
||||||
gracefullyCloseSet.add(gracefullyClose);
|
gracefullyCloseSet.add(gracefullyClose);
|
||||||
|
killSet.add(killProcessAndCleanup);
|
||||||
|
|
||||||
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.
|
||||||
|
|
@ -161,10 +198,11 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
options.log(`[pid=${spawnedProcess.pid}] <gracefully close end>`);
|
options.log(`[pid=${spawnedProcess.pid}] <gracefully close end>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method has to be sync to be used as 'exit' event handler.
|
// This method has to be sync to be used in the 'exit' event handler.
|
||||||
function killProcess() {
|
function killProcess() {
|
||||||
|
gracefullyCloseSet.delete(gracefullyClose);
|
||||||
|
killSet.delete(killProcessAndCleanup);
|
||||||
options.log(`[pid=${spawnedProcess.pid}] <kill>`);
|
options.log(`[pid=${spawnedProcess.pid}] <kill>`);
|
||||||
eventsHelper.removeEventListeners(listeners);
|
|
||||||
if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) {
|
if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) {
|
||||||
options.log(`[pid=${spawnedProcess.pid}] <will force kill>`);
|
options.log(`[pid=${spawnedProcess.pid}] <will force kill>`);
|
||||||
// Force kill the browser.
|
// Force kill the browser.
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ export class RemoteServer implements PlaywrightServer {
|
||||||
};
|
};
|
||||||
this._process = childProcess({
|
this._process = childProcess({
|
||||||
command: ['node', path.join(__dirname, 'remote-server-impl.js'), JSON.stringify(options)],
|
command: ['node', path.join(__dirname, 'remote-server-impl.js'), JSON.stringify(options)],
|
||||||
|
env: { ...process.env, PWTEST_UNDER_TEST: '1' },
|
||||||
});
|
});
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,8 @@ test.describe('signals', () => {
|
||||||
expect(await remoteServer.childExitCode()).toBe(0);
|
expect(await remoteServer.childExitCode()).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should kill the browser on SIGTERM + SIGINT', async ({ startRemoteServer, server }) => {
|
test('should kill the browser on SIGTERM + SIGINT', async ({ startRemoteServer, server, isMac, browserName }) => {
|
||||||
|
test.fixme(isMac && browserName === 'webkit' && parseInt(os.release(), 10) >= 22, 'https://github.com/microsoft/playwright/issues/22226');
|
||||||
const remoteServer = await startRemoteServer('launchServer', { stallOnClose: true, url: server.EMPTY_PAGE });
|
const remoteServer = await startRemoteServer('launchServer', { stallOnClose: true, url: server.EMPTY_PAGE });
|
||||||
process.kill(remoteServer.child().pid, 'SIGTERM');
|
process.kill(remoteServer.child().pid, 'SIGTERM');
|
||||||
await remoteServer.out('stalled');
|
await remoteServer.out('stalled');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue