fix: improve Ctrl + C support (#6011)

Fix child process killing when the parent process SIGINTs.

If you `ctrl + c` the Puppeteer parent process, we would sometimes not properly handle killing of the child processes. This would then leave child processes behind, with running Chromium instances. This in turn could block Puppeteer from launching again and results in
cryptic errors.

Instead of using the generic `process.kill` with the process id (which for some reason is negative the pid, which I don't get), we can kill the child process directly by calling `proc.kill`.

Fixes #5729.
Fixes #4796.
Fixes #4963.
Fixes #4333.
Fixes #1825.
This commit is contained in:
Tim van der Lippe 2020-06-15 14:02:00 +01:00 committed by GitHub
parent b659969a38
commit 03ab1c1b9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 27 additions and 13 deletions

View File

@ -1,4 +1,6 @@
module.exports = { module.exports = {
reporter: 'dot', reporter: 'dot',
// Allow `console.log`s to show up during test execution
logLevel: 'debug',
exit: !!process.env.CI, exit: !!process.env.CI,
}; };

View File

@ -28,6 +28,10 @@ import { TimeoutError } from '../Errors';
const removeFolderAsync = helper.promisify(removeFolder); const removeFolderAsync = helper.promisify(removeFolder);
const debugLauncher = debug('puppeteer:launcher'); const debugLauncher = debug('puppeteer:launcher');
const PROCESS_ERROR_EXPLANATION = `Puppeteer was unable to kill the process which ran the browser binary.
This means that, on future Puppeteer launches, Puppeteer might not be able to launch the browser.
Please check your open processes and ensure that the browser processes that Puppeteer launched have been killed.
If you think this is a bug, please report it on the Puppeteer issue tracker.`;
export class BrowserRunner { export class BrowserRunner {
private _executablePath: string; private _executablePath: string;
@ -51,7 +55,7 @@ export class BrowserRunner {
this._tempDirectory = tempDirectory; this._tempDirectory = tempDirectory;
} }
start(options: LaunchOptions = {}): void { start(options: LaunchOptions): void {
const { const {
handleSIGINT, handleSIGINT,
handleSIGTERM, handleSIGTERM,
@ -121,7 +125,6 @@ export class BrowserRunner {
close(): Promise<void> { close(): Promise<void> {
if (this._closed) return Promise.resolve(); if (this._closed) return Promise.resolve();
helper.removeEventListeners(this._listeners);
if (this._tempDirectory) { if (this._tempDirectory) {
this.kill(); this.kill();
} else if (this.connection) { } else if (this.connection) {
@ -131,24 +134,33 @@ export class BrowserRunner {
this.kill(); this.kill();
}); });
} }
// Cleanup this listener last, as that makes sure the full callback runs. If we
// perform this earlier, then the previous function calls would not happen.
helper.removeEventListeners(this._listeners);
return this._processClosing; return this._processClosing;
} }
kill(): void { kill(): void {
helper.removeEventListeners(this._listeners);
if (this.proc && this.proc.pid && !this.proc.killed && !this._closed) {
try {
if (process.platform === 'win32')
childProcess.execSync(`taskkill /pid ${this.proc.pid} /T /F`);
else process.kill(-this.proc.pid, 'SIGKILL');
} catch (error) {
// the process might have already stopped
}
}
// Attempt to remove temporary profile directory to avoid littering. // Attempt to remove temporary profile directory to avoid littering.
try { try {
removeFolder.sync(this._tempDirectory); removeFolder.sync(this._tempDirectory);
} catch (error) {} } catch (error) {}
// If the process failed to launch (for example if the browser executable path
// is invalid), then the process does not get a pid assigned. A call to
// `proc.kill` would error, as the `pid` to-be-killed can not be found.
if (this.proc && this.proc.pid && !this.proc.killed) {
try {
this.proc.kill('SIGKILL');
} catch (error) {
throw new Error(
`${PROCESS_ERROR_EXPLANATION}\nError cause: ${error.stack}`
);
}
}
// Cleanup this listener last, as that makes sure the full callback runs. If we
// perform this earlier, then the previous function calls would not happen.
helper.removeEventListeners(this._listeners);
} }
async setupConnection(options: { async setupConnection(options: {

View File

@ -71,7 +71,7 @@ try {
const defaultBrowserOptions = Object.assign( const defaultBrowserOptions = Object.assign(
{ {
handleSIGINT: false, handleSIGINT: true,
executablePath: process.env.BINARY, executablePath: process.env.BINARY,
slowMo: false, slowMo: false,
headless: isHeadless, headless: isHeadless,