mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
refactor: use browsers for launchers (#9937)
This commit is contained in:
parent
817288cd90
commit
c8f6adf9f3
@ -8,15 +8,15 @@ sidebar_label: ProductLauncher.launch
|
||||
|
||||
```typescript
|
||||
class ProductLauncher {
|
||||
launch(object: PuppeteerNodeLaunchOptions): Promise<Browser>;
|
||||
launch(options?: PuppeteerNodeLaunchOptions): Promise<Browser>;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ----------------------------------------------------------------------- | ----------- |
|
||||
| object | [PuppeteerNodeLaunchOptions](./puppeteer.puppeteernodelaunchoptions.md) | |
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ----------------------------------------------------------------------- | ------------ |
|
||||
| options | [PuppeteerNodeLaunchOptions](./puppeteer.puppeteernodelaunchoptions.md) | _(Optional)_ |
|
||||
|
||||
**Returns:**
|
||||
|
||||
|
@ -28,4 +28,4 @@ The constructor for this class is marked as internal. Third-party code should no
|
||||
| ------------------------------------------------------------------------ | --------- | ----------- |
|
||||
| [defaultArgs(object)](./puppeteer.productlauncher.defaultargs.md) | | |
|
||||
| [executablePath(channel)](./puppeteer.productlauncher.executablepath.md) | | |
|
||||
| [launch(object)](./puppeteer.productlauncher.launch.md) | | |
|
||||
| [launch(options)](./puppeteer.productlauncher.launch.md) | | |
|
||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -9482,6 +9482,7 @@
|
||||
"version": "19.8.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "0.3.2",
|
||||
"chromium-bidi": "0.4.6",
|
||||
"cross-fetch": "3.1.5",
|
||||
"debug": "4.3.4",
|
||||
@ -14464,6 +14465,7 @@
|
||||
"puppeteer-core": {
|
||||
"version": "file:packages/puppeteer-core",
|
||||
"requires": {
|
||||
"@puppeteer/browsers": "0.3.2",
|
||||
"chromium-bidi": "0.4.6",
|
||||
"cross-fetch": "3.1.5",
|
||||
"debug": "4.3.4",
|
||||
|
@ -270,10 +270,9 @@ class Process {
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.#runHooks();
|
||||
if (this.#exited) {
|
||||
return this.#browserProcessExiting;
|
||||
if (!this.#exited) {
|
||||
this.kill();
|
||||
}
|
||||
this.kill();
|
||||
return this.#browserProcessExiting;
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,8 @@
|
||||
"clean": "if-file-deleted",
|
||||
"dependencies": [
|
||||
"generate:package-json",
|
||||
"generate:sources"
|
||||
"generate:sources",
|
||||
"../browsers:build"
|
||||
],
|
||||
"files": [
|
||||
"{compat,src,third_party}/**",
|
||||
@ -140,7 +141,8 @@
|
||||
"proxy-from-env": "1.1.0",
|
||||
"tar-fs": "2.1.1",
|
||||
"unbzip2-stream": "1.4.3",
|
||||
"ws": "8.13.0"
|
||||
"ws": "8.13.0",
|
||||
"@puppeteer/browsers": "0.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">= 4.7.4"
|
||||
|
@ -275,6 +275,13 @@ export class Connection extends EventEmitter {
|
||||
}) as Promise<ProtocolMapping.Commands[T]['returnType']>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async closeBrowser(): Promise<void> {
|
||||
await this.send('Browser.close');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -1,392 +0,0 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import childProcess from 'child_process';
|
||||
import fs from 'fs';
|
||||
import {rename, unlink} from 'fs/promises';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
|
||||
import type {Connection as BiDiConnection} from '../common/bidi/bidi.js';
|
||||
import {Connection} from '../common/Connection.js';
|
||||
import {debug} from '../common/Debug.js';
|
||||
import {TimeoutError} from '../common/Errors.js';
|
||||
import {NodeWebSocketTransport as WebSocketTransport} from '../common/NodeWebSocketTransport.js';
|
||||
import {Product} from '../common/Product.js';
|
||||
import {
|
||||
addEventListener,
|
||||
debugError,
|
||||
PuppeteerEventListener,
|
||||
removeEventListeners,
|
||||
} from '../common/util.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {isErrnoException, isErrorLike} from '../util/ErrorLike.js';
|
||||
import {rm, rmSync} from '../util/fs.js';
|
||||
|
||||
import {LaunchOptions} from './LaunchOptions.js';
|
||||
import {PipeTransport} from './PipeTransport.js';
|
||||
|
||||
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.`;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class BrowserRunner {
|
||||
#product: Product;
|
||||
#executablePath: string;
|
||||
#processArguments: string[];
|
||||
#userDataDir: string;
|
||||
#isTempUserDataDir?: boolean;
|
||||
#closed = true;
|
||||
#listeners: PuppeteerEventListener[] = [];
|
||||
#processClosing!: Promise<void>;
|
||||
|
||||
proc?: childProcess.ChildProcess;
|
||||
connection?: Connection;
|
||||
|
||||
constructor(
|
||||
product: Product,
|
||||
executablePath: string,
|
||||
processArguments: string[],
|
||||
userDataDir: string,
|
||||
isTempUserDataDir?: boolean
|
||||
) {
|
||||
this.#product = product;
|
||||
this.#executablePath = executablePath;
|
||||
this.#processArguments = processArguments;
|
||||
this.#userDataDir = userDataDir;
|
||||
this.#isTempUserDataDir = isTempUserDataDir;
|
||||
}
|
||||
|
||||
start(options: LaunchOptions): void {
|
||||
const {handleSIGINT, handleSIGTERM, handleSIGHUP, dumpio, env, pipe} =
|
||||
options;
|
||||
let stdio: Array<'ignore' | 'pipe'>;
|
||||
if (pipe) {
|
||||
if (dumpio) {
|
||||
stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
|
||||
} else {
|
||||
stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
|
||||
}
|
||||
} else {
|
||||
if (dumpio) {
|
||||
stdio = ['pipe', 'pipe', 'pipe'];
|
||||
} else {
|
||||
stdio = ['pipe', 'ignore', '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, reject) => {
|
||||
this.proc!.once('exit', async () => {
|
||||
this.#closed = true;
|
||||
// Cleanup as processes exit.
|
||||
if (this.#isTempUserDataDir) {
|
||||
try {
|
||||
await rm(this.#userDataDir);
|
||||
fulfill();
|
||||
} catch (error) {
|
||||
debugError(error);
|
||||
reject(error);
|
||||
}
|
||||
} else {
|
||||
if (this.#product === 'firefox') {
|
||||
try {
|
||||
// When an existing user profile has been used remove the user
|
||||
// preferences file and restore possibly backuped preferences.
|
||||
await unlink(path.join(this.#userDataDir, 'user.js'));
|
||||
|
||||
const prefsBackupPath = path.join(
|
||||
this.#userDataDir,
|
||||
'prefs.js.puppeteer'
|
||||
);
|
||||
if (fs.existsSync(prefsBackupPath)) {
|
||||
const prefsPath = path.join(this.#userDataDir, 'prefs.js');
|
||||
await unlink(prefsPath);
|
||||
await rename(prefsBackupPath, prefsPath);
|
||||
}
|
||||
} catch (error) {
|
||||
debugError(error);
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
fulfill();
|
||||
}
|
||||
});
|
||||
});
|
||||
this.#listeners = [addEventListener(process, 'exit', this.kill.bind(this))];
|
||||
if (handleSIGINT) {
|
||||
this.#listeners.push(
|
||||
addEventListener(process, 'SIGINT', () => {
|
||||
this.kill();
|
||||
process.exit(130);
|
||||
})
|
||||
);
|
||||
}
|
||||
if (handleSIGTERM) {
|
||||
this.#listeners.push(
|
||||
addEventListener(process, 'SIGTERM', this.close.bind(this))
|
||||
);
|
||||
}
|
||||
if (handleSIGHUP) {
|
||||
this.#listeners.push(
|
||||
addEventListener(process, 'SIGHUP', this.close.bind(this))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
if (this.#closed) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (this.#isTempUserDataDir) {
|
||||
this.kill();
|
||||
} else if (this.connection) {
|
||||
// Attempt to close the browser gracefully
|
||||
this.connection.send('Browser.close').catch(error => {
|
||||
debugError(error);
|
||||
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.
|
||||
removeEventListeners(this.#listeners);
|
||||
return this.#processClosing;
|
||||
}
|
||||
|
||||
kill(): void {
|
||||
// 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 && pidExists(this.proc.pid)) {
|
||||
const proc = this.proc;
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
childProcess.exec(`taskkill /pid ${this.proc.pid} /T /F`, error => {
|
||||
if (error) {
|
||||
// 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
|
||||
// processes of `this.proc` until the main Node.js process dies.
|
||||
proc.kill();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// on linux the process group can be killed with the group id prefixed with
|
||||
// a minus sign. The process group id is the group leader's pid.
|
||||
const processGroupId = -this.proc.pid;
|
||||
|
||||
try {
|
||||
process.kill(processGroupId, 'SIGKILL');
|
||||
} catch (error) {
|
||||
// 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
|
||||
// processes of `this.proc` until the main Node.js process dies.
|
||||
proc.kill('SIGKILL');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`${PROCESS_ERROR_EXPLANATION}\nError cause: ${
|
||||
isErrorLike(error) ? error.stack : error
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to remove temporary profile directory to avoid littering.
|
||||
try {
|
||||
if (this.#isTempUserDataDir) {
|
||||
rmSync(this.#userDataDir);
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
// 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.
|
||||
removeEventListeners(this.#listeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async setupWebDriverBiDiConnection(options: {
|
||||
timeout: number;
|
||||
slowMo: number;
|
||||
preferredRevision: string;
|
||||
protocolTimeout?: number;
|
||||
}): Promise<BiDiConnection> {
|
||||
assert(this.proc, 'BrowserRunner not started.');
|
||||
|
||||
const {timeout, slowMo, preferredRevision, protocolTimeout} = options;
|
||||
let browserWSEndpoint = await waitForWSEndpoint(
|
||||
this.proc,
|
||||
timeout,
|
||||
preferredRevision,
|
||||
/^WebDriver BiDi listening on (ws:\/\/.*)$/
|
||||
);
|
||||
browserWSEndpoint += '/session';
|
||||
const transport = await WebSocketTransport.create(browserWSEndpoint);
|
||||
const BiDi = await import(
|
||||
/* webpackIgnore: true */ '../common/bidi/bidi.js'
|
||||
);
|
||||
return new BiDi.Connection(transport, slowMo, protocolTimeout);
|
||||
}
|
||||
|
||||
async setupConnection(options: {
|
||||
usePipe?: boolean;
|
||||
timeout: number;
|
||||
slowMo: number;
|
||||
preferredRevision: string;
|
||||
protocolTimeout?: number;
|
||||
}): Promise<Connection> {
|
||||
assert(this.proc, 'BrowserRunner not started.');
|
||||
|
||||
const {usePipe, timeout, slowMo, preferredRevision, protocolTimeout} =
|
||||
options;
|
||||
if (!usePipe) {
|
||||
const browserWSEndpoint = await waitForWSEndpoint(
|
||||
this.proc,
|
||||
timeout,
|
||||
preferredRevision
|
||||
);
|
||||
const transport = await WebSocketTransport.create(browserWSEndpoint);
|
||||
this.connection = new Connection(
|
||||
browserWSEndpoint,
|
||||
transport,
|
||||
slowMo,
|
||||
protocolTimeout
|
||||
);
|
||||
} 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, protocolTimeout);
|
||||
}
|
||||
return this.connection;
|
||||
}
|
||||
}
|
||||
|
||||
function waitForWSEndpoint(
|
||||
browserProcess: childProcess.ChildProcess,
|
||||
timeout: number,
|
||||
preferredRevision: string,
|
||||
regex = /^DevTools listening on (ws:\/\/.*)$/
|
||||
): Promise<string> {
|
||||
assert(browserProcess.stderr, '`browserProcess` does not have stderr.');
|
||||
const rl = readline.createInterface(browserProcess.stderr);
|
||||
let stderr = '';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const listeners = [
|
||||
addEventListener(rl, 'line', onLine),
|
||||
addEventListener(rl, 'close', () => {
|
||||
return onClose();
|
||||
}),
|
||||
addEventListener(browserProcess, 'exit', () => {
|
||||
return onClose();
|
||||
}),
|
||||
addEventListener(browserProcess, 'error', error => {
|
||||
return onClose(error);
|
||||
}),
|
||||
];
|
||||
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
|
||||
|
||||
function onClose(error?: Error): void {
|
||||
cleanup();
|
||||
reject(
|
||||
new Error(
|
||||
[
|
||||
'Failed to launch the browser process!' +
|
||||
(error ? ' ' + error.message : ''),
|
||||
stderr,
|
||||
'',
|
||||
'TROUBLESHOOTING: https://pptr.dev/troubleshooting',
|
||||
'',
|
||||
].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(regex);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
cleanup();
|
||||
// The RegExp matches, so this will obviously exist.
|
||||
resolve(match[1]!);
|
||||
}
|
||||
|
||||
function cleanup(): void {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
removeEventListeners(listeners);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function pidExists(pid: number): boolean {
|
||||
try {
|
||||
return process.kill(pid, 0);
|
||||
} catch (error) {
|
||||
if (isErrnoException(error)) {
|
||||
if (error.code && error.code === 'ESRCH') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
@ -1,19 +1,38 @@
|
||||
import {accessSync} from 'fs';
|
||||
/**
|
||||
* Copyright 2023 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {mkdtemp} from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import {Browser} from '../api/Browser.js';
|
||||
import {CDPBrowser} from '../common/Browser.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {
|
||||
computeSystemExecutablePath,
|
||||
Browser as SupportedBrowsers,
|
||||
ChromeReleaseChannel as BrowsersChromeReleaseChannel,
|
||||
} from '@puppeteer/browsers';
|
||||
|
||||
import {debugError} from '../common/util.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {rm} from '../util/fs.js';
|
||||
|
||||
import {BrowserRunner} from './BrowserRunner.js';
|
||||
import {
|
||||
BrowserLaunchArgumentOptions,
|
||||
ChromeReleaseChannel,
|
||||
PuppeteerNodeLaunchOptions,
|
||||
} from './LaunchOptions.js';
|
||||
import {ProductLauncher} from './ProductLauncher.js';
|
||||
import {ProductLauncher, ResolvedLaunchArgs} from './ProductLauncher.js';
|
||||
import {PuppeteerNode} from './PuppeteerNode.js';
|
||||
|
||||
/**
|
||||
@ -24,28 +43,19 @@ export class ChromeLauncher extends ProductLauncher {
|
||||
super(puppeteer, 'chrome');
|
||||
}
|
||||
|
||||
override async launch(
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
override async computeLaunchArguments(
|
||||
options: PuppeteerNodeLaunchOptions = {}
|
||||
): Promise<Browser> {
|
||||
): Promise<ResolvedLaunchArgs> {
|
||||
const {
|
||||
ignoreDefaultArgs = false,
|
||||
args = [],
|
||||
dumpio = false,
|
||||
pipe = false,
|
||||
debuggingPort,
|
||||
channel,
|
||||
executablePath,
|
||||
pipe = false,
|
||||
env = process.env,
|
||||
handleSIGINT = true,
|
||||
handleSIGTERM = true,
|
||||
handleSIGHUP = true,
|
||||
ignoreHTTPSErrors = false,
|
||||
defaultViewport = {width: 800, height: 600},
|
||||
slowMo = 0,
|
||||
timeout = 30000,
|
||||
waitForInitialPage = true,
|
||||
debuggingPort,
|
||||
protocol,
|
||||
protocolTimeout,
|
||||
} = options;
|
||||
|
||||
const chromeArguments = [];
|
||||
@ -104,82 +114,29 @@ export class ChromeLauncher extends ProductLauncher {
|
||||
chromeExecutable = this.executablePath(channel);
|
||||
}
|
||||
|
||||
const usePipe = chromeArguments.includes('--remote-debugging-pipe');
|
||||
const runner = new BrowserRunner(
|
||||
this.product,
|
||||
chromeExecutable,
|
||||
chromeArguments,
|
||||
return {
|
||||
executablePath: chromeExecutable,
|
||||
args: chromeArguments,
|
||||
isTempUserDataDir,
|
||||
userDataDir,
|
||||
isTempUserDataDir
|
||||
);
|
||||
runner.start({
|
||||
handleSIGHUP,
|
||||
handleSIGTERM,
|
||||
handleSIGINT,
|
||||
dumpio,
|
||||
env,
|
||||
pipe: usePipe,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
let browser;
|
||||
try {
|
||||
const connection = await runner.setupConnection({
|
||||
usePipe,
|
||||
timeout,
|
||||
slowMo,
|
||||
preferredRevision: this.puppeteer.browserRevision,
|
||||
protocolTimeout,
|
||||
});
|
||||
|
||||
if (protocol === 'webDriverBiDi') {
|
||||
try {
|
||||
const BiDi = await import(
|
||||
/* webpackIgnore: true */ '../common/bidi/bidi.js'
|
||||
);
|
||||
const bidiConnection = await BiDi.connectBidiOverCDP(connection);
|
||||
browser = await BiDi.Browser.create({
|
||||
connection: bidiConnection,
|
||||
closeCallback: runner.close.bind(runner),
|
||||
process: runner.proc,
|
||||
});
|
||||
} catch (error) {
|
||||
runner.kill();
|
||||
throw error;
|
||||
}
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
browser = await CDPBrowser._create(
|
||||
this.product,
|
||||
connection,
|
||||
[],
|
||||
ignoreHTTPSErrors,
|
||||
defaultViewport,
|
||||
runner.proc,
|
||||
runner.close.bind(runner),
|
||||
options.targetFilter
|
||||
);
|
||||
} catch (error) {
|
||||
runner.kill();
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (waitForInitialPage) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
override async cleanUserDataDir(
|
||||
path: string,
|
||||
opts: {isTemp: boolean}
|
||||
): Promise<void> {
|
||||
if (opts.isTemp) {
|
||||
try {
|
||||
await browser.waitForTarget(
|
||||
t => {
|
||||
return t.type() === 'page';
|
||||
},
|
||||
{timeout}
|
||||
);
|
||||
await rm(path);
|
||||
} catch (error) {
|
||||
await browser.close();
|
||||
debugError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
override defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] {
|
||||
@ -248,86 +205,27 @@ export class ChromeLauncher extends ProductLauncher {
|
||||
|
||||
override executablePath(channel?: ChromeReleaseChannel): string {
|
||||
if (channel) {
|
||||
return this.#executablePathForChannel(channel);
|
||||
return computeSystemExecutablePath({
|
||||
browser: SupportedBrowsers.CHROME,
|
||||
channel: convertPuppeteerChannelToBrowsersChannel(channel),
|
||||
});
|
||||
} else {
|
||||
return this.resolveExecutablePath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
#executablePathForChannel(channel: ChromeReleaseChannel): string {
|
||||
const platform = os.platform();
|
||||
|
||||
let chromePath: string | undefined;
|
||||
switch (platform) {
|
||||
case 'win32':
|
||||
switch (channel) {
|
||||
case 'chrome':
|
||||
chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe`;
|
||||
break;
|
||||
case 'chrome-beta':
|
||||
chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome Beta\\Application\\chrome.exe`;
|
||||
break;
|
||||
case 'chrome-canary':
|
||||
chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome SxS\\Application\\chrome.exe`;
|
||||
break;
|
||||
case 'chrome-dev':
|
||||
chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome Dev\\Application\\chrome.exe`;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'darwin':
|
||||
switch (channel) {
|
||||
case 'chrome':
|
||||
chromePath =
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
break;
|
||||
case 'chrome-beta':
|
||||
chromePath =
|
||||
'/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta';
|
||||
break;
|
||||
case 'chrome-canary':
|
||||
chromePath =
|
||||
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary';
|
||||
break;
|
||||
case 'chrome-dev':
|
||||
chromePath =
|
||||
'/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'linux':
|
||||
switch (channel) {
|
||||
case 'chrome':
|
||||
chromePath = '/opt/google/chrome/chrome';
|
||||
break;
|
||||
case 'chrome-beta':
|
||||
chromePath = '/opt/google/chrome-beta/chrome';
|
||||
break;
|
||||
case 'chrome-dev':
|
||||
chromePath = '/opt/google/chrome-unstable/chrome';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!chromePath) {
|
||||
throw new Error(
|
||||
`Unable to detect browser executable path for '${channel}' on ${platform}.`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if Chrome exists and is accessible.
|
||||
try {
|
||||
accessSync(chromePath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Could not find Google Chrome executable for channel '${channel}' at '${chromePath}'.`
|
||||
);
|
||||
}
|
||||
|
||||
return chromePath;
|
||||
function convertPuppeteerChannelToBrowsersChannel(
|
||||
channel: ChromeReleaseChannel
|
||||
): BrowsersChromeReleaseChannel {
|
||||
switch (channel) {
|
||||
case 'chrome':
|
||||
return BrowsersChromeReleaseChannel.STABLE;
|
||||
case 'chrome-dev':
|
||||
return BrowsersChromeReleaseChannel.DEV;
|
||||
case 'chrome-beta':
|
||||
return BrowsersChromeReleaseChannel.BETA;
|
||||
case 'chrome-canary':
|
||||
return BrowsersChromeReleaseChannel.CANARY;
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,35 @@
|
||||
/**
|
||||
* Copyright 2023 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import {rename, unlink, mkdtemp} from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import {Browser} from '../api/Browser.js';
|
||||
import {CDPBrowser} from '../common/Browser.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {Browser as SupportedBrowsers, createProfile} from '@puppeteer/browsers';
|
||||
|
||||
import {debugError} from '../common/util.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {rm} from '../util/fs.js';
|
||||
|
||||
import {BrowserRunner} from './BrowserRunner.js';
|
||||
import {
|
||||
BrowserLaunchArgumentOptions,
|
||||
PuppeteerNodeLaunchOptions,
|
||||
} from './LaunchOptions.js';
|
||||
import {ProductLauncher} from './ProductLauncher.js';
|
||||
import {ProductLauncher, ResolvedLaunchArgs} from './ProductLauncher.js';
|
||||
import {PuppeteerNode} from './PuppeteerNode.js';
|
||||
|
||||
/**
|
||||
@ -21,29 +39,19 @@ export class FirefoxLauncher extends ProductLauncher {
|
||||
constructor(puppeteer: PuppeteerNode) {
|
||||
super(puppeteer, 'firefox');
|
||||
}
|
||||
|
||||
override async launch(
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
override async computeLaunchArguments(
|
||||
options: PuppeteerNodeLaunchOptions = {}
|
||||
): Promise<Browser> {
|
||||
): Promise<ResolvedLaunchArgs> {
|
||||
const {
|
||||
ignoreDefaultArgs = false,
|
||||
args = [],
|
||||
dumpio = false,
|
||||
executablePath,
|
||||
pipe = false,
|
||||
env = process.env,
|
||||
handleSIGINT = true,
|
||||
handleSIGTERM = true,
|
||||
handleSIGHUP = true,
|
||||
ignoreHTTPSErrors = false,
|
||||
defaultViewport = {width: 800, height: 600},
|
||||
slowMo = 0,
|
||||
timeout = 30000,
|
||||
extraPrefsFirefox = {},
|
||||
waitForInitialPage = true,
|
||||
debuggingPort = null,
|
||||
protocol = 'cdp',
|
||||
protocolTimeout,
|
||||
} = options;
|
||||
|
||||
const firefoxArguments = [];
|
||||
@ -91,14 +99,17 @@ export class FirefoxLauncher extends ProductLauncher {
|
||||
// When using a custom Firefox profile it needs to be populated
|
||||
// with required preferences.
|
||||
isTempUserDataDir = false;
|
||||
const prefs = this.defaultPreferences(extraPrefsFirefox);
|
||||
this.writePreferences(prefs, userDataDir);
|
||||
} else {
|
||||
userDataDir = await this._createProfile(extraPrefsFirefox);
|
||||
userDataDir = await mkdtemp(this.getProfilePath());
|
||||
firefoxArguments.push('--profile');
|
||||
firefoxArguments.push(userDataDir);
|
||||
}
|
||||
|
||||
await createProfile(SupportedBrowsers.FIREFOX, {
|
||||
path: userDataDir,
|
||||
preferences: extraPrefsFirefox,
|
||||
});
|
||||
|
||||
let firefoxExecutable: string;
|
||||
if (this.puppeteer._isPuppeteerCore || executablePath) {
|
||||
assert(
|
||||
@ -110,86 +121,44 @@ export class FirefoxLauncher extends ProductLauncher {
|
||||
firefoxExecutable = this.executablePath();
|
||||
}
|
||||
|
||||
const runner = new BrowserRunner(
|
||||
this.product,
|
||||
firefoxExecutable,
|
||||
firefoxArguments,
|
||||
return {
|
||||
isTempUserDataDir,
|
||||
userDataDir,
|
||||
isTempUserDataDir
|
||||
);
|
||||
runner.start({
|
||||
handleSIGHUP,
|
||||
handleSIGTERM,
|
||||
handleSIGINT,
|
||||
dumpio,
|
||||
env,
|
||||
pipe,
|
||||
});
|
||||
args: firefoxArguments,
|
||||
executablePath: firefoxExecutable,
|
||||
};
|
||||
}
|
||||
|
||||
if (protocol === 'webDriverBiDi') {
|
||||
let browser: Browser;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
override async cleanUserDataDir(
|
||||
userDataDir: string,
|
||||
opts: {isTemp: boolean}
|
||||
): Promise<void> {
|
||||
if (opts.isTemp) {
|
||||
try {
|
||||
const connection = await runner.setupWebDriverBiDiConnection({
|
||||
timeout,
|
||||
slowMo,
|
||||
preferredRevision: this.puppeteer.browserRevision,
|
||||
protocolTimeout,
|
||||
});
|
||||
const BiDi = await import(
|
||||
/* webpackIgnore: true */ '../common/bidi/bidi.js'
|
||||
);
|
||||
browser = await BiDi.Browser.create({
|
||||
connection,
|
||||
closeCallback: runner.close.bind(runner),
|
||||
process: runner.proc,
|
||||
});
|
||||
await rm(userDataDir);
|
||||
} catch (error) {
|
||||
runner.kill();
|
||||
debugError(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
let browser;
|
||||
try {
|
||||
const connection = await runner.setupConnection({
|
||||
usePipe: pipe,
|
||||
timeout,
|
||||
slowMo,
|
||||
preferredRevision: this.puppeteer.browserRevision,
|
||||
protocolTimeout,
|
||||
});
|
||||
browser = await CDPBrowser._create(
|
||||
this.product,
|
||||
connection,
|
||||
[],
|
||||
ignoreHTTPSErrors,
|
||||
defaultViewport,
|
||||
runner.proc,
|
||||
runner.close.bind(runner),
|
||||
options.targetFilter
|
||||
);
|
||||
} catch (error) {
|
||||
runner.kill();
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (waitForInitialPage) {
|
||||
} else {
|
||||
try {
|
||||
await browser.waitForTarget(
|
||||
t => {
|
||||
return t.type() === 'page';
|
||||
},
|
||||
{timeout}
|
||||
);
|
||||
// When an existing user profile has been used remove the user
|
||||
// preferences file and restore possibly backuped preferences.
|
||||
await unlink(path.join(userDataDir, 'user.js'));
|
||||
|
||||
const prefsBackupPath = path.join(userDataDir, 'prefs.js.puppeteer');
|
||||
if (fs.existsSync(prefsBackupPath)) {
|
||||
const prefsPath = path.join(userDataDir, 'prefs.js');
|
||||
await unlink(prefsPath);
|
||||
await rename(prefsBackupPath, prefsPath);
|
||||
}
|
||||
} catch (error) {
|
||||
await browser.close();
|
||||
throw error;
|
||||
debugError(error);
|
||||
}
|
||||
}
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
override executablePath(): string {
|
||||
@ -245,256 +214,4 @@ export class FirefoxLauncher extends ProductLauncher {
|
||||
firefoxArguments.push(...args);
|
||||
return firefoxArguments;
|
||||
}
|
||||
|
||||
defaultPreferences(extraPrefs: {[x: string]: unknown}): {
|
||||
[x: string]: unknown;
|
||||
} {
|
||||
const server = 'dummy.test';
|
||||
|
||||
const defaultPrefs = {
|
||||
// Make sure Shield doesn't hit the network.
|
||||
'app.normandy.api_url': '',
|
||||
// Disable Firefox old build background check
|
||||
'app.update.checkInstallTime': false,
|
||||
// Disable automatically upgrading Firefox
|
||||
'app.update.disabledForTesting': true,
|
||||
|
||||
// Increase the APZ content response timeout to 1 minute
|
||||
'apz.content_response_timeout': 60000,
|
||||
|
||||
// Prevent various error message on the console
|
||||
// jest-puppeteer asserts that no error message is emitted by the console
|
||||
'browser.contentblocking.features.standard':
|
||||
'-tp,tpPrivate,cookieBehavior0,-cm,-fp',
|
||||
|
||||
// Enable the dump function: which sends messages to the system
|
||||
// console
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1543115
|
||||
'browser.dom.window.dump.enabled': true,
|
||||
// Disable topstories
|
||||
'browser.newtabpage.activity-stream.feeds.system.topstories': false,
|
||||
// Always display a blank page
|
||||
'browser.newtabpage.enabled': false,
|
||||
// Background thumbnails in particular cause grief: and disabling
|
||||
// thumbnails in general cannot hurt
|
||||
'browser.pagethumbnails.capturing_disabled': true,
|
||||
|
||||
// Disable safebrowsing components.
|
||||
'browser.safebrowsing.blockedURIs.enabled': false,
|
||||
'browser.safebrowsing.downloads.enabled': false,
|
||||
'browser.safebrowsing.malware.enabled': false,
|
||||
'browser.safebrowsing.passwords.enabled': false,
|
||||
'browser.safebrowsing.phishing.enabled': false,
|
||||
|
||||
// Disable updates to search engines.
|
||||
'browser.search.update': false,
|
||||
// Do not restore the last open set of tabs if the browser has crashed
|
||||
'browser.sessionstore.resume_from_crash': false,
|
||||
// Skip check for default browser on startup
|
||||
'browser.shell.checkDefaultBrowser': false,
|
||||
|
||||
// Disable newtabpage
|
||||
'browser.startup.homepage': 'about:blank',
|
||||
// Do not redirect user when a milstone upgrade of Firefox is detected
|
||||
'browser.startup.homepage_override.mstone': 'ignore',
|
||||
// Start with a blank page about:blank
|
||||
'browser.startup.page': 0,
|
||||
|
||||
// Do not allow background tabs to be zombified on Android: otherwise for
|
||||
// tests that open additional tabs: the test harness tab itself might get
|
||||
// unloaded
|
||||
'browser.tabs.disableBackgroundZombification': false,
|
||||
// Do not warn when closing all other open tabs
|
||||
'browser.tabs.warnOnCloseOtherTabs': false,
|
||||
// Do not warn when multiple tabs will be opened
|
||||
'browser.tabs.warnOnOpen': false,
|
||||
|
||||
// Disable the UI tour.
|
||||
'browser.uitour.enabled': false,
|
||||
// Turn off search suggestions in the location bar so as not to trigger
|
||||
// network connections.
|
||||
'browser.urlbar.suggest.searches': false,
|
||||
// Disable first run splash page on Windows 10
|
||||
'browser.usedOnWindows10.introURL': '',
|
||||
// Do not warn on quitting Firefox
|
||||
'browser.warnOnQuit': false,
|
||||
|
||||
// Defensively disable data reporting systems
|
||||
'datareporting.healthreport.documentServerURI': `http://${server}/dummy/healthreport/`,
|
||||
'datareporting.healthreport.logging.consoleEnabled': false,
|
||||
'datareporting.healthreport.service.enabled': false,
|
||||
'datareporting.healthreport.service.firstRun': false,
|
||||
'datareporting.healthreport.uploadEnabled': false,
|
||||
|
||||
// Do not show datareporting policy notifications which can interfere with tests
|
||||
'datareporting.policy.dataSubmissionEnabled': false,
|
||||
'datareporting.policy.dataSubmissionPolicyBypassNotification': true,
|
||||
|
||||
// DevTools JSONViewer sometimes fails to load dependencies with its require.js.
|
||||
// This doesn't affect Puppeteer but spams console (Bug 1424372)
|
||||
'devtools.jsonview.enabled': false,
|
||||
|
||||
// Disable popup-blocker
|
||||
'dom.disable_open_during_load': false,
|
||||
|
||||
// Enable the support for File object creation in the content process
|
||||
// Required for |Page.setFileInputFiles| protocol method.
|
||||
'dom.file.createInChild': true,
|
||||
|
||||
// Disable the ProcessHangMonitor
|
||||
'dom.ipc.reportProcessHangs': false,
|
||||
|
||||
// Disable slow script dialogues
|
||||
'dom.max_chrome_script_run_time': 0,
|
||||
'dom.max_script_run_time': 0,
|
||||
|
||||
// Only load extensions from the application and user profile
|
||||
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
|
||||
'extensions.autoDisableScopes': 0,
|
||||
'extensions.enabledScopes': 5,
|
||||
|
||||
// Disable metadata caching for installed add-ons by default
|
||||
'extensions.getAddons.cache.enabled': false,
|
||||
|
||||
// Disable installing any distribution extensions or add-ons.
|
||||
'extensions.installDistroAddons': false,
|
||||
|
||||
// Disabled screenshots extension
|
||||
'extensions.screenshots.disabled': true,
|
||||
|
||||
// Turn off extension updates so they do not bother tests
|
||||
'extensions.update.enabled': false,
|
||||
|
||||
// Turn off extension updates so they do not bother tests
|
||||
'extensions.update.notifyUser': false,
|
||||
|
||||
// Make sure opening about:addons will not hit the network
|
||||
'extensions.webservice.discoverURL': `http://${server}/dummy/discoveryURL`,
|
||||
|
||||
// Temporarily force disable BFCache in parent (https://bit.ly/bug-1732263)
|
||||
'fission.bfcacheInParent': false,
|
||||
|
||||
// Force all web content to use a single content process
|
||||
'fission.webContentIsolationStrategy': 0,
|
||||
|
||||
// Allow the application to have focus even it runs in the background
|
||||
'focusmanager.testmode': true,
|
||||
// Disable useragent updates
|
||||
'general.useragent.updates.enabled': false,
|
||||
// Always use network provider for geolocation tests so we bypass the
|
||||
// macOS dialog raised by the corelocation provider
|
||||
'geo.provider.testing': true,
|
||||
// Do not scan Wifi
|
||||
'geo.wifi.scan': false,
|
||||
// No hang monitor
|
||||
'hangmonitor.timeout': 0,
|
||||
// Show chrome errors and warnings in the error console
|
||||
'javascript.options.showInConsole': true,
|
||||
|
||||
// Disable download and usage of OpenH264: and Widevine plugins
|
||||
'media.gmp-manager.updateEnabled': false,
|
||||
// Prevent various error message on the console
|
||||
// jest-puppeteer asserts that no error message is emitted by the console
|
||||
'network.cookie.cookieBehavior': 0,
|
||||
|
||||
// Disable experimental feature that is only available in Nightly
|
||||
'network.cookie.sameSite.laxByDefault': false,
|
||||
|
||||
// Do not prompt for temporary redirects
|
||||
'network.http.prompt-temp-redirect': false,
|
||||
|
||||
// Disable speculative connections so they are not reported as leaking
|
||||
// when they are hanging around
|
||||
'network.http.speculative-parallel-limit': 0,
|
||||
|
||||
// Do not automatically switch between offline and online
|
||||
'network.manage-offline-status': false,
|
||||
|
||||
// Make sure SNTP requests do not hit the network
|
||||
'network.sntp.pools': server,
|
||||
|
||||
// Disable Flash.
|
||||
'plugin.state.flash': 0,
|
||||
|
||||
'privacy.trackingprotection.enabled': false,
|
||||
|
||||
// Can be removed once Firefox 89 is no longer supported
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1710839
|
||||
'remote.enabled': true,
|
||||
|
||||
// Don't do network connections for mitm priming
|
||||
'security.certerrors.mitm.priming.enabled': false,
|
||||
// Local documents have access to all other local documents,
|
||||
// including directory listings
|
||||
'security.fileuri.strict_origin_policy': false,
|
||||
// Do not wait for the notification button security delay
|
||||
'security.notification_enable_delay': 0,
|
||||
|
||||
// Ensure blocklist updates do not hit the network
|
||||
'services.settings.server': `http://${server}/dummy/blocklist/`,
|
||||
|
||||
// Do not automatically fill sign-in forms with known usernames and
|
||||
// passwords
|
||||
'signon.autofillForms': false,
|
||||
// Disable password capture, so that tests that include forms are not
|
||||
// influenced by the presence of the persistent doorhanger notification
|
||||
'signon.rememberSignons': false,
|
||||
|
||||
// Disable first-run welcome page
|
||||
'startup.homepage_welcome_url': 'about:blank',
|
||||
|
||||
// Disable first-run welcome page
|
||||
'startup.homepage_welcome_url.additional': '',
|
||||
|
||||
// Disable browser animations (tabs, fullscreen, sliding alerts)
|
||||
'toolkit.cosmeticAnimations.enabled': false,
|
||||
|
||||
// Prevent starting into safe mode after application crashes
|
||||
'toolkit.startup.max_resumed_crashes': -1,
|
||||
};
|
||||
|
||||
return Object.assign(defaultPrefs, extraPrefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the user.js file with custom preferences as needed to allow
|
||||
* Firefox's CDP support to properly function. These preferences will be
|
||||
* automatically copied over to prefs.js during startup of Firefox. To be
|
||||
* able to restore the original values of preferences a backup of prefs.js
|
||||
* will be created.
|
||||
*
|
||||
* @param prefs - List of preferences to add.
|
||||
* @param profilePath - Firefox profile to write the preferences to.
|
||||
*/
|
||||
async writePreferences(
|
||||
prefs: {[x: string]: unknown},
|
||||
profilePath: string
|
||||
): Promise<void> {
|
||||
const lines = Object.entries(prefs).map(([key, value]) => {
|
||||
return `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`;
|
||||
});
|
||||
|
||||
await fs.promises.writeFile(
|
||||
path.join(profilePath, 'user.js'),
|
||||
lines.join('\n')
|
||||
);
|
||||
|
||||
// Create a backup of the preferences file if it already exitsts.
|
||||
const prefsPath = path.join(profilePath, 'prefs.js');
|
||||
if (fs.existsSync(prefsPath)) {
|
||||
const prefsBackupPath = path.join(profilePath, 'prefs.js.puppeteer');
|
||||
await fs.promises.copyFile(prefsPath, prefsBackupPath);
|
||||
}
|
||||
}
|
||||
|
||||
async _createProfile(extraPrefs: {[x: string]: unknown}): Promise<string> {
|
||||
const temporaryProfilePath = await fs.promises.mkdtemp(
|
||||
this.getProfilePath()
|
||||
);
|
||||
|
||||
const prefs = this.defaultPreferences(extraPrefs);
|
||||
await this.writePreferences(prefs, temporaryProfilePath);
|
||||
|
||||
return temporaryProfilePath;
|
||||
}
|
||||
}
|
||||
|
@ -17,16 +17,39 @@ import {existsSync} from 'fs';
|
||||
import os, {tmpdir} from 'os';
|
||||
import {join} from 'path';
|
||||
|
||||
import {Browser} from '../api/Browser.js';
|
||||
import {
|
||||
CDP_WEBSOCKET_ENDPOINT_REGEX,
|
||||
launch,
|
||||
TimeoutError as BrowsersTimeoutError,
|
||||
WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
|
||||
} from '@puppeteer/browsers';
|
||||
|
||||
import {Browser, BrowserCloseCallback} from '../api/Browser.js';
|
||||
import {CDPBrowser} from '../common/Browser.js';
|
||||
import {Connection} from '../common/Connection.js';
|
||||
import {TimeoutError} from '../common/Errors.js';
|
||||
import {NodeWebSocketTransport as WebSocketTransport} from '../common/NodeWebSocketTransport.js';
|
||||
import {Product} from '../common/Product.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
|
||||
import {
|
||||
BrowserLaunchArgumentOptions,
|
||||
ChromeReleaseChannel,
|
||||
PuppeteerNodeLaunchOptions,
|
||||
} from './LaunchOptions.js';
|
||||
import {PipeTransport} from './PipeTransport.js';
|
||||
import {PuppeteerNode} from './PuppeteerNode.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type ResolvedLaunchArgs = {
|
||||
isTempUserDataDir: boolean;
|
||||
userDataDir: string;
|
||||
executablePath: string;
|
||||
args: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes a launcher - a class that is able to create and launch a browser instance.
|
||||
*
|
||||
@ -57,9 +80,113 @@ export class ProductLauncher {
|
||||
return this.#product;
|
||||
}
|
||||
|
||||
launch(object: PuppeteerNodeLaunchOptions): Promise<Browser>;
|
||||
launch(): Promise<Browser> {
|
||||
throw new Error('Not implemented');
|
||||
async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> {
|
||||
const {
|
||||
dumpio = false,
|
||||
env = process.env,
|
||||
handleSIGINT = true,
|
||||
handleSIGTERM = true,
|
||||
handleSIGHUP = true,
|
||||
ignoreHTTPSErrors = false,
|
||||
defaultViewport = {width: 800, height: 600},
|
||||
slowMo = 0,
|
||||
timeout = 30000,
|
||||
waitForInitialPage = true,
|
||||
protocol,
|
||||
protocolTimeout,
|
||||
} = options;
|
||||
|
||||
const launchArgs = await this.computeLaunchArguments(options);
|
||||
|
||||
const usePipe = launchArgs.args.includes('--remote-debugging-pipe');
|
||||
|
||||
const onProcessExit = async () => {
|
||||
await this.cleanUserDataDir(launchArgs.userDataDir, {
|
||||
isTemp: launchArgs.isTempUserDataDir,
|
||||
});
|
||||
};
|
||||
|
||||
const browserProcess = launch({
|
||||
executablePath: launchArgs.executablePath,
|
||||
args: launchArgs.args,
|
||||
handleSIGHUP,
|
||||
handleSIGTERM,
|
||||
handleSIGINT,
|
||||
dumpio,
|
||||
env,
|
||||
pipe: usePipe,
|
||||
onExit: onProcessExit,
|
||||
});
|
||||
|
||||
let browser: Browser;
|
||||
let connection: Connection;
|
||||
let closing = false;
|
||||
|
||||
const browserCloseCallback = async () => {
|
||||
if (closing) {
|
||||
return;
|
||||
}
|
||||
closing = true;
|
||||
await this.closeBrowser(browserProcess, connection);
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.#product === 'firefox' && protocol === 'webDriverBiDi') {
|
||||
browser = await this.createBiDiBrowser(
|
||||
browserProcess,
|
||||
browserCloseCallback,
|
||||
{
|
||||
timeout,
|
||||
protocolTimeout,
|
||||
slowMo,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if (usePipe) {
|
||||
connection = await this.createCDPPipeConnection(browserProcess, {
|
||||
timeout,
|
||||
protocolTimeout,
|
||||
slowMo,
|
||||
});
|
||||
} else {
|
||||
connection = await this.createCDPSocketConnection(browserProcess, {
|
||||
timeout,
|
||||
protocolTimeout,
|
||||
slowMo,
|
||||
});
|
||||
}
|
||||
if (protocol === 'webDriverBiDi') {
|
||||
browser = await this.createBiDiOverCDPBrowser(
|
||||
browserProcess,
|
||||
connection,
|
||||
browserCloseCallback
|
||||
);
|
||||
} else {
|
||||
browser = await CDPBrowser._create(
|
||||
this.product,
|
||||
connection,
|
||||
[],
|
||||
ignoreHTTPSErrors,
|
||||
defaultViewport,
|
||||
browserProcess.nodeProcess,
|
||||
browserCloseCallback,
|
||||
options.targetFilter
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
browserCloseCallback();
|
||||
if (error instanceof BrowsersTimeoutError) {
|
||||
throw new TimeoutError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (waitForInitialPage && protocol !== 'webDriverBiDi') {
|
||||
await this.waitForPageTarget(browser, timeout);
|
||||
}
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
executablePath(channel?: ChromeReleaseChannel): string;
|
||||
@ -81,6 +208,153 @@ export class ProductLauncher {
|
||||
return this.actualBrowserRevision;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async computeLaunchArguments(
|
||||
options: PuppeteerNodeLaunchOptions
|
||||
): Promise<ResolvedLaunchArgs>;
|
||||
protected async computeLaunchArguments(): Promise<ResolvedLaunchArgs> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async cleanUserDataDir(
|
||||
path: string,
|
||||
opts: {isTemp: boolean}
|
||||
): Promise<void>;
|
||||
protected async cleanUserDataDir(): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async closeBrowser(
|
||||
browserProcess: ReturnType<typeof launch>,
|
||||
connection?: Connection
|
||||
): Promise<void> {
|
||||
if (connection) {
|
||||
// Attempt to close the browser gracefully
|
||||
try {
|
||||
await connection.closeBrowser();
|
||||
await browserProcess.hasClosed();
|
||||
} catch (error) {
|
||||
debugError(error);
|
||||
await browserProcess.close();
|
||||
}
|
||||
} else {
|
||||
await browserProcess.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async waitForPageTarget(
|
||||
browser: Browser,
|
||||
timeout: number
|
||||
): Promise<void> {
|
||||
try {
|
||||
await browser.waitForTarget(
|
||||
t => {
|
||||
return t.type() === 'page';
|
||||
},
|
||||
{timeout}
|
||||
);
|
||||
} catch (error) {
|
||||
await browser.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async createCDPSocketConnection(
|
||||
browserProcess: ReturnType<typeof launch>,
|
||||
opts: {timeout: number; protocolTimeout: number | undefined; slowMo: number}
|
||||
): Promise<Connection> {
|
||||
const browserWSEndpoint = await browserProcess.waitForLineOutput(
|
||||
CDP_WEBSOCKET_ENDPOINT_REGEX,
|
||||
opts.timeout
|
||||
);
|
||||
const transport = await WebSocketTransport.create(browserWSEndpoint);
|
||||
return new Connection(
|
||||
browserWSEndpoint,
|
||||
transport,
|
||||
opts.slowMo,
|
||||
opts.protocolTimeout
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async createCDPPipeConnection(
|
||||
browserProcess: ReturnType<typeof launch>,
|
||||
opts: {timeout: number; protocolTimeout: number | undefined; slowMo: number}
|
||||
): Promise<Connection> {
|
||||
// stdio was assigned during start(), and the 'pipe' option there adds the
|
||||
// 4th and 5th items to stdio array
|
||||
const {3: pipeWrite, 4: pipeRead} = browserProcess.nodeProcess.stdio;
|
||||
const transport = new PipeTransport(
|
||||
pipeWrite as NodeJS.WritableStream,
|
||||
pipeRead as NodeJS.ReadableStream
|
||||
);
|
||||
return new Connection('', transport, opts.slowMo, opts.protocolTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async createBiDiOverCDPBrowser(
|
||||
browserProcess: ReturnType<typeof launch>,
|
||||
connection: Connection,
|
||||
closeCallback: BrowserCloseCallback
|
||||
): Promise<Browser> {
|
||||
const BiDi = await import(
|
||||
/* webpackIgnore: true */ '../common/bidi/bidi.js'
|
||||
);
|
||||
const bidiConnection = await BiDi.connectBidiOverCDP(connection);
|
||||
return await BiDi.Browser.create({
|
||||
connection: bidiConnection,
|
||||
closeCallback,
|
||||
process: browserProcess.nodeProcess,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async createBiDiBrowser(
|
||||
browserProcess: ReturnType<typeof launch>,
|
||||
closeCallback: BrowserCloseCallback,
|
||||
opts: {timeout: number; protocolTimeout: number | undefined; slowMo: number}
|
||||
): Promise<Browser> {
|
||||
const browserWSEndpoint =
|
||||
(await browserProcess.waitForLineOutput(
|
||||
WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
|
||||
opts.timeout
|
||||
)) + '/session';
|
||||
const transport = await WebSocketTransport.create(browserWSEndpoint);
|
||||
const BiDi = await import(
|
||||
/* webpackIgnore: true */ '../common/bidi/bidi.js'
|
||||
);
|
||||
const bidiConnection = new BiDi.Connection(
|
||||
transport,
|
||||
opts.slowMo,
|
||||
opts.protocolTimeout
|
||||
);
|
||||
return await BiDi.Browser.create({
|
||||
connection: bidiConnection,
|
||||
closeCallback,
|
||||
process: browserProcess.nodeProcess,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
export * from './BrowserFetcher.js';
|
||||
export * from './BrowserRunner.js';
|
||||
export * from './ChromeLauncher.js';
|
||||
export * from './FirefoxLauncher.js';
|
||||
export * from './LaunchOptions.js';
|
||||
|
@ -104,8 +104,10 @@ export const describeInstallation = (
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (process.env['KEEP_SANDBOX']) {
|
||||
if (!process.env['KEEP_SANDBOX']) {
|
||||
await rm(sandbox, {recursive: true, force: true, maxRetries: 5});
|
||||
} else {
|
||||
console.log('sandbox saved in ' + sandbox);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -19,7 +19,7 @@ import {readAsset} from './util.js';
|
||||
|
||||
describeInstallation(
|
||||
'`puppeteer-core`',
|
||||
{dependencies: ['puppeteer-core']},
|
||||
{dependencies: ['@puppeteer/browsers', 'puppeteer-core']},
|
||||
({itEvaluates}) => {
|
||||
itEvaluates('CommonJS', {commonjs: true}, async () => {
|
||||
return readAsset('puppeteer-core', 'requires.cjs');
|
||||
|
@ -462,10 +462,13 @@ describe('Launcher specs', function () {
|
||||
const options = Object.assign({}, defaultBrowserOptions);
|
||||
options.ignoreDefaultArgs = true;
|
||||
const browser = await puppeteer.launch(options);
|
||||
const page = await browser.newPage();
|
||||
expect(await page.evaluate('11 * 11')).toBe(121);
|
||||
await page.close();
|
||||
await browser.close();
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
expect(await page.evaluate('11 * 11')).toBe(121);
|
||||
await page.close();
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
});
|
||||
it('should filter out ignored default arguments in Chrome', async () => {
|
||||
const {defaultBrowserOptions, puppeteer} = getTestState();
|
||||
|
Loading…
Reference in New Issue
Block a user