fix(browsers): various fixes and improvements (#9966)

This commit is contained in:
Alex Rudenko 2023-04-04 13:58:32 +02:00 committed by GitHub
parent fe934ad092
commit f1211cbec0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 65 additions and 15 deletions

View File

@ -20,7 +20,7 @@
}, },
"wireit": { "wireit": {
"build": { "build": {
"command": "tsc -b && chmod a+x lib/cjs/main-cli.js lib/esm/main-cli.js", "command": "tsc -b && chmod a+x lib/cjs/main-cli.js lib/esm/main-cli.js && echo '{\"type\":\"module\"}' > lib/esm/package.json",
"files": [ "files": [
"src/**/*.ts", "src/**/*.ts",
"tsconfig.json" "tsconfig.json"

View File

@ -18,8 +18,9 @@ import {stdin as input, stdout as output} from 'process';
import * as readline from 'readline'; import * as readline from 'readline';
import ProgressBar from 'progress'; import ProgressBar from 'progress';
import yargs from 'yargs'; import type * as Yargs from 'yargs';
import {hideBin} from 'yargs/helpers'; import {hideBin} from 'yargs/helpers';
import yargs from 'yargs/yargs';
import { import {
resolveBuildId, resolveBuildId,
@ -69,7 +70,7 @@ export class CLI {
this.#rl = rl; this.#rl = rl;
} }
#defineBrowserParameter(yargs: yargs.Argv<unknown>): void { #defineBrowserParameter(yargs: Yargs.Argv<unknown>): void {
yargs.positional('browser', { yargs.positional('browser', {
description: description:
'Which browser to install <browser>[@<buildId|latest>]. `latest` will try to find the latest available build. `buildId` is a browser-specific identifier such as a version or a revision.', 'Which browser to install <browser>[@<buildId|latest>]. `latest` will try to find the latest available build. `buildId` is a browser-specific identifier such as a version or a revision.',
@ -83,7 +84,7 @@ export class CLI {
}); });
} }
#definePlatformParameter(yargs: yargs.Argv<unknown>): void { #definePlatformParameter(yargs: Yargs.Argv<unknown>): void {
yargs.option('platform', { yargs.option('platform', {
type: 'string', type: 'string',
desc: 'Platform that the binary needs to be compatible with.', desc: 'Platform that the binary needs to be compatible with.',
@ -92,7 +93,7 @@ export class CLI {
}); });
} }
#definePathParameter(yargs: yargs.Argv<unknown>, required = false): void { #definePathParameter(yargs: Yargs.Argv<unknown>, required = false): void {
yargs.option('path', { yargs.option('path', {
type: 'string', type: 'string',
desc: 'Path to the root folder for the browser downloads and installation. The installation folder structure is compatible with the cache structure used by Puppeteer.', desc: 'Path to the root folder for the browser downloads and installation. The installation folder structure is compatible with the cache structure used by Puppeteer.',
@ -105,7 +106,8 @@ export class CLI {
} }
async run(argv: string[]): Promise<void> { async run(argv: string[]): Promise<void> {
await yargs(hideBin(argv)) const yargsInstance = yargs(hideBin(argv));
await yargsInstance
.scriptName('@puppeteer/browsers') .scriptName('@puppeteer/browsers')
.command( .command(
'install <browser>', 'install <browser>',
@ -254,7 +256,7 @@ export class CLI {
) )
.demandCommand(1) .demandCommand(1)
.help() .help()
.wrap(Math.min(120, yargs.terminalWidth())) .wrap(Math.min(120, yargsInstance.terminalWidth()))
.parse(); .parse();
} }

View File

@ -122,11 +122,12 @@ type LaunchOptions = {
pipe?: boolean; pipe?: boolean;
dumpio?: boolean; dumpio?: boolean;
args?: string[]; args?: string[];
env?: Record<string, string>; env?: Record<string, string | undefined>;
handleSIGINT?: boolean; handleSIGINT?: boolean;
handleSIGTERM?: boolean; handleSIGTERM?: boolean;
handleSIGHUP?: boolean; handleSIGHUP?: boolean;
detached?: boolean; detached?: boolean;
onExit?: () => Promise<void>;
}; };
export function launch(opts: LaunchOptions): Process { export function launch(opts: LaunchOptions): Process {
@ -143,6 +144,11 @@ class Process {
#args: string[]; #args: string[];
#browserProcess: childProcess.ChildProcess; #browserProcess: childProcess.ChildProcess;
#exited = false; #exited = false;
// The browser process can be closed externally or from the driver process. We
// need to invoke the hooks only once though but we don't know how many times
// we will be invoked.
#hooksRan = false;
#onExitHook = async () => {};
#browserProcessExiting: Promise<void>; #browserProcessExiting: Promise<void>;
constructor(opts: LaunchOptions) { constructor(opts: LaunchOptions) {
@ -190,15 +196,36 @@ class Process {
if (opts.handleSIGHUP) { if (opts.handleSIGHUP) {
process.on('SIGHUP', this.#onDriverProcessSignal); process.on('SIGHUP', this.#onDriverProcessSignal);
} }
this.#browserProcessExiting = new Promise(resolve => { if (opts.onExit) {
this.#browserProcess.once('exit', () => { this.#onExitHook = opts.onExit;
this.#exited = true; }
this.#browserProcessExiting = new Promise((resolve, reject) => {
this.#browserProcess.once('exit', async () => {
this.#clearListeners(); this.#clearListeners();
this.#exited = true;
try {
await this.#runHooks();
} catch (err) {
reject(err);
return;
}
resolve(); resolve();
}); });
}); });
} }
async #runHooks() {
if (this.#hooksRan) {
return;
}
this.#hooksRan = true;
await this.#onExitHook();
}
get nodeProcess(): childProcess.ChildProcess {
return this.#browserProcess;
}
#configureStdio(opts: { #configureStdio(opts: {
pipe: boolean; pipe: boolean;
dumpio: boolean; dumpio: boolean;
@ -236,19 +263,24 @@ class Process {
process.exit(130); process.exit(130);
case 'SIGTERM': case 'SIGTERM':
case 'SIGHUP': case 'SIGHUP':
this.kill(); this.close();
break; break;
} }
}; };
close(): Promise<void> { async close(): Promise<void> {
await this.#runHooks();
if (this.#exited) { if (this.#exited) {
return Promise.resolve(); return this.#browserProcessExiting;
} }
this.kill(); this.kill();
return this.#browserProcessExiting; return this.#browserProcessExiting;
} }
hasClosed(): Promise<void> {
return this.#browserProcessExiting;
}
kill(): void { kill(): void {
// If the process failed to launch (for example if the browser executable path // 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 // is invalid), then the process does not get a pid assigned. A call to
@ -329,6 +361,9 @@ class Process {
error ? ' ' + error.message : '' error ? ' ' + error.message : ''
}`, }`,
stderr, stderr,
'',
'TROUBLESHOOTING: https://pptr.dev/troubleshooting',
'',
].join('\n') ].join('\n')
) )
); );
@ -337,7 +372,7 @@ class Process {
function onTimeout(): void { function onTimeout(): void {
cleanup(); cleanup();
reject( reject(
new Error( new TimeoutError(
`Timed out after ${timeout} ms while waiting for the WS endpoint URL to appear in stdout!` `Timed out after ${timeout} ms while waiting for the WS endpoint URL to appear in stdout!`
) )
); );
@ -403,3 +438,14 @@ export function isErrnoException(obj: unknown): obj is NodeJS.ErrnoException {
('errno' in obj || 'code' in obj || 'path' in obj || 'syscall' in obj) ('errno' in obj || 'code' in obj || 'path' in obj || 'syscall' in obj)
); );
} }
export class TimeoutError extends Error {
/**
* @internal
*/
constructor(message?: string) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}

View File

@ -18,6 +18,7 @@ export {
launch, launch,
computeExecutablePath, computeExecutablePath,
computeSystemExecutablePath, computeSystemExecutablePath,
TimeoutError,
CDP_WEBSOCKET_ENDPOINT_REGEX, CDP_WEBSOCKET_ENDPOINT_REGEX,
WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX, WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
} from './launcher.js'; } from './launcher.js';
@ -28,6 +29,7 @@ export {
Browser, Browser,
BrowserPlatform, BrowserPlatform,
ChromeReleaseChannel, ChromeReleaseChannel,
createProfile,
} from './browser-data/browser-data.js'; } from './browser-data/browser-data.js';
export {CLI, makeProgressCallback} from './CLI.js'; export {CLI, makeProgressCallback} from './CLI.js';
export {Cache} from './Cache.js'; export {Cache} from './Cache.js';