From 347101883f159acd2ee4fbfad017998f02985195 Mon Sep 17 00:00:00 2001 From: jrandolf <101637635+jrandolf@users.noreply.github.com> Date: Thu, 23 Jun 2022 00:13:39 +0200 Subject: [PATCH] chore: split `Launcher.ts` (#8544) --- src/api-docs-entry.ts | 2 +- src/node/ChromeLauncher.ts | 263 ++++++++++ src/node/{Launcher.ts => FirefoxLauncher.ts} | 504 +------------------ src/node/ProductLauncher.ts | 211 ++++++++ src/node/Puppeteer.ts | 12 +- src/node/util.ts | 13 + test/src/launcher.spec.ts | 19 - 7 files changed, 516 insertions(+), 508 deletions(-) create mode 100644 src/node/ChromeLauncher.ts rename src/node/{Launcher.ts => FirefoxLauncher.ts} (51%) create mode 100644 src/node/ProductLauncher.ts create mode 100644 src/node/util.ts diff --git a/src/api-docs-entry.ts b/src/api-docs-entry.ts index c3a7c02c..dbcf1438 100644 --- a/src/api-docs-entry.ts +++ b/src/api-docs-entry.ts @@ -61,7 +61,7 @@ export * from './common/Page.js'; export * from './common/Product.js'; export * from './common/Puppeteer.js'; export * from './common/BrowserConnector.js'; -export * from './node/Launcher.js'; +export * from './node/ProductLauncher.js'; export * from './node/LaunchOptions.js'; export * from './common/HTTPRequest.js'; export * from './common/HTTPResponse.js'; diff --git a/src/node/ChromeLauncher.ts b/src/node/ChromeLauncher.ts new file mode 100644 index 00000000..26b8ef28 --- /dev/null +++ b/src/node/ChromeLauncher.ts @@ -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 { + 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'; + } +} diff --git a/src/node/Launcher.ts b/src/node/FirefoxLauncher.ts similarity index 51% rename from src/node/Launcher.ts rename to src/node/FirefoxLauncher.ts index 7d152723..6a4316a3 100644 --- a/src/node/Launcher.ts +++ b/src/node/FirefoxLauncher.ts @@ -1,305 +1,22 @@ -/** - * 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 * as os from 'os'; -import * as path from 'path'; -import * as fs from 'fs'; - +import fs from 'fs'; +import os from 'os'; +import path from 'path'; import {assert} from '../common/assert.js'; -import {BrowserFetcher} from './BrowserFetcher.js'; import {Browser} from '../common/Browser.js'; +import {Product} from '../common/Product.js'; +import {BrowserFetcher} from './BrowserFetcher.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 { BrowserLaunchArgumentOptions, - ChromeReleaseChannel, PuppeteerNodeLaunchOptions, } from './LaunchOptions.js'; - -import {Product} from '../common/Product.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; - executablePath: (path?: any) => string; - defaultArgs(object: BrowserLaunchArgumentOptions): string[]; - product: Product; -} +import {ProductLauncher, resolveExecutablePath} from './ProductLauncher.js'; +import {tmpdir} from './util.js'; /** * @internal */ -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 { - 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 { +export class FirefoxLauncher implements ProductLauncher { /** * @internal */ @@ -500,10 +217,13 @@ class FirefoxLauncher implements ProductLauncher { const firefoxArguments = ['--no-remote']; - if (os.platform() === 'darwin') { - firefoxArguments.push('--foreground'); - } else if (os.platform().startsWith('win')) { - firefoxArguments.push('--wait-for-browser'); + switch (os.platform()) { + case 'darwin': + firefoxArguments.push('--foreground'); + break; + case 'win32': + firefoxArguments.push('--wait-for-browser'); + break; } if (userDataDir) { firefoxArguments.push('--profile'); @@ -754,19 +474,22 @@ class FirefoxLauncher implements ProductLauncher { 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. const prefsPath = path.join(profilePath, 'prefs.js'); if (fs.existsSync(prefsPath)) { 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 { - const temporaryProfilePath = await mkdtempAsync( - path.join(tmpDir(), 'puppeteer_dev_firefox_profile-') + const temporaryProfilePath = await fs.promises.mkdtemp( + path.join(tmpdir(), 'puppeteer_dev_firefox_profile-') ); const prefs = this.defaultPreferences(extraPrefs); @@ -775,186 +498,3 @@ class FirefoxLauncher implements ProductLauncher { 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 - ); - } -} diff --git a/src/node/ProductLauncher.ts b/src/node/ProductLauncher.ts new file mode 100644 index 00000000..8d5c233d --- /dev/null +++ b/src/node/ProductLauncher.ts @@ -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; + 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 + ); + } +} diff --git a/src/node/Puppeteer.ts b/src/node/Puppeteer.ts index 539845d8..fb9af91a 100644 --- a/src/node/Puppeteer.ts +++ b/src/node/Puppeteer.ts @@ -23,7 +23,7 @@ import {BrowserFetcher, BrowserFetcherOptions} from './BrowserFetcher.js'; import {LaunchOptions, BrowserLaunchArgumentOptions} from './LaunchOptions.js'; import {BrowserConnectOptions} from '../common/BrowserConnector.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 {Product} from '../common/Product.js'; @@ -74,7 +74,7 @@ export interface PuppeteerLaunchOptions * @public */ export class PuppeteerNode extends Puppeteer { - #lazyLauncher?: ProductLauncher; + #launcher?: ProductLauncher; #projectRoot?: string; #productName?: Product; @@ -187,8 +187,8 @@ export class PuppeteerNode extends Puppeteer { */ get _launcher(): ProductLauncher { if ( - !this.#lazyLauncher || - this.#lazyLauncher.product !== this._productName || + !this.#launcher || + this.#launcher.product !== this._productName || this._changedProduct ) { switch (this._productName) { @@ -200,14 +200,14 @@ export class PuppeteerNode extends Puppeteer { this._preferredRevision = PUPPETEER_REVISIONS.chromium; } this._changedProduct = false; - this.#lazyLauncher = Launcher( + this.#launcher = createLauncher( this.#projectRoot, this._preferredRevision, this._isPuppeteerCore, this._productName ); } - return this.#lazyLauncher; + return this.#launcher; } /** diff --git a/src/node/util.ts b/src/node/util.ts new file mode 100644 index 00000000..c362a39e --- /dev/null +++ b/src/node/util.ts @@ -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(); +}; diff --git a/test/src/launcher.spec.ts b/test/src/launcher.spec.ts index cdc38dc5..4c934063 100644 --- a/test/src/launcher.spec.ts +++ b/test/src/launcher.spec.ts @@ -624,25 +624,6 @@ describe('Launcher specs', function () { 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( 'should be able to launch Firefox', async function () {