chore: split Launcher.ts (#8544)

This commit is contained in:
jrandolf 2022-06-23 00:13:39 +02:00 committed by GitHub
parent 70c7f64a58
commit 347101883f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 516 additions and 508 deletions

View File

@ -61,7 +61,7 @@ export * from './common/Page.js';
export * from './common/Product.js'; export * from './common/Product.js';
export * from './common/Puppeteer.js'; export * from './common/Puppeteer.js';
export * from './common/BrowserConnector.js'; export * from './common/BrowserConnector.js';
export * from './node/Launcher.js'; export * from './node/ProductLauncher.js';
export * from './node/LaunchOptions.js'; export * from './node/LaunchOptions.js';
export * from './common/HTTPRequest.js'; export * from './common/HTTPRequest.js';
export * from './common/HTTPResponse.js'; export * from './common/HTTPResponse.js';

263
src/node/ChromeLauncher.ts Normal file
View File

@ -0,0 +1,263 @@
import fs from 'fs';
import path from 'path';
import {assert} from '../common/assert.js';
import {Browser} from '../common/Browser.js';
import {Product} from '../common/Product.js';
import {BrowserRunner} from './BrowserRunner.js';
import {
BrowserLaunchArgumentOptions,
ChromeReleaseChannel,
PuppeteerNodeLaunchOptions,
} from './LaunchOptions.js';
import {
executablePathForChannel,
ProductLauncher,
resolveExecutablePath,
} from './ProductLauncher.js';
import {tmpdir} from './util.js';
/**
* @internal
*/
export class ChromeLauncher implements ProductLauncher {
/**
* @internal
*/
_projectRoot: string | undefined;
/**
* @internal
*/
_preferredRevision: string;
/**
* @internal
*/
_isPuppeteerCore: boolean;
constructor(
projectRoot: string | undefined,
preferredRevision: string,
isPuppeteerCore: boolean
) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
}
async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> {
const {
ignoreDefaultArgs = false,
args = [],
dumpio = false,
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,
} = options;
const chromeArguments = [];
if (!ignoreDefaultArgs) {
chromeArguments.push(...this.defaultArgs(options));
} else if (Array.isArray(ignoreDefaultArgs)) {
chromeArguments.push(
...this.defaultArgs(options).filter(arg => {
return !ignoreDefaultArgs.includes(arg);
})
);
} else {
chromeArguments.push(...args);
}
if (
!chromeArguments.some(argument => {
return argument.startsWith('--remote-debugging-');
})
) {
if (pipe) {
assert(
!debuggingPort,
'Browser should be launched with either pipe or debugging port - not both.'
);
chromeArguments.push('--remote-debugging-pipe');
} else {
chromeArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
}
}
let isTempUserDataDir = true;
// Check for the user data dir argument, which will always be set even
// with a custom directory specified via the userDataDir option.
let userDataDirIndex = chromeArguments.findIndex(arg => {
return arg.startsWith('--user-data-dir');
});
if (userDataDirIndex < 0) {
chromeArguments.push(
`--user-data-dir=${await fs.promises.mkdtemp(
path.join(tmpdir(), 'puppeteer_dev_chrome_profile-')
)}`
);
userDataDirIndex = chromeArguments.length - 1;
}
const userDataDir = chromeArguments[userDataDirIndex]!.split('=', 2)[1];
assert(typeof userDataDir === 'string', '`--user-data-dir` is malformed');
isTempUserDataDir = false;
let chromeExecutable = executablePath;
if (channel) {
// executablePath is detected by channel, so it should not be specified by user.
assert(
!chromeExecutable,
'`executablePath` must not be specified when `channel` is given.'
);
chromeExecutable = executablePathForChannel(channel);
} else if (!chromeExecutable) {
const {missingText, executablePath} = resolveExecutablePath(this);
if (missingText) {
throw new Error(missingText);
}
chromeExecutable = executablePath;
}
const usePipe = chromeArguments.includes('--remote-debugging-pipe');
const runner = new BrowserRunner(
this.product,
chromeExecutable,
chromeArguments,
userDataDir,
isTempUserDataDir
);
runner.start({
handleSIGHUP,
handleSIGTERM,
handleSIGINT,
dumpio,
env,
pipe: usePipe,
});
let browser;
try {
const connection = await runner.setupConnection({
usePipe,
timeout,
slowMo,
preferredRevision: this._preferredRevision,
});
browser = await Browser._create(
connection,
[],
ignoreHTTPSErrors,
defaultViewport,
runner.proc,
runner.close.bind(runner)
);
} catch (error) {
runner.kill();
throw error;
}
if (waitForInitialPage) {
try {
await browser.waitForTarget(
t => {
return t.type() === 'page';
},
{timeout}
);
} catch (error) {
await browser.close();
throw error;
}
}
return browser;
}
defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] {
const chromeArguments = [
'--allow-pre-commit-input',
'--disable-background-networking',
'--enable-features=NetworkServiceInProcess2',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
// TODO: remove AvoidUnnecessaryBeforeUnloadCheckSync below
// once crbug.com/1324138 is fixed and released.
'--disable-features=Translate,BackForwardCache,AvoidUnnecessaryBeforeUnloadCheckSync',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-sync',
'--force-color-profile=srgb',
'--metrics-recording-only',
'--no-first-run',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain',
// TODO(sadym): remove '--enable-blink-features=IdleDetection'
// once IdleDetection is turned on by default.
'--enable-blink-features=IdleDetection',
'--export-tagged-pdf',
];
const {
devtools = false,
headless = !devtools,
args = [],
userDataDir,
} = options;
if (userDataDir) {
chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`);
}
if (devtools) {
chromeArguments.push('--auto-open-devtools-for-tabs');
}
if (headless) {
chromeArguments.push(
headless === 'chrome' ? '--headless=chrome' : '--headless',
'--hide-scrollbars',
'--mute-audio'
);
}
if (
args.every(arg => {
return arg.startsWith('-');
})
) {
chromeArguments.push('about:blank');
}
chromeArguments.push(...args);
return chromeArguments;
}
executablePath(channel?: ChromeReleaseChannel): string {
if (channel) {
return executablePathForChannel(channel);
} else {
const results = resolveExecutablePath(this);
return results.executablePath;
}
}
get product(): Product {
return 'chrome';
}
}

View File

@ -1,305 +1,22 @@
/** import fs from 'fs';
* Copyright 2017 Google Inc. All rights reserved. import os from 'os';
* import path from 'path';
* 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 * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import {assert} from '../common/assert.js'; import {assert} from '../common/assert.js';
import {BrowserFetcher} from './BrowserFetcher.js';
import {Browser} from '../common/Browser.js'; import {Browser} from '../common/Browser.js';
import {Product} from '../common/Product.js';
import {BrowserFetcher} from './BrowserFetcher.js';
import {BrowserRunner} from './BrowserRunner.js'; import {BrowserRunner} from './BrowserRunner.js';
import {promisify} from 'util';
const copyFileAsync = promisify(fs.copyFile);
const mkdtempAsync = promisify(fs.mkdtemp);
const writeFileAsync = promisify(fs.writeFile);
import { import {
BrowserLaunchArgumentOptions, BrowserLaunchArgumentOptions,
ChromeReleaseChannel,
PuppeteerNodeLaunchOptions, PuppeteerNodeLaunchOptions,
} from './LaunchOptions.js'; } from './LaunchOptions.js';
import {ProductLauncher, resolveExecutablePath} from './ProductLauncher.js';
import {Product} from '../common/Product.js'; import {tmpdir} from './util.js';
const tmpDir = () => {
return process.env['PUPPETEER_TMP_DIR'] || os.tmpdir();
};
/**
* Describes a launcher - a class that is able to create and launch a browser instance.
* @public
*/
export interface ProductLauncher {
launch(object: PuppeteerNodeLaunchOptions): Promise<Browser>;
executablePath: (path?: any) => string;
defaultArgs(object: BrowserLaunchArgumentOptions): string[];
product: Product;
}
/** /**
* @internal * @internal
*/ */
class ChromeLauncher implements ProductLauncher { export class FirefoxLauncher implements ProductLauncher {
/**
* @internal
*/
_projectRoot: string | undefined;
/**
* @internal
*/
_preferredRevision: string;
/**
* @internal
*/
_isPuppeteerCore: boolean;
constructor(
projectRoot: string | undefined,
preferredRevision: string,
isPuppeteerCore: boolean
) {
this._projectRoot = projectRoot;
this._preferredRevision = preferredRevision;
this._isPuppeteerCore = isPuppeteerCore;
}
async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> {
const {
ignoreDefaultArgs = false,
args = [],
dumpio = false,
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,
} = options;
const chromeArguments = [];
if (!ignoreDefaultArgs) {
chromeArguments.push(...this.defaultArgs(options));
} else if (Array.isArray(ignoreDefaultArgs)) {
chromeArguments.push(
...this.defaultArgs(options).filter(arg => {
return !ignoreDefaultArgs.includes(arg);
})
);
} else {
chromeArguments.push(...args);
}
if (
!chromeArguments.some(argument => {
return argument.startsWith('--remote-debugging-');
})
) {
if (pipe) {
assert(
!debuggingPort,
'Browser should be launched with either pipe or debugging port - not both.'
);
chromeArguments.push('--remote-debugging-pipe');
} else {
chromeArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
}
}
let isTempUserDataDir = true;
// Check for the user data dir argument, which will always be set even
// with a custom directory specified via the userDataDir option.
let userDataDirIndex = chromeArguments.findIndex(arg => {
return arg.startsWith('--user-data-dir');
});
if (userDataDirIndex < 0) {
chromeArguments.push(
`--user-data-dir=${await mkdtempAsync(
path.join(tmpDir(), 'puppeteer_dev_chrome_profile-')
)}`
);
userDataDirIndex = chromeArguments.length - 1;
}
const userDataDir = chromeArguments[userDataDirIndex]!.split('=', 2)[1];
assert(typeof userDataDir === 'string', '`--user-data-dir` is malformed');
isTempUserDataDir = false;
let chromeExecutable = executablePath;
if (channel) {
// executablePath is detected by channel, so it should not be specified by user.
assert(
!chromeExecutable,
'`executablePath` must not be specified when `channel` is given.'
);
chromeExecutable = executablePathForChannel(channel);
} else if (!chromeExecutable) {
const {missingText, executablePath} = resolveExecutablePath(this);
if (missingText) {
throw new Error(missingText);
}
chromeExecutable = executablePath;
}
const usePipe = chromeArguments.includes('--remote-debugging-pipe');
const runner = new BrowserRunner(
this.product,
chromeExecutable,
chromeArguments,
userDataDir,
isTempUserDataDir
);
runner.start({
handleSIGHUP,
handleSIGTERM,
handleSIGINT,
dumpio,
env,
pipe: usePipe,
});
let browser;
try {
const connection = await runner.setupConnection({
usePipe,
timeout,
slowMo,
preferredRevision: this._preferredRevision,
});
browser = await Browser._create(
connection,
[],
ignoreHTTPSErrors,
defaultViewport,
runner.proc,
runner.close.bind(runner)
);
} catch (error) {
runner.kill();
throw error;
}
if (waitForInitialPage) {
try {
await browser.waitForTarget(
t => {
return t.type() === 'page';
},
{timeout}
);
} catch (error) {
await browser.close();
throw error;
}
}
return browser;
}
defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] {
const chromeArguments = [
'--allow-pre-commit-input', // TODO(crbug.com/1320996): neither headful nor headless should rely on this flag.
'--disable-background-networking',
'--enable-features=NetworkServiceInProcess2',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
// TODO: remove AvoidUnnecessaryBeforeUnloadCheckSync below
// once crbug.com/1324138 is fixed and released.
'--disable-features=Translate,BackForwardCache,AvoidUnnecessaryBeforeUnloadCheckSync',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-sync',
'--force-color-profile=srgb',
'--metrics-recording-only',
'--no-first-run',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain',
// TODO(sadym): remove '--enable-blink-features=IdleDetection'
// once IdleDetection is turned on by default.
'--enable-blink-features=IdleDetection',
'--export-tagged-pdf',
];
const {
devtools = false,
headless = !devtools,
args = [],
userDataDir,
} = options;
if (userDataDir) {
chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`);
}
if (devtools) {
chromeArguments.push('--auto-open-devtools-for-tabs');
}
if (headless) {
chromeArguments.push(
headless === 'chrome' ? '--headless=chrome' : '--headless',
'--hide-scrollbars',
'--mute-audio'
);
}
if (
args.every(arg => {
return arg.startsWith('-');
})
) {
chromeArguments.push('about:blank');
}
chromeArguments.push(...args);
return chromeArguments;
}
executablePath(channel?: ChromeReleaseChannel): string {
if (channel) {
return executablePathForChannel(channel);
} else {
const results = resolveExecutablePath(this);
return results.executablePath;
}
}
get product(): Product {
return 'chrome';
}
}
/**
* @internal
*/
class FirefoxLauncher implements ProductLauncher {
/** /**
* @internal * @internal
*/ */
@ -500,10 +217,13 @@ class FirefoxLauncher implements ProductLauncher {
const firefoxArguments = ['--no-remote']; const firefoxArguments = ['--no-remote'];
if (os.platform() === 'darwin') { switch (os.platform()) {
firefoxArguments.push('--foreground'); case 'darwin':
} else if (os.platform().startsWith('win')) { firefoxArguments.push('--foreground');
firefoxArguments.push('--wait-for-browser'); break;
case 'win32':
firefoxArguments.push('--wait-for-browser');
break;
} }
if (userDataDir) { if (userDataDir) {
firefoxArguments.push('--profile'); firefoxArguments.push('--profile');
@ -754,19 +474,22 @@ class FirefoxLauncher implements ProductLauncher {
return `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`; return `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`;
}); });
await writeFileAsync(path.join(profilePath, 'user.js'), lines.join('\n')); await fs.promises.writeFile(
path.join(profilePath, 'user.js'),
lines.join('\n')
);
// Create a backup of the preferences file if it already exitsts. // Create a backup of the preferences file if it already exitsts.
const prefsPath = path.join(profilePath, 'prefs.js'); const prefsPath = path.join(profilePath, 'prefs.js');
if (fs.existsSync(prefsPath)) { if (fs.existsSync(prefsPath)) {
const prefsBackupPath = path.join(profilePath, 'prefs.js.puppeteer'); const prefsBackupPath = path.join(profilePath, 'prefs.js.puppeteer');
await copyFileAsync(prefsPath, prefsBackupPath); await fs.promises.copyFile(prefsPath, prefsBackupPath);
} }
} }
async _createProfile(extraPrefs: {[x: string]: unknown}): Promise<string> { async _createProfile(extraPrefs: {[x: string]: unknown}): Promise<string> {
const temporaryProfilePath = await mkdtempAsync( const temporaryProfilePath = await fs.promises.mkdtemp(
path.join(tmpDir(), 'puppeteer_dev_firefox_profile-') path.join(tmpdir(), 'puppeteer_dev_firefox_profile-')
); );
const prefs = this.defaultPreferences(extraPrefs); const prefs = this.defaultPreferences(extraPrefs);
@ -775,186 +498,3 @@ class FirefoxLauncher implements ProductLauncher {
return temporaryProfilePath; return temporaryProfilePath;
} }
} }
function 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 {
fs.accessSync(chromePath);
} catch (error) {
throw new Error(
`Could not find Google Chrome executable for channel '${channel}' at '${chromePath}'.`
);
}
return chromePath;
}
function resolveExecutablePath(launcher: ChromeLauncher | FirefoxLauncher): {
executablePath: string;
missingText?: string;
} {
const {product, _isPuppeteerCore, _projectRoot, _preferredRevision} =
launcher;
let downloadPath: string | undefined;
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!_isPuppeteerCore) {
const executablePath =
process.env['PUPPETEER_EXECUTABLE_PATH'] ||
process.env['npm_config_puppeteer_executable_path'] ||
process.env['npm_package_config_puppeteer_executable_path'];
if (executablePath) {
const missingText = !fs.existsSync(executablePath)
? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' +
executablePath
: undefined;
return {executablePath, missingText};
}
const ubuntuChromiumPath = '/usr/bin/chromium-browser';
if (
product === 'chrome' &&
os.platform() !== 'darwin' &&
os.arch() === 'arm64' &&
fs.existsSync(ubuntuChromiumPath)
) {
return {executablePath: ubuntuChromiumPath, missingText: undefined};
}
downloadPath =
process.env['PUPPETEER_DOWNLOAD_PATH'] ||
process.env['npm_config_puppeteer_download_path'] ||
process.env['npm_package_config_puppeteer_download_path'];
}
if (!_projectRoot) {
throw new Error(
'_projectRoot is undefined. Unable to create a BrowserFetcher.'
);
}
const browserFetcher = new BrowserFetcher(_projectRoot, {
product: product,
path: downloadPath,
});
if (!_isPuppeteerCore && product === 'chrome') {
const revision = process.env['PUPPETEER_CHROMIUM_REVISION'];
if (revision) {
const revisionInfo = browserFetcher.revisionInfo(revision);
const missingText = !revisionInfo.local
? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' +
revisionInfo.executablePath
: undefined;
return {executablePath: revisionInfo.executablePath, missingText};
}
}
const revisionInfo = browserFetcher.revisionInfo(_preferredRevision);
const firefoxHelp = `Run \`PUPPETEER_PRODUCT=firefox npm install\` to download a supported Firefox browser binary.`;
const chromeHelp = `Run \`npm install\` to download the correct Chromium revision (${launcher._preferredRevision}).`;
const missingText = !revisionInfo.local
? `Could not find expected browser (${product}) locally. ${
product === 'chrome' ? chromeHelp : firefoxHelp
}`
: undefined;
return {executablePath: revisionInfo.executablePath, missingText};
}
/**
* @internal
*/
export default function Launcher(
projectRoot: string | undefined,
preferredRevision: string,
isPuppeteerCore: boolean,
product?: string
): ProductLauncher {
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!product && !isPuppeteerCore) {
product =
process.env['PUPPETEER_PRODUCT'] ||
process.env['npm_config_puppeteer_product'] ||
process.env['npm_package_config_puppeteer_product'];
}
switch (product) {
case 'firefox':
return new FirefoxLauncher(
projectRoot,
preferredRevision,
isPuppeteerCore
);
case 'chrome':
default:
if (typeof product !== 'undefined' && product !== 'chrome') {
/* The user gave us an incorrect product name
* we'll default to launching Chrome, but log to the console
* to let the user know (they've probably typoed).
*/
console.warn(
`Warning: unknown product name ${product}. Falling back to chrome.`
);
}
return new ChromeLauncher(
projectRoot,
preferredRevision,
isPuppeteerCore
);
}
}

