Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: extract BrowserRunner into its own module #5850

Merged
merged 2 commits into from May 12, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
258 changes: 7 additions & 251 deletions src/Launcher.ts
Expand Up @@ -19,26 +19,23 @@ import * as http from 'http';
import * as https from 'https';
import * as URL from 'url';
import * as fs from 'fs';
import * as readline from 'readline';
import * as debug from 'debug';

import * as removeFolder from 'rimraf';
import * as childProcess from 'child_process';

import { BrowserFetcher } from './BrowserFetcher';
import { Connection } from './Connection';
import { Browser } from './Browser';
import { helper, assert, debugError } from './helper';
import { TimeoutError } from './Errors';
import type { ConnectionTransport } from './ConnectionTransport';
import { WebSocketTransport } from './WebSocketTransport';
import { PipeTransport } from './PipeTransport';
import type { Viewport } from './PuppeteerViewport';
import { BrowserRunner } from './launcher/BrowserRunner';

const mkdtempAsync = helper.promisify(fs.mkdtemp);
const removeFolderAsync = helper.promisify(removeFolder);
const writeFileAsync = helper.promisify(fs.writeFile);
const debugLauncher = debug('puppeteer:launcher');

import type {
ChromeArgOptions,
LaunchOptions,
BrowserOptions,
} from './launcher/LaunchOptions';

export interface ProductLauncher {
launch(object);
Expand All @@ -48,186 +45,6 @@ export interface ProductLauncher {
product: string;
}

export interface ChromeArgOptions {
headless?: boolean;
args?: string[];
userDataDir?: string;
devtools?: boolean;
}

export interface LaunchOptions {
executablePath?: string;
ignoreDefaultArgs?: boolean | string[];
handleSIGINT?: boolean;
handleSIGTERM?: boolean;
handleSIGHUP?: boolean;
timeout?: number;
dumpio?: boolean;
env?: Record<string, string | undefined>;
pipe?: boolean;
}

export interface BrowserOptions {
ignoreHTTPSErrors?: boolean;
defaultViewport?: Viewport;
slowMo?: number;
}

class BrowserRunner {
_executablePath: string;
_processArguments: string[];
_tempDirectory?: string;

proc = null;
connection = null;

_closed = true;
_listeners = [];
_processClosing: Promise<void>;

constructor(
executablePath: string,
processArguments: string[],
tempDirectory?: string
) {
this._executablePath = executablePath;
this._processArguments = processArguments;
this._tempDirectory = tempDirectory;
}

start(options: LaunchOptions = {}): void {
const {
handleSIGINT,
handleSIGTERM,
handleSIGHUP,
dumpio,
env,
pipe,
} = options;
let stdio: Array<'ignore' | 'pipe'> = ['pipe', 'pipe', 'pipe'];
if (pipe) {
if (dumpio) stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
else stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
}
assert(!this.proc, 'This process has previously been started.');
debugLauncher(
`Calling ${this._executablePath} ${this._processArguments.join(' ')}`
);
this.proc = childProcess.spawn(
this._executablePath,
this._processArguments,
{
// On non-windows platforms, `detached: true` makes child process a leader of a new
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
detached: process.platform !== 'win32',
env,
stdio,
}
);
if (dumpio) {
this.proc.stderr.pipe(process.stderr);
this.proc.stdout.pipe(process.stdout);
}
this._closed = false;
this._processClosing = new Promise((fulfill) => {
this.proc.once('exit', () => {
this._closed = true;
// Cleanup as processes exit.
if (this._tempDirectory) {
removeFolderAsync(this._tempDirectory)
.then(() => fulfill())
.catch((error) => console.error(error));
} else {
fulfill();
}
});
});
this._listeners = [
helper.addEventListener(process, 'exit', this.kill.bind(this)),
];
if (handleSIGINT)
this._listeners.push(
helper.addEventListener(process, 'SIGINT', () => {
this.kill();
process.exit(130);
})
);
if (handleSIGTERM)
this._listeners.push(
helper.addEventListener(process, 'SIGTERM', this.close.bind(this))
);
if (handleSIGHUP)
this._listeners.push(
helper.addEventListener(process, 'SIGHUP', this.close.bind(this))
);
}

close(): Promise<void> {
if (this._closed) return Promise.resolve();
helper.removeEventListeners(this._listeners);
if (this._tempDirectory) {
this.kill();
} else if (this.connection) {
// Attempt to close the browser gracefully
this.connection.send('Browser.close').catch((error) => {
debugError(error);
this.kill();
});
}
return this._processClosing;
}

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.
try {
removeFolder.sync(this._tempDirectory);
} catch (error) {}
}

/**
* @param {!({usePipe?: boolean, timeout: number, slowMo: number, preferredRevision: string})} options
*
* @return {!Promise<!Connection>}
*/
async setupConnection(options: {
usePipe?: boolean;
timeout: number;
slowMo: number;
preferredRevision: string;
}): Promise<Connection> {
const { usePipe, timeout, slowMo, preferredRevision } = options;
if (!usePipe) {
const browserWSEndpoint = await waitForWSEndpoint(
this.proc,
timeout,
preferredRevision
);
const transport = await WebSocketTransport.create(browserWSEndpoint);
this.connection = new Connection(browserWSEndpoint, transport, slowMo);
} else {
// stdio was assigned during start(), and the 'pipe' option there adds the 4th and 5th items to stdio array
const { 3: pipeWrite, 4: pipeRead } = this.proc.stdio;
const transport = new PipeTransport(
pipeWrite as NodeJS.WritableStream,
pipeRead as NodeJS.ReadableStream
);
this.connection = new Connection('', transport, slowMo);
}
return this.connection;
}
}

