refactor: misc refactoring around browsers debugging and stability (#9979)

This commit is contained in:
Alex Rudenko 2023-04-06 11:14:58 +02:00 committed by GitHub
parent 0b4a2635f5
commit c874a81445
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 186 additions and 117 deletions

View File

@ -399,11 +399,6 @@ jobs:
run: npm ci run: npm ci
env: env:
PUPPETEER_SKIP_DOWNLOAD: true PUPPETEER_SKIP_DOWNLOAD: true
- name: Setup cache for browser binaries
uses: actions/cache@v3
with:
path: packages/browsers/test/cache
key: browsers-${{ hashFiles('packages/browsers/tools/downloadTestBrowsers.mjs') }}-${{ hashFiles('packages/browsers/test/src/versions.ts') }}
- name: Run tests - name: Run tests
run: npm run test --workspace @puppeteer/browsers run: npm run test --workspace @puppeteer/browsers

View File

@ -60,7 +60,7 @@
] ]
}, },
"test": { "test": {
"command": "node tools/downloadTestBrowsers.mjs && mocha", "command": "node tools/downloadTestBrowsers.mjs && cross-env DEBUG=puppeteer:* mocha",
"files": [ "files": [
".mocharc.cjs" ".mocharc.cjs"
], ],

View File

@ -62,7 +62,7 @@ export class Cache {
force: true, force: true,
recursive: true, recursive: true,
maxRetries: 10, maxRetries: 10,
retryDelay: 200, retryDelay: 500,
}); });
} }
} }

View File