211
src/node/ProductLauncher.ts Normal file
View File

@ -0,0 +1,211 @@
/**
* Copyright 2017 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 os from 'os';
import {Browser} from '../common/Browser.js';
import {BrowserFetcher} from './BrowserFetcher.js';
import {
BrowserLaunchArgumentOptions,
ChromeReleaseChannel,
PuppeteerNodeLaunchOptions,
} from './LaunchOptions.js';
import {Product} from '../common/Product.js';
import {ChromeLauncher} from './ChromeLauncher.js';
import {FirefoxLauncher} from './FirefoxLauncher.js';
import {accessSync, existsSync} from 'fs';
/**
* Describes a launcher - a class that is able to create and launch a browser instance.
* @public
*/
export interface ProductLauncher {
launch(object: PuppeteerNodeLaunchOptions): Promise<Browser>;
executablePath: (path?: any) => string;
defaultArgs(object: BrowserLaunchArgumentOptions): string[];
product: Product;
}
export function 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;
}
export function resolveExecutablePath(
launcher: ChromeLauncher | FirefoxLauncher
): {
executablePath: string;
missingText?: string;
} {
const {product, _isPuppeteerCore, _projectRoot, _preferredRevision} =
launcher;
let downloadPath: string | undefined;
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!_isPuppeteerCore) {
const executablePath =
process.env['PUPPETEER_EXECUTABLE_PATH'] ||
process.env['npm_config_puppeteer_executable_path'] ||
process.env['npm_package_config_puppeteer_executable_path'];
if (executablePath) {
const missingText = !existsSync(executablePath)
? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' +
executablePath
: undefined;
return {executablePath, missingText};
}
const ubuntuChromiumPath = '/usr/bin/chromium-browser';
if (
product === 'chrome' &&
os.platform() !== 'darwin' &&
os.arch() === 'arm64' &&
existsSync(ubuntuChromiumPath)
) {
return {executablePath: ubuntuChromiumPath, missingText: undefined};
}
downloadPath =
process.env['PUPPETEER_DOWNLOAD_PATH'] ||
process.env['npm_config_puppeteer_download_path'] ||
process.env['npm_package_config_puppeteer_download_path'];
}
if (!_projectRoot) {
throw new Error(
'_projectRoot is undefined. Unable to create a BrowserFetcher.'
);
}
const browserFetcher = new BrowserFetcher(_projectRoot, {
product: product,
path: downloadPath,
});
if (!_isPuppeteerCore && product === 'chrome') {
const revision = process.env['PUPPETEER_CHROMIUM_REVISION'];
if (revision) {
const revisionInfo = browserFetcher.revisionInfo(revision);
const missingText = !revisionInfo.local
? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' +
revisionInfo.executablePath
: undefined;
return {executablePath: revisionInfo.executablePath, missingText};
}
}
const revisionInfo = browserFetcher.revisionInfo(_preferredRevision);
const firefoxHelp = `Run \`PUPPETEER_PRODUCT=firefox npm install\` to download a supported Firefox browser binary.`;
const chromeHelp = `Run \`npm install\` to download the correct Chromium revision (${launcher._preferredRevision}).`;
const missingText = !revisionInfo.local
? `Could not find expected browser (${product}) locally. ${
product === 'chrome' ? chromeHelp : firefoxHelp
}`
: undefined;
return {executablePath: revisionInfo.executablePath, missingText};
}
/**
* @internal
*/
export function createLauncher(
projectRoot: string | undefined,
preferredRevision: string,
isPuppeteerCore: boolean,
product: Product = 'chrome'
): ProductLauncher {
switch (product) {
case 'firefox':
return new FirefoxLauncher(
projectRoot,
preferredRevision,
isPuppeteerCore
);
case 'chrome':
return new ChromeLauncher(
projectRoot,
preferredRevision,
isPuppeteerCore
);
}
}