class ChromeLauncher implements ProductLauncher {
_projectRoot: string;
_preferredRevision: string;
Expand Down Expand Up @@ -871,67 +688,6 @@ class FirefoxLauncher implements ProductLauncher {
}
}

function waitForWSEndpoint(
browserProcess: childProcess.ChildProcess,
timeout: number,
preferredRevision: string
): Promise<string> {
return new Promise((resolve, reject) => {
const rl = readline.createInterface({ input: browserProcess.stderr });
let stderr = '';
const listeners = [
helper.addEventListener(rl, 'line', onLine),
helper.addEventListener(rl, 'close', () => onClose()),
helper.addEventListener(browserProcess, 'exit', () => onClose()),
helper.addEventListener(browserProcess, 'error', (error) =>
onClose(error)
),
];
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;

/**
* @param {!Error=} error
*/
function onClose(error?: Error): void {
cleanup();
reject(
new Error(
[
'Failed to launch the browser process!' +
(error ? ' ' + error.message : ''),
stderr,
'',
'TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md',
'',
].join('\n')
)
);
}

function onTimeout(): void {
cleanup();
reject(
new TimeoutError(
`Timed out after ${timeout} ms while trying to connect to the browser! Only Chrome at revision r${preferredRevision} is guaranteed to work.`
)
);
}

function onLine(line: string): void {
stderr += line + '\n';
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match) return;
cleanup();
resolve(match[1]);
}

function cleanup(): void {
if (timeoutId) clearTimeout(timeoutId);
helper.removeEventListeners(listeners);
}
});
}

function getWSEndpoint(browserURL: string): Promise<string> {
let resolve, reject;
const promise = new Promise<string>((res, rej) => {
Expand Down
4 changes: 2 additions & 2 deletions src/Puppeteer.ts
Expand Up @@ -18,8 +18,8 @@ import type {
LaunchOptions,
ChromeArgOptions,
BrowserOptions,
ProductLauncher,
} from './Launcher';
} from './launcher/LaunchOptions';
import type { ProductLauncher } from './Launcher';
import { BrowserFetcher, BrowserFetcherOptions } from './BrowserFetcher';
import { puppeteerErrors, PuppeteerErrors } from './Errors';
import type { ConnectionTransport } from './ConnectionTransport';
Expand Down