@ -33,6 +33,22 @@ import {downloadFile, headHttpRequest} from './httpUtil.js';
const debugFetch = debug('puppeteer:browsers:fetcher'); const debugFetch = debug('puppeteer:browsers:fetcher');
const times = new Map<string, [number, number]>();
function debugTime(label: string) {
times.set(label, process.hrtime());
}
function debugTimeEnd(label: string) {
const end = process.hrtime();
const start = times.get(label);
if (!start) {
return;
}
const duration =
end[0] * 1000 + end[1] / 1e6 - (start[0] * 1000 + start[1] / 1e6); // calculate duration in milliseconds
debugFetch(`Duration for ${label}: ${duration}ms`);
}
/** /**
* @public * @public
*/ */
@ -121,7 +137,9 @@ export async function fetch(options: Options): Promise<InstalledBrowser> {
}; };
} }
debugFetch(`Downloading binary from ${url}`); debugFetch(`Downloading binary from ${url}`);
debugTime('download');
await downloadFile(url, archivePath, options.downloadProgressCallback); await downloadFile(url, archivePath, options.downloadProgressCallback);
debugTimeEnd('download');
return { return {
path: archivePath, path: archivePath,
browser: options.browser, browser: options.browser,
@ -145,9 +163,22 @@ export async function fetch(options: Options): Promise<InstalledBrowser> {
} }
try { try {
debugFetch(`Downloading binary from ${url}`); debugFetch(`Downloading binary from ${url}`);
await downloadFile(url, archivePath, options.downloadProgressCallback); try {
debugTime('download');
await downloadFile(url, archivePath, options.downloadProgressCallback);
} finally {
debugTimeEnd('download');
}
debugFetch(`Installing ${archivePath} to ${outputPath}`); debugFetch(`Installing ${archivePath} to ${outputPath}`);
await unpackArchive(archivePath, outputPath); try {
debugTime('extract');
await unpackArchive(archivePath, outputPath);
} finally {
debugTimeEnd('extract');
}
} catch (err) {
debugFetch(`Error during installation`, err);
} finally { } finally {
if (existsSync(archivePath)) { if (existsSync(archivePath)) {
await unlink(archivePath); await unlink(archivePath);

View File

@ -160,28 +160,34 @@ class Process {
opts.handleSIGINT ??= true; opts.handleSIGINT ??= true;
opts.handleSIGTERM ??= true; opts.handleSIGTERM ??= true;
opts.handleSIGHUP ??= true; opts.handleSIGHUP ??= true;
opts.detached ??= true; // 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
opts.detached ??= process.platform !== 'win32';
const stdio = this.#configureStdio({ const stdio = this.#configureStdio({
pipe: opts.pipe, pipe: opts.pipe,
dumpio: opts.dumpio, dumpio: opts.dumpio,
}); });
debugLaunch(`Launching ${this.#executablePath} ${this.#args.join(' ')}`); debugLaunch(`Launching ${this.#executablePath} ${this.#args.join(' ')}`, {
detached: opts.detached,
env: opts.env,
stdio,
});
this.#browserProcess = childProcess.spawn( this.#browserProcess = childProcess.spawn(
this.#executablePath, this.#executablePath,
this.#args, this.#args,
{ {
// 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: opts.detached, detached: opts.detached,
env: opts.env, env: opts.env,
stdio, stdio,
} }
); );
debugLaunch(`Launched ${this.#browserProcess.pid}`);
if (opts.dumpio) { if (opts.dumpio) {
this.#browserProcess.stderr?.pipe(process.stderr); this.#browserProcess.stderr?.pipe(process.stderr);
this.#browserProcess.stdout?.pipe(process.stdout); this.#browserProcess.stdout?.pipe(process.stdout);
@ -201,6 +207,7 @@ class Process {
} }
this.#browserProcessExiting = new Promise((resolve, reject) => { this.#browserProcessExiting = new Promise((resolve, reject) => {
this.#browserProcess.once('exit', async () => { this.#browserProcess.once('exit', async () => {
debugLaunch(`Browser process ${this.#browserProcess.pid} onExit`);
this.#clearListeners(); this.#clearListeners();
this.#exited = true; this.#exited = true;
try { try {
@ -281,6 +288,7 @@ class Process {
} }
kill(): void { kill(): void {
debugLaunch(`Trying to kill ${this.#browserProcess.pid}`);
// 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
// `proc.kill` would error, as the `pid` to-be-killed can not be found. // `proc.kill` would error, as the `pid` to-be-killed can not be found.
@ -290,12 +298,17 @@ class Process {
pidExists(this.#browserProcess.pid) pidExists(this.#browserProcess.pid)
) { ) {
try { try {
debugLaunch(`Browser process ${this.#browserProcess.pid} exists`);
if (process.platform === 'win32') { if (process.platform === 'win32') {
try { try {
childProcess.execSync( childProcess.execSync(
`taskkill /pid ${this.#browserProcess.pid} /T /F` `taskkill /pid ${this.#browserProcess.pid} /T /F`
); );
} catch (error) { } catch (error) {
debugLaunch(
`Killing ${this.#browserProcess.pid} using taskkill failed`,
error
);
// taskkill can fail to kill the process e.g. due to missing permissions. // taskkill can fail to kill the process e.g. due to missing permissions.
// Let's kill the process via Node API. This delays killing of all child // Let's kill the process via Node API. This delays killing of all child
// processes of `this.proc` until the main Node.js process dies. // processes of `this.proc` until the main Node.js process dies.
@ -309,6 +322,10 @@ class Process {
try { try {
process.kill(processGroupId, 'SIGKILL'); process.kill(processGroupId, 'SIGKILL');
} catch (error) { } catch (error) {
debugLaunch(
`Killing ${this.#browserProcess.pid} using process.kill failed`,
error
);
// Killing the process group can fail due e.g. to missing permissions. // Killing the process group can fail due e.g. to missing permissions.
// Let's kill the process via Node API. This delays killing of all child // Let's kill the process via Node API. This delays killing of all child
// processes of `this.proc` until the main Node.js process dies. // processes of `this.proc` until the main Node.js process dies.

View File

@ -176,7 +176,7 @@ describe('Chrome fetch', () => {
}); });
it('can fetch via a proxy', async function () { it('can fetch via a proxy', async function () {
this.timeout(60000); this.timeout(120000);
const expectedOutputPath = path.join( const expectedOutputPath = path.join(
tmpDir, tmpDir,
'chrome', 'chrome',

View File

@ -26,9 +26,8 @@ import {
fetch, fetch,
Browser, Browser,
BrowserPlatform, BrowserPlatform,
Cache,
} from '../../../lib/cjs/main.js'; } from '../../../lib/cjs/main.js';
import {getServerUrl, setupTestServer} from '../utils.js'; import {getServerUrl, setupTestServer, clearCache} from '../utils.js';
import {testChromeBuildId} from '../versions.js'; import {testChromeBuildId} from '../versions.js';
describe('Chrome', () => { describe('Chrome', () => {
@ -64,9 +63,44 @@ describe('Chrome', () => {
}); });
afterEach(() => { afterEach(() => {
new Cache(tmpDir).clear(); clearCache(tmpDir);
}); });
function getArgs() {
return [
'--allow-pre-commit-input',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-component-update',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
'--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints,DialMediaRouteProvider',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-sync',
'--enable-automation',
'--enable-features=NetworkServiceInProcess2',
'--export-tagged-pdf',
'--force-color-profile=srgb',
'--headless=new',
'--metrics-recording-only',
'--no-first-run',
'--password-store=basic',
'--remote-debugging-port=9222',
'--use-mock-keychain',
`--user-data-dir=${path.join(tmpDir, 'profile')}`,
'about:blank',
];
}
it('should launch a Chrome browser', async () => { it('should launch a Chrome browser', async () => {
const executablePath = computeExecutablePath({ const executablePath = computeExecutablePath({
cacheDir: tmpDir, cacheDir: tmpDir,
@ -75,12 +109,7 @@ describe('Chrome', () => {
}); });
const process = launch({ const process = launch({
executablePath, executablePath,
args: [ args: getArgs(),
'--headless=new',
'--use-mock-keychain',
'--disable-features=DialMediaRouteProvider',
`--user-data-dir=${path.join(tmpDir, 'profile')}`,
],
}); });
await process.close(); await process.close();
}); });
@ -93,38 +122,7 @@ describe('Chrome', () => {
}); });
const process = launch({ const process = launch({
executablePath, executablePath,
args: [ args: getArgs(),
'--allow-pre-commit-input',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-component-update',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
'--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints,DialMediaRouteProvider',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-sync',
'--enable-automation',
'--enable-features=NetworkServiceInProcess2',
'--export-tagged-pdf',
'--force-color-profile=srgb',
'--headless=new',
'--metrics-recording-only',
'--no-first-run',
'--password-store=basic',
'--remote-debugging-port=9222',
'--use-mock-keychain',
`--user-data-dir=${path.join(tmpDir, 'profile')}`,
'about:blank',
],
}); });
const url = await process.waitForLineOutput(CDP_WEBSOCKET_ENDPOINT_REGEX); const url = await process.waitForLineOutput(CDP_WEBSOCKET_ENDPOINT_REGEX);
await process.close(); await process.close();

View File

@ -26,9 +26,8 @@ import {
fetch, fetch,
Browser, Browser,
BrowserPlatform, BrowserPlatform,
Cache,
} from '../../../lib/cjs/main.js'; } from '../../../lib/cjs/main.js';
import {getServerUrl, setupTestServer} from '../utils.js'; import {getServerUrl, setupTestServer, clearCache} from '../utils.js';
import {testChromiumBuildId} from '../versions.js'; import {testChromiumBuildId} from '../versions.js';
describe('Chromium', () => { describe('Chromium', () => {
@ -47,7 +46,7 @@ describe('Chromium', () => {
describe('launcher', function () { describe('launcher', function () {
setupTestServer(); setupTestServer();
this.timeout(60000); this.timeout(120000);
let tmpDir = '/tmp/puppeteer-browsers-test'; let tmpDir = '/tmp/puppeteer-browsers-test';
@ -64,10 +63,45 @@ describe('Chromium', () => {
}); });
afterEach(() => { afterEach(() => {
new Cache(tmpDir).clear(); clearCache(tmpDir);
}); });
it('should launch a Chrome browser', async () => { function getArgs() {
return [
'--allow-pre-commit-input',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-component-update',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
'--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints,DialMediaRouteProvider',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-sync',
'--enable-automation',
'--enable-features=NetworkServiceInProcess2',
'--export-tagged-pdf',
'--force-color-profile=srgb',
'--headless=new',
'--metrics-recording-only',
'--no-first-run',
'--password-store=basic',
'--remote-debugging-port=9222',
'--use-mock-keychain',
`--user-data-dir=${path.join(tmpDir, 'profile')}`,
'about:blank',
];
}
it('should launch a Chromium browser', async () => {
const executablePath = computeExecutablePath({ const executablePath = computeExecutablePath({
cacheDir: tmpDir, cacheDir: tmpDir,
browser: Browser.CHROMIUM, browser: Browser.CHROMIUM,
@ -75,12 +109,7 @@ describe('Chromium', () => {
}); });
const process = launch({ const process = launch({
executablePath, executablePath,
args: [ args: getArgs(),
'--headless=new',
'--use-mock-keychain',
'--disable-features=DialMediaRouteProvider',
`--user-data-dir=${path.join(tmpDir, 'profile')}`,
],
}); });
await process.close(); await process.close();
}); });
@ -93,13 +122,7 @@ describe('Chromium', () => {
}); });
const process = launch({ const process = launch({
executablePath, executablePath,
args: [ args: getArgs(),
'--headless=new',
'--use-mock-keychain',
'--disable-features=DialMediaRouteProvider',
'--remote-debugging-port=9222',
`--user-data-dir=${path.join(tmpDir, 'profile')}`,
],
}); });
const url = await process.waitForLineOutput(CDP_WEBSOCKET_ENDPOINT_REGEX); const url = await process.waitForLineOutput(CDP_WEBSOCKET_ENDPOINT_REGEX);
await process.close(); await process.close();

View File

@ -19,8 +19,8 @@ import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import {fetch, Browser, BrowserPlatform, Cache} from '../../../lib/cjs/main.js'; import {fetch, Browser, BrowserPlatform} from '../../../lib/cjs/main.js';
import {setupTestServer, getServerUrl} from '../utils.js'; import {setupTestServer, getServerUrl, clearCache} from '../utils.js';
import {testFirefoxBuildId} from '../versions.js'; import {testFirefoxBuildId} from '../versions.js';
/** /**
@ -37,7 +37,7 @@ describe('Firefox fetch', () => {
}); });
afterEach(() => { afterEach(() => {
new Cache(tmpDir).clear(); clearCache(tmpDir);
}); });
it('should download a buildId that is a bzip2 archive', async function () { it('should download a buildId that is a bzip2 archive', async function () {

View File

@ -15,7 +15,6 @@
*/ */
import assert from 'assert'; import assert from 'assert';
import {execSync} from 'child_process';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
@ -26,10 +25,9 @@ import {
fetch, fetch,
Browser, Browser,
BrowserPlatform, BrowserPlatform,
Cache,
createProfile, createProfile,
} from '../../../lib/cjs/main.js'; } from '../../../lib/cjs/main.js';
import {setupTestServer, getServerUrl} from '../utils.js'; import {setupTestServer, getServerUrl, clearCache} from '../utils.js';
import {testFirefoxBuildId} from '../versions.js'; import {testFirefoxBuildId} from '../versions.js';
describe('Firefox', () => { describe('Firefox', () => {
@ -65,14 +63,7 @@ describe('Firefox', () => {
}); });
afterEach(() => { afterEach(() => {
try { clearCache(tmpDir);
new Cache(tmpDir).clear();
} catch (err) {
if (os.platform() === 'win32') {
console.log(execSync('tasklist').toString('utf-8'));
}
throw err;
}
}); });
it('should launch a Firefox browser', async () => { it('should launch a Firefox browser', async () => {

View File

@ -14,12 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
import {execSync} from 'child_process';
import os from 'os';
import path from 'path'; import path from 'path';
import * as readline from 'readline'; import * as readline from 'readline';
import {Writable, Readable} from 'stream'; import {Writable, Readable} from 'stream';
import {TestServer} from '@pptr/testserver'; import {TestServer} from '@pptr/testserver';
import {isErrorLike} from '../../lib/cjs/launcher.js';
import {Cache} from '../../lib/cjs/main.js';
export function createMockedReadlineInterface( export function createMockedReadlineInterface(
input: string input: string
): readline.Interface { ): readline.Interface {
@ -62,3 +67,19 @@ export function setupTestServer(): void {
export function getServerUrl(): string { export function getServerUrl(): string {
return `http://localhost:${state.server!.port}`; return `http://localhost:${state.server!.port}`;
} }
export function clearCache(tmpDir: string): void {
try {
new Cache(tmpDir).clear();
} catch (err) {
if (os.platform() === 'win32') {
console.log(execSync('tasklist').toString('utf-8'));
// Sometimes on Windows the folder cannot be removed due to unknown reasons.
// We suppress the error to avoud flakiness.
if (isErrorLike(err) && err.message.includes('EBUSY')) {
return;
}
}
throw err;
}
}

View File

@ -38,7 +38,6 @@ function getBrowser(str) {
} }
const cacheDir = path.normalize(path.join('.', 'test', 'cache')); const cacheDir = path.normalize(path.join('.', 'test', 'cache'));
const promises = [];
for (const version of Object.keys(versions)) { for (const version of Object.keys(versions)) {
const browser = getBrowser(version); const browser = getBrowser(version);
@ -50,37 +49,31 @@ for (const version of Object.keys(versions)) {
const buildId = versions[version]; const buildId = versions[version];
for (const platform of Object.values(BrowserPlatform)) { for (const platform of Object.values(BrowserPlatform)) {
promises.push( const targetPath = path.join(
(async function download(buildId, platform) { cacheDir,
const targetPath = path.join( 'server',
cacheDir, ...downloadPaths[browser](platform, buildId)
'server',
...downloadPaths[browser](platform, buildId)
);
if (fs.existsSync(targetPath)) {
return;
}
const result = await fetch({
browser,
buildId,
platform,
cacheDir: path.join(cacheDir, 'tmp'),
install: false,
});
fs.mkdirSync(path.dirname(targetPath), {
recursive: true,
});
fs.copyFileSync(result.path, targetPath);
})(buildId, platform)
); );
if (fs.existsSync(targetPath)) {
continue;
}
const result = await fetch({
browser,
buildId,
platform,
cacheDir: path.join(cacheDir, 'tmp'),
install: false,
});
fs.mkdirSync(path.dirname(targetPath), {
recursive: true,
});
fs.copyFileSync(result.path, targetPath);
} }
} }
await Promise.all(promises);
fs.rmSync(path.join(cacheDir, 'tmp'), { fs.rmSync(path.join(cacheDir, 'tmp'), {
recursive: true, recursive: true,
force: true, force: true,