/**
 * 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 { assert } from '../common/assert.js' ; import { BrowserFetcher } from './BrowserFetcher.js' ; import { Browser } from '../common/Browser.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 = ( ) => process . env . PUPPETEER_TMP_DIR || os . tmpdir ( ) ; public export interface ProductLauncher { launch ( object ) ; executablePath : ( string ? ) => string ; defaultArgs ( object ) ; product : Product ; } internal class ChromeLauncher implements ProductLauncher { _projectRoot : string ; _preferredRevision : string ; _isPuppeteerCore : boolean ; constructor ( projectRoot , preferredRevision , isPuppeteerCore ) { this . _projectRoot = projectRoot ; this . _preferredRevision = preferredRevision ; this . _isPuppeteerCore = isPuppeteerCore ; } async launch ( options = { } ) { const { ignoreDefaultArgs = false , args = [ ] , dumpio = false , channel = null , executablePath = null , 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 = null , } = options ; const chromeArguments = [ ] ; if ( ! ignoreDefaultArgs ) chromeArguments . push ( ... this . defaultArgs ( options ) ) ; else if ( Array . isArray ( ignoreDefaultArgs ) ) chromeArguments . push ( ... this . defaultArgs ( options ) . filter ( ( arg ) => ! ignoreDefaultArgs . includes ( arg ) ) ) ; else chromeArguments . push ( ... args ) ; if ( ! chromeArguments . some ( ( argument ) => argument . startsWith ( '--remote-debugging-' ) ) ) { if ( pipe ) { assert ( debuggingPort === null , '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 userDataDir ; let isTempUserDataDir = true ; const userDataDirIndex = chromeArguments . findIndex ( ( arg ) => { return arg . startsWith ( '--user-data-dir' ) ; } ) ; if ( userDataDirIndex !== - 1 ) { userDataDir = chromeArguments [ userDataDirIndex ] . split ( '=' ) [ 1 ] ; if ( ! fs . existsSync ( userDataDir ) ) { throw new Error ( `Chrome user data dir not found at '${ userDataDir }'` ) ; } isTempUserDataDir = false ; } else { userDataDir = await mkdtempAsync ( path . join ( tmpDir ( ) , 'puppeteer_dev_chrome_profile-' ) ) ; chromeArguments . push ( `--user-data-dir=${ userDataDir }` ) ; } let chromeExecutable = executablePath ; if ( channel ) { assert ( ! executablePath , '`executablePath` must not be specified when `channel` is given.' ) ; chromeExecutable = executablePathForChannel ( channel ) ; } else if ( ! executablePath ) { if ( os . platform ( ) !== 'darwin' && os . arch ( ) === 'arm64' ) { chromeExecutable = '/usr/bin/chromium-browser' ; } else { 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 ) => t . type ( ) === 'page' , { timeout } ) ; } catch ( error ) { await browser . close ( ) ; throw error ; } } return browser ; } defaultArgs ( options = { } ) { const chromeArguments = [ '--disable-background-networking' , '--enable-features=NetworkService,NetworkServiceInProcess' , '--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' , '--disable-features=Translate' , '--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' , '--enable-blink-features=IdleDetection' , '--export-tagged-pdf' , ] ; const { devtools = false , headless = ! devtools , args = [ ] , userDataDir = null , } = 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' , '--hide-scrollbars' , '--mute-audio' ) ; } if ( args . every ( ( arg ) => arg . startsWith ( '-' ) ) ) chromeArguments . push ( 'about:blank' ) ; chromeArguments . push ( ... args ) ; return chromeArguments ; } executablePath ( channel ? ) { if ( channel ) { return executablePathForChannel ( channel ) ; } else { return resolveExecutablePath ( this ) . executablePath ; } } get product ( ) : Product { return 'chrome' ; } } internal class FirefoxLauncher implements ProductLauncher { _projectRoot : string ; _preferredRevision : string ; _isPuppeteerCore : boolean ; constructor ( projectRoot , preferredRevision , isPuppeteerCore ) { this . _projectRoot = projectRoot ; this . _preferredRevision = preferredRevision ; this . _isPuppeteerCore = isPuppeteerCore ; } async launch ( options = { } ) { const { ignoreDefaultArgs = false , args = [ ] , dumpio = false , executablePath = null , 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 , } = options ; const firefoxArguments = [ ] ; if ( ! ignoreDefaultArgs ) firefoxArguments . push ( ... this . defaultArgs ( options ) ) ; else if ( Array . isArray ( ignoreDefaultArgs ) ) firefoxArguments . push ( ... this . defaultArgs ( options ) . filter ( ( arg ) => ! ignoreDefaultArgs . includes ( arg ) ) ) ; else firefoxArguments . push ( ... args ) ; if ( ! firefoxArguments . some ( ( argument ) => argument . startsWith ( '--remote-debugging-' ) ) ) { if ( pipe ) { assert ( debuggingPort === null , 'Browser should be launched with either pipe or debugging port - not both.' ) ; } firefoxArguments . push ( `--remote-debugging-port=${ debuggingPort || 0 }` ) ; } let userDataDir = null ; let isTempUserDataDir = true ; const profileArgIndex = firefoxArguments . findIndex ( ( arg ) => { return [ '-profile' , '--profile' ] . includes ( arg ) ; } ) ; if ( profileArgIndex !== - 1 ) { userDataDir = firefoxArguments [ profileArgIndex + 1 ] ; if ( ! fs . existsSync ( userDataDir ) ) { throw new Error ( `Firefox profile not found at '${ userDataDir }'` ) ; } isTempUserDataDir = false ; const prefs = this . defaultPreferences ( extraPrefsFirefox ) ; this . writePreferences ( prefs , userDataDir ) ; } else { userDataDir = await this . _createProfile ( extraPrefsFirefox ) ; firefoxArguments . push ( '--profile' ) ; firefoxArguments . push ( userDataDir ) ; } await this . _updateRevision ( ) ; let firefoxExecutable = executablePath ; if ( ! executablePath ) { const { missingText , executablePath } = resolveExecutablePath ( this ) ; if ( missingText ) throw new Error ( missingText ) ; firefoxExecutable = executablePath ; } const runner = new BrowserRunner ( this . product , firefoxExecutable , firefoxArguments , userDataDir , isTempUserDataDir ) ; runner . start ( { handleSIGHUP , handleSIGTERM , handleSIGINT , dumpio , env , pipe , } ) ; let browser ; try { const connection = await runner . setupConnection ( { usePipe : pipe , 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 ) => t . type ( ) === 'page' , { timeout } ) ; } catch ( error ) { await browser . close ( ) ; throw error ; } } return browser ; } executablePath ( ) { return resolveExecutablePath ( this ) . executablePath ; } async _updateRevision ( ) { if ( this . _preferredRevision === 'latest' ) { const browserFetcher = new BrowserFetcher ( this . _projectRoot , { product : this . product , } ) ; const localRevisions = await browserFetcher . localRevisions ( ) ; if ( localRevisions [ 0 ] ) this . _preferredRevision = localRevisions [ 0 ] ; } } get product ( ) : Product { return 'firefox' ; } defaultArgs ( options = { } ) { const { devtools = false , headless = ! devtools , args = [ ] , userDataDir = null , } = options ; const firefoxArguments = [ '--no-remote' ] ; if ( os . platform ( ) === 'darwin' ) firefoxArguments . push ( '--foreground' ) ; else if ( os . platform ( ) . startsWith ( 'win' ) ) { firefoxArguments . push ( '--wait-for-browser' ) ; } if ( userDataDir ) { firefoxArguments . push ( '--profile' ) ; firefoxArguments . push ( userDataDir ) ; } if ( headless ) firefoxArguments . push ( '--headless' ) ; if ( devtools ) firefoxArguments . push ( '--devtools' ) ; if ( args . every ( ( arg ) => arg . startsWith ( '-' ) ) ) firefoxArguments . push ( 'about:blank' ) ; firefoxArguments . push ( ... args ) ; return firefoxArguments ; } defaultPreferences ( extraPrefs ) { const server = 'dummy.test' ; const defaultPrefs = { 'app.normandy.api_url' : '' , 'app.update.checkInstallTime' : false , 'app.update.disabledForTesting' : true , 'apz.content_response_timeout' : 60000 , 'browser.contentblocking.features.standard' : '-tp,tpPrivate,cookieBehavior0,-cm,-fp' , 'browser.dom.window.dump.enabled' : true , 'browser.newtabpage.activity-stream.feeds.system.topstories' : false , 'browser.newtabpage.enabled' : false , 'browser.pagethumbnails.capturing_disabled' : true , '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 , 'browser.search.update' : false , 'browser.sessionstore.resume_from_crash' : false , 'browser.shell.checkDefaultBrowser' : false , 'browser.startup.homepage' : 'about:blank' , 'browser.startup.homepage_override.mstone' : 'ignore' , 'browser.startup.page' : 0 , 'browser.tabs.disableBackgroundZombification' : false , 'browser.tabs.warnOnCloseOtherTabs' : false , 'browser.tabs.warnOnOpen' : false , 'browser.uitour.enabled' : false , 'browser.urlbar.suggest.searches' : false , 'browser.usedOnWindows10.introURL' : '' , 'browser.warnOnQuit' : false , '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 , 'datareporting.policy.dataSubmissionEnabled' : false , 'datareporting.policy.dataSubmissionPolicyBypassNotification' : true , 'devtools.jsonview.enabled' : false , 'dom.disable_open_during_load' : false , 'dom.file.createInChild' : true , 'dom.ipc.reportProcessHangs' : false , 'dom.max_chrome_script_run_time' : 0 , 'dom.max_script_run_time' : 0 , 'extensions.autoDisableScopes' : 0 , 'extensions.enabledScopes' : 5 , 'extensions.getAddons.cache.enabled' : false , 'extensions.installDistroAddons' : false , 'extensions.screenshots.disabled' : true , 'extensions.update.enabled' : false , 'extensions.update.notifyUser' : false , 'extensions.webservice.discoverURL' : `http://${ server }/dummy/discoveryURL` , 'fission.bfcacheInParent' : false , 'fission.webContentIsolationStrategy' : 0 , 'focusmanager.testmode' : true , 'general.useragent.updates.enabled' : false , 'geo.provider.testing' : true , 'geo.wifi.scan' : false , 'hangmonitor.timeout' : 0 , 'javascript.options.showInConsole' : true , 'media.gmp-manager.updateEnabled' : false , 'network.cookie.cookieBehavior' : 0 , 'network.cookie.sameSite.laxByDefault' : false , 'network.http.prompt-temp-redirect' : false , 'network.http.speculative-parallel-limit' : 0 , 'network.manage-offline-status' : false , 'network.sntp.pools' : server , 'plugin.state.flash' : 0 , 'privacy.trackingprotection.enabled' : false , 'remote.enabled' : true , 'security.certerrors.mitm.priming.enabled' : false , 'security.fileuri.strict_origin_policy' : false , 'security.notification_enable_delay' : 0 , 'services.settings.server' : `http://${ server }/dummy/blocklist/` , 'signon.autofillForms' : false , 'signon.rememberSignons' : false , 'startup.homepage_welcome_url' : 'about:blank' , 'startup.homepage_welcome_url.additional' : '' , 'toolkit.cosmeticAnimations.enabled' : false , 'toolkit.startup.max_resumed_crashes' : - 1 , } ; return Object . assign ( defaultPrefs , extraPrefs ) ; } param prefs param profilePath async writePreferences ( prefs , profilePath ) { const lines = Object . entries ( prefs ) . map ( ( [ key , value ] ) => { return `user_pref(${ JSON . stringify ( key ) }, ${ JSON . stringify ( value ) });` ; } ) ; await writeFileAsync ( path . join ( profilePath , 'user.js' ) , lines . join ( '\n' ) ) ; const prefsPath = path . join ( profilePath , 'prefs.js' ) ; if ( fs . existsSync ( prefsPath ) ) { const prefsBackupPath = path . join ( profilePath , 'prefs.js.puppeteer' ) ; await copyFileAsync ( prefsPath , prefsBackupPath ) ; } } async _createProfile ( extraPrefs ) { const temporaryProfilePath = await mkdtempAsync ( path . join ( tmpDir ( ) , 'puppeteer_dev_firefox_profile-' ) ) ; const prefs = this . defaultPreferences ( extraPrefs ) ; await this . writePreferences ( prefs , temporaryProfilePath ) ; return temporaryProfilePath ; } } function executablePathForChannel ( channel ) { const platform = os . platform ( ) ; let chromePath ; 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 }.` ) ; } 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 ) { let downloadPath ; if ( ! launcher . _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 : null ; return { executablePath , missingText } ; } downloadPath = process . env . PUPPETEER_DOWNLOAD_PATH || process . env . npm_config_puppeteer_download_path || process . env . npm_package_config_puppeteer_download_path ; } const browserFetcher = new BrowserFetcher ( launcher . _projectRoot , { product : launcher . product , path : downloadPath , } ) ; if ( ! launcher . _isPuppeteerCore && launcher . 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 : null ; return { executablePath : revisionInfo . executablePath , missingText } ; } } const revisionInfo = browserFetcher . revisionInfo ( launcher . _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 (${ launcher . product }) locally. ${ launcher . product === 'chrome' ? chromeHelp : firefoxHelp }` : null ; return { executablePath : revisionInfo . executablePath , missingText } ; } internal export default function Launcher ( projectRoot , preferredRevision , isPuppeteerCore , product ? ) { 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' ) { console . warn ( `Warning: unknown product name ${ product }. Falling back to chrome.` ) ; } return new ChromeLauncher ( projectRoot , preferredRevision , isPuppeteerCore ) ; } }