View File

@ -23,7 +23,7 @@ import {BrowserFetcher, BrowserFetcherOptions} from './BrowserFetcher.js';
import {LaunchOptions, BrowserLaunchArgumentOptions} from './LaunchOptions.js'; import {LaunchOptions, BrowserLaunchArgumentOptions} from './LaunchOptions.js';
import {BrowserConnectOptions} from '../common/BrowserConnector.js'; import {BrowserConnectOptions} from '../common/BrowserConnector.js';
import {Browser} from '../common/Browser.js'; import {Browser} from '../common/Browser.js';
import Launcher, {ProductLauncher} from './Launcher.js'; import {createLauncher, ProductLauncher} from './ProductLauncher.js';
import {PUPPETEER_REVISIONS} from '../revisions.js'; import {PUPPETEER_REVISIONS} from '../revisions.js';
import {Product} from '../common/Product.js'; import {Product} from '../common/Product.js';
@ -74,7 +74,7 @@ export interface PuppeteerLaunchOptions
* @public * @public
*/ */
export class PuppeteerNode extends Puppeteer { export class PuppeteerNode extends Puppeteer {
#lazyLauncher?: ProductLauncher; #launcher?: ProductLauncher;
#projectRoot?: string; #projectRoot?: string;
#productName?: Product; #productName?: Product;
@ -187,8 +187,8 @@ export class PuppeteerNode extends Puppeteer {
*/ */
get _launcher(): ProductLauncher { get _launcher(): ProductLauncher {
if ( if (
!this.#lazyLauncher || !this.#launcher ||
this.#lazyLauncher.product !== this._productName || this.#launcher.product !== this._productName ||
this._changedProduct this._changedProduct
) { ) {
switch (this._productName) { switch (this._productName) {
@ -200,14 +200,14 @@ export class PuppeteerNode extends Puppeteer {
this._preferredRevision = PUPPETEER_REVISIONS.chromium; this._preferredRevision = PUPPETEER_REVISIONS.chromium;
} }
this._changedProduct = false; this._changedProduct = false;
this.#lazyLauncher = Launcher( this.#launcher = createLauncher(
this.#projectRoot, this.#projectRoot,
this._preferredRevision, this._preferredRevision,
this._isPuppeteerCore, this._isPuppeteerCore,
this._productName this._productName
); );
} }
return this.#lazyLauncher; return this.#launcher;
} }
/** /**

13
src/node/util.ts Normal file
View File

@ -0,0 +1,13 @@
import * as os from 'os';
/**
* Gets the temporary directory, either from the environmental variable
* `PUPPETEER_TMP_DIR` or the `os.tmpdir`.
*
* @returns The temporary directory path.
*
* @internal
*/
export const tmpdir = (): string => {
return process.env['PUPPETEER_TMP_DIR'] || os.tmpdir();
};

View File

@ -624,25 +624,6 @@ describe('Launcher specs', function () {
expect(userAgent).toContain('Chrome'); expect(userAgent).toContain('Chrome');
}); });
itOnlyRegularInstall(
'falls back to launching chrome if there is an unknown product but logs a warning',
async () => {
const {puppeteer} = getTestState();
const consoleStub = sinon.stub(console, 'warn');
const browser = await puppeteer.launch({
// @ts-expect-error purposeful bad input
product: 'SO_NOT_A_PRODUCT',
});
const userAgent = await browser.userAgent();
await browser.close();
expect(userAgent).toContain('Chrome');
expect(consoleStub.callCount).toEqual(1);
expect(consoleStub.firstCall.args).toEqual([
'Warning: unknown product name SO_NOT_A_PRODUCT. Falling back to chrome.',
]);
}
);
itOnlyRegularInstall( itOnlyRegularInstall(
'should be able to launch Firefox', 'should be able to launch Firefox',
async function () { async function () {