2017-08-15 01:08:06 +00:00
/ * *
* 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 .
* /
2017-08-22 03:12:16 +00:00
const os = require ( 'os' ) ;
2017-08-15 01:08:06 +00:00
const path = require ( 'path' ) ;
2017-10-11 07:55:48 +00:00
const removeFolder = require ( 'rimraf' ) ;
2017-08-15 01:08:06 +00:00
const childProcess = require ( 'child_process' ) ;
2018-02-07 17:31:53 +00:00
const BrowserFetcher = require ( './BrowserFetcher' ) ;
2017-10-10 05:31:40 +00:00
const { Connection } = require ( './Connection' ) ;
2018-05-10 20:26:08 +00:00
const { Browser } = require ( './Browser' ) ;
2017-08-15 01:08:06 +00:00
const readline = require ( 'readline' ) ;
2017-08-22 03:12:16 +00:00
const fs = require ( 'fs' ) ;
2018-09-05 21:59:14 +00:00
const { helper , debugError } = require ( './helper' ) ;
2018-08-09 23:51:12 +00:00
const { TimeoutError } = require ( './Errors' ) ;
2018-09-07 20:36:16 +00:00
const WebSocketTransport = require ( './WebSocketTransport' ) ;
const PipeTransport = require ( './PipeTransport' ) ;
2017-08-15 01:08:06 +00:00
2017-10-11 07:55:48 +00:00
const mkdtempAsync = helper . promisify ( fs . mkdtemp ) ;
const removeFolderAsync = helper . promisify ( removeFolder ) ;
2017-08-22 03:12:16 +00:00
const CHROME _PROFILE _PATH = path . join ( os . tmpdir ( ) , 'puppeteer_dev_profile-' ) ;
2017-08-15 01:08:06 +00:00
const DEFAULT _ARGS = [
'--disable-background-networking' ,
2019-01-09 23:47:08 +00:00
'--enable-features=NetworkService,NetworkServiceInProcess' ,
2017-08-15 01:08:06 +00:00
'--disable-background-timer-throttling' ,
2018-11-01 22:41:08 +00:00
'--disable-backgrounding-occluded-windows' ,
2018-06-01 01:17:50 +00:00
'--disable-breakpad' ,
2017-08-15 01:08:06 +00:00
'--disable-client-side-phishing-detection' ,
'--disable-default-apps' ,
2018-04-11 03:05:27 +00:00
'--disable-dev-shm-usage' ,
2017-09-15 02:07:22 +00:00
'--disable-extensions' ,
2018-06-01 22:20:37 +00:00
// TODO: Support OOOPIF. @see https://github.com/GoogleChrome/puppeteer/issues/2548
'--disable-features=site-per-process' ,
2017-08-15 01:08:06 +00:00
'--disable-hang-monitor' ,
2018-11-01 22:41:08 +00:00
'--disable-ipc-flooding-protection' ,
2017-08-15 01:08:06 +00:00
'--disable-popup-blocking' ,
'--disable-prompt-on-repost' ,
2018-11-01 22:41:08 +00:00
'--disable-renderer-backgrounding' ,
2017-08-15 01:08:06 +00:00
'--disable-sync' ,
2017-09-15 02:07:22 +00:00
'--disable-translate' ,
2019-01-08 20:55:40 +00:00
'--force-color-profile=srgb' ,
2017-08-15 01:08:06 +00:00
'--metrics-recording-only' ,
'--no-first-run' ,
'--safebrowsing-disable-auto-update' ,
2017-09-29 22:16:32 +00:00
'--enable-automation' ,
'--password-store=basic' ,
2017-08-15 01:08:06 +00:00
'--use-mock-keychain' ,
] ;
class Launcher {
2018-09-06 19:33:41 +00:00
/ * *
* @ param { string } projectRoot
* @ param { string } preferredRevision
* @ param { boolean } isPuppeteerCore
* /
constructor ( projectRoot , preferredRevision , isPuppeteerCore ) {
this . _projectRoot = projectRoot ;
this . _preferredRevision = preferredRevision ;
this . _isPuppeteerCore = isPuppeteerCore ;
}
2017-08-15 01:08:06 +00:00
/ * *
2018-11-12 20:59:21 +00:00
* @ param { ! ( Launcher . LaunchOptions & Launcher . ChromeArgOptions & Launcher . BrowserOptions ) = } options
2017-08-15 01:08:06 +00:00
* @ return { ! Promise < ! Browser > }
* /
2018-09-06 19:33:41 +00:00
async launch ( options = { } ) {
2018-08-07 20:22:04 +00:00
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
} = options ;
2018-04-03 22:05:27 +00:00
2018-08-09 02:10:10 +00:00
const chromeArguments = [ ] ;
if ( ! ignoreDefaultArgs )
chromeArguments . push ( ... this . defaultArgs ( options ) ) ;
else if ( Array . isArray ( ignoreDefaultArgs ) )
chromeArguments . push ( ... this . defaultArgs ( options ) . filter ( arg => ignoreDefaultArgs . indexOf ( arg ) === - 1 ) ) ;
else
chromeArguments . push ( ... args ) ;
2018-08-07 20:22:04 +00:00
let temporaryUserDataDir = null ;
2017-08-18 06:18:08 +00:00
2018-08-07 20:22:04 +00:00
if ( ! chromeArguments . some ( argument => argument . startsWith ( '--remote-debugging-' ) ) )
chromeArguments . push ( pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0' ) ;
if ( ! chromeArguments . some ( arg => arg . startsWith ( '--user-data-dir' ) ) ) {
temporaryUserDataDir = await mkdtempAsync ( CHROME _PROFILE _PATH ) ;
chromeArguments . push ( ` --user-data-dir= ${ temporaryUserDataDir } ` ) ;
2017-08-28 19:14:21 +00:00
}
2018-05-26 00:26:40 +00:00
2018-08-07 20:22:04 +00:00
let chromeExecutable = executablePath ;
if ( ! executablePath ) {
2018-09-06 19:33:41 +00:00
const { missingText , executablePath } = this . _resolveExecutablePath ( ) ;
2018-09-05 21:59:14 +00:00
if ( missingText )
throw new Error ( missingText ) ;
chromeExecutable = executablePath ;
2017-08-15 01:08:06 +00:00
}
2017-08-21 22:43:36 +00:00
2018-04-03 22:05:27 +00:00
const usePipe = chromeArguments . includes ( '--remote-debugging-pipe' ) ;
2018-10-02 20:38:06 +00:00
/** @type {!Array<"ignore"|"pipe">} */
2018-07-24 18:36:35 +00:00
const stdio = usePipe ? [ 'ignore' , 'ignore' , 'ignore' , 'pipe' , 'pipe' ] : [ 'pipe' , 'pipe' , 'pipe' ] ;
2017-10-05 21:34:35 +00:00
const chromeProcess = childProcess . spawn (
chromeExecutable ,
chromeArguments ,
{
2018-03-15 18:50:16 +00:00
// On non-windows platforms, `detached: false` 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' ,
2018-08-07 20:22:04 +00:00
env ,
2018-02-15 01:51:29 +00:00
stdio
2017-10-05 21:34:35 +00:00
}
) ;
2017-08-15 01:08:06 +00:00
2018-08-07 20:22:04 +00:00
if ( dumpio ) {
2018-02-23 03:06:13 +00:00
chromeProcess . stderr . pipe ( process . stderr ) ;
chromeProcess . stdout . pipe ( process . stdout ) ;
}
2017-10-17 22:35:00 +00:00
let chromeClosed = false ;
2017-10-11 07:55:48 +00:00
const waitForChromeToClose = new Promise ( ( fulfill , reject ) => {
2018-07-04 23:36:49 +00:00
chromeProcess . once ( 'exit' , ( ) => {
2017-10-17 22:35:00 +00:00
chromeClosed = true ;
2017-09-14 04:27:14 +00:00
// Cleanup as processes exit.
2017-10-11 07:55:48 +00:00
if ( temporaryUserDataDir ) {
removeFolderAsync ( temporaryUserDataDir )
. then ( ( ) => fulfill ( ) )
. catch ( err => console . error ( err ) ) ;
} else {
fulfill ( ) ;
}
2017-09-14 04:27:14 +00:00
} ) ;
} ) ;
2017-08-23 18:55:33 +00:00
2018-04-09 21:49:02 +00:00
const listeners = [ helper . addEventListener ( process , 'exit' , killChrome ) ] ;
2018-08-07 20:22:04 +00:00
if ( handleSIGINT )
2018-04-25 22:29:14 +00:00
listeners . push ( helper . addEventListener ( process , 'SIGINT' , ( ) => { killChrome ( ) ; process . exit ( 130 ) ; } ) ) ;
2018-08-07 20:22:04 +00:00
if ( handleSIGTERM )
2018-04-09 21:49:02 +00:00
listeners . push ( helper . addEventListener ( process , 'SIGTERM' , gracefullyCloseChrome ) ) ;
2018-08-07 20:22:04 +00:00
if ( handleSIGHUP )
2018-04-09 21:49:02 +00:00
listeners . push ( helper . addEventListener ( process , 'SIGHUP' , gracefullyCloseChrome ) ) ;
2017-10-17 22:35:00 +00:00
/** @type {?Connection} */
let connection = null ;
2017-08-21 22:43:36 +00:00
try {
2018-04-03 22:05:27 +00:00
if ( ! usePipe ) {
2018-09-06 19:33:41 +00:00
const browserWSEndpoint = await waitForWSEndpoint ( chromeProcess , timeout , this . _preferredRevision ) ;
2018-09-07 20:36:16 +00:00
const transport = await WebSocketTransport . create ( browserWSEndpoint ) ;
connection = new Connection ( browserWSEndpoint , transport , slowMo ) ;
2018-02-15 01:51:29 +00:00
} else {
2018-09-07 20:36:16 +00:00
const transport = new PipeTransport ( /** @type {!NodeJS.WritableStream} */ ( chromeProcess . stdio [ 3 ] ) , /** @type {!NodeJS.ReadableStream} */ ( chromeProcess . stdio [ 4 ] ) ) ;
connection = new Connection ( '' , transport , slowMo ) ;
2018-02-15 01:51:29 +00:00
}
2018-08-01 23:23:03 +00:00
const browser = await Browser . create ( connection , [ ] , ignoreHTTPSErrors , defaultViewport , chromeProcess , gracefullyCloseChrome ) ;
2018-06-01 20:57:50 +00:00
await ensureInitialPage ( browser ) ;
return browser ;
2017-08-21 22:43:36 +00:00
} catch ( e ) {
2018-04-09 21:49:02 +00:00
killChrome ( ) ;
2017-08-21 22:43:36 +00:00
throw e ;
2017-08-15 01:08:06 +00:00
}
2018-06-01 20:57:50 +00:00
/ * *
* @ param { ! Browser } browser
* /
async function ensureInitialPage ( browser ) {
// Wait for initial page target to be created.
if ( browser . targets ( ) . find ( target => target . type ( ) === 'page' ) )
return ;
let initialPageCallback ;
const initialPagePromise = new Promise ( resolve => initialPageCallback = resolve ) ;
const listeners = [ helper . addEventListener ( browser , 'targetcreated' , target => {
if ( target . type ( ) === 'page' )
initialPageCallback ( ) ;
} ) ] ;
await initialPagePromise ;
helper . removeEventListeners ( listeners ) ;
}
2017-09-14 04:27:14 +00:00
/ * *
* @ return { Promise }
* /
2018-04-09 21:49:02 +00:00
function gracefullyCloseChrome ( ) {
2017-08-30 05:49:50 +00:00
helper . removeEventListeners ( listeners ) ;
2017-10-17 22:35:00 +00:00
if ( temporaryUserDataDir ) {
2018-04-09 21:49:02 +00:00
killChrome ( ) ;
2017-10-17 22:35:00 +00:00
} else if ( connection ) {
// Attempt to close chrome gracefully
2018-04-09 21:49:02 +00:00
connection . send ( 'Browser.close' ) . catch ( error => {
debugError ( error ) ;
killChrome ( ) ;
} ) ;
2017-09-14 04:27:14 +00:00
}
return waitForChromeToClose ;
2017-08-21 22:43:36 +00:00
}
2017-10-24 23:05:12 +00:00
2018-04-09 21:49:02 +00:00
// This method has to be sync to be used as 'exit' event handler.
function killChrome ( ) {
2017-10-24 23:05:12 +00:00
helper . removeEventListeners ( listeners ) ;
if ( chromeProcess . pid && ! chromeProcess . killed && ! chromeClosed ) {
// Force kill chrome.
2018-03-16 00:40:24 +00:00
try {
if ( process . platform === 'win32' )
childProcess . execSync ( ` taskkill /pid ${ chromeProcess . pid } /T /F ` ) ;
else
process . kill ( - chromeProcess . pid , 'SIGKILL' ) ;
} catch ( e ) {
// the process might have already stopped
}
2017-10-24 23:05:12 +00:00
}
// Attempt to remove temporary profile directory to avoid littering.
try {
removeFolder . sync ( temporaryUserDataDir ) ;
} catch ( e ) { }
}
2017-08-15 01:08:06 +00:00
}
2017-08-15 21:29:42 +00:00
2017-12-20 01:51:21 +00:00
/ * *
2018-11-12 20:59:21 +00:00
* @ param { ! Launcher . ChromeArgOptions = } options
2017-12-20 01:51:21 +00:00
* @ return { ! Array < string > }
* /
2018-09-06 19:33:41 +00:00
defaultArgs ( options = { } ) {
2018-08-07 20:22:04 +00:00
const {
devtools = false ,
headless = ! devtools ,
args = [ ] ,
userDataDir = null
} = options ;
const chromeArguments = [ ... DEFAULT _ARGS ] ;
if ( userDataDir )
chromeArguments . push ( ` --user-data-dir= ${ userDataDir } ` ) ;
if ( devtools )
chromeArguments . push ( '--auto-open-devtools-for-tabs' ) ;
if ( headless ) {
chromeArguments . push (
'--headless' ,
'--hide-scrollbars' ,
'--mute-audio'
) ;
if ( os . platform ( ) === 'win32' )
chromeArguments . push ( '--disable-gpu' ) ;
}
if ( args . every ( arg => arg . startsWith ( '-' ) ) )
chromeArguments . push ( 'about:blank' ) ;
chromeArguments . push ( ... args ) ;
return chromeArguments ;
2017-12-20 01:51:21 +00:00
}
2017-09-14 00:39:18 +00:00
/ * *
* @ return { string }
* /
2018-09-06 19:33:41 +00:00
executablePath ( ) {
return this . _resolveExecutablePath ( ) . executablePath ;
2017-09-14 00:39:18 +00:00
}
2017-08-15 21:29:42 +00:00
/ * *
2018-11-12 20:59:21 +00:00
* @ param { ! ( Launcher . BrowserOptions & { browserWSEndpoint : string , transport ? : ! Puppeteer . ConnectionTransport } ) } options
2017-08-15 21:29:42 +00:00
* @ return { ! Promise < ! Browser > }
* /
2018-09-06 19:33:41 +00:00
async connect ( options ) {
2018-08-01 23:23:03 +00:00
const {
browserWSEndpoint ,
ignoreHTTPSErrors = false ,
defaultViewport = { width : 800 , height : 600 } ,
2018-09-20 18:55:23 +00:00
transport = await WebSocketTransport . create ( browserWSEndpoint ) ,
2018-08-01 23:23:03 +00:00
slowMo = 0 ,
} = options ;
2018-09-07 20:36:16 +00:00
const connection = new Connection ( browserWSEndpoint , transport , slowMo ) ;
2018-05-10 20:26:08 +00:00
const { browserContextIds } = await connection . send ( 'Target.getBrowserContexts' ) ;
2018-08-01 23:23:03 +00:00
return Browser . create ( connection , browserContextIds , ignoreHTTPSErrors , defaultViewport , null , ( ) => connection . send ( 'Browser.close' ) . catch ( debugError ) ) ;
2017-08-15 21:29:42 +00:00
}
2018-09-06 19:33:41 +00:00
/ * *
* @ return { { executablePath : string , missingText : ? string } }
* /
_resolveExecutablePath ( ) {
const browserFetcher = new BrowserFetcher ( this . _projectRoot ) ;
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if ( ! this . _isPuppeteerCore ) {
const executablePath = process . env [ '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 } ;
}
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 ( this . _preferredRevision ) ;
const missingText = ! revisionInfo . local ? ` Chromium revision is not downloaded. Run "npm install" or "yarn install" ` : null ;
return { executablePath : revisionInfo . executablePath , missingText } ;
}
2017-08-15 01:08:06 +00:00
}
/ * *
2017-10-10 05:31:40 +00:00
* @ param { ! Puppeteer . ChildProcess } chromeProcess
2017-08-21 22:43:36 +00:00
* @ param { number } timeout
2018-09-06 19:33:41 +00:00
* @ param { string } preferredRevision
2017-08-15 21:29:42 +00:00
* @ return { ! Promise < string > }
2017-08-15 01:08:06 +00:00
* /
2018-09-06 19:33:41 +00:00
function waitForWSEndpoint ( chromeProcess , timeout , preferredRevision ) {
2017-08-21 22:43:36 +00:00
return new Promise ( ( resolve , reject ) => {
2017-08-15 01:08:06 +00:00
const rl = readline . createInterface ( { input : chromeProcess . stderr } ) ;
2017-08-21 22:43:36 +00:00
let stderr = '' ;
2017-08-21 23:39:04 +00:00
const listeners = [
2017-08-21 22:43:36 +00:00
helper . addEventListener ( rl , 'line' , onLine ) ,
2017-09-29 19:21:24 +00:00
helper . addEventListener ( rl , 'close' , ( ) => onClose ( ) ) ,
helper . addEventListener ( chromeProcess , 'exit' , ( ) => onClose ( ) ) ,
helper . addEventListener ( chromeProcess , 'error' , error => onClose ( error ) )
2017-08-21 22:43:36 +00:00
] ;
2017-08-21 23:39:04 +00:00
const timeoutId = timeout ? setTimeout ( onTimeout , timeout ) : 0 ;
2017-08-21 22:43:36 +00:00
2017-09-29 19:21:24 +00:00
/ * *
* @ param { ! Error = } error
* /
function onClose ( error ) {
2017-08-21 22:43:36 +00:00
cleanup ( ) ;
reject ( new Error ( [
2017-09-29 19:21:24 +00:00
'Failed to launch chrome!' + ( error ? ' ' + error . message : '' ) ,
2017-08-21 22:43:36 +00:00
stderr ,
'' ,
'TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md' ,
'' ,
] . join ( '\n' ) ) ) ;
}
function onTimeout ( ) {
cleanup ( ) ;
2018-09-06 19:33:41 +00:00
reject ( new TimeoutError ( ` Timed out after ${ timeout } ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r ${ preferredRevision } ` ) ) ;
2017-08-21 22:43:36 +00:00
}
2017-08-15 01:08:06 +00:00
/ * *
* @ param { string } line
* /
function onLine ( line ) {
2017-08-21 22:43:36 +00:00
stderr += line + '\n' ;
2017-08-15 21:29:42 +00:00
const match = line . match ( /^DevTools listening on (ws:\/\/.*)$/ ) ;
2017-08-15 01:08:06 +00:00
if ( ! match )
return ;
2017-08-21 22:43:36 +00:00
cleanup ( ) ;
resolve ( match [ 1 ] ) ;
}
function cleanup ( ) {
if ( timeoutId )
clearTimeout ( timeoutId ) ;
helper . removeEventListeners ( listeners ) ;
2017-08-15 01:08:06 +00:00
}
} ) ;
}
2018-04-03 22:05:27 +00:00
/ * *
2018-11-12 20:59:21 +00:00
* @ typedef { Object } Launcher . ChromeArgOptions
2018-04-03 22:05:27 +00:00
* @ property { boolean = } headless
2018-08-07 20:22:04 +00:00
* @ property { Array < string >= } args
* @ property { string = } userDataDir
* @ property { boolean = } devtools
* /
/ * *
2018-11-12 20:59:21 +00:00
* @ typedef { Object } Launcher . LaunchOptions
2018-04-03 22:05:27 +00:00
* @ property { string = } executablePath
2018-11-12 20:59:21 +00:00
* @ property { boolean | Array < string >= } ignoreDefaultArgs
2018-04-03 22:05:27 +00:00
* @ property { boolean = } handleSIGINT
* @ property { boolean = } handleSIGTERM
* @ property { boolean = } handleSIGHUP
* @ property { number = } timeout
* @ property { boolean = } dumpio
* @ property { ! Object < string , string | undefined >= } env
* @ property { boolean = } pipe
2018-08-01 23:23:03 +00:00
* /
/ * *
2018-11-12 20:59:21 +00:00
* @ typedef { Object } Launcher . BrowserOptions
2018-08-01 23:23:03 +00:00
* @ property { boolean = } ignoreHTTPSErrors
* @ property { ( ? Puppeteer . Viewport ) = } defaultViewport
* @ property { number = } slowMo
2018-04-03 22:05:27 +00:00
* /
2017-08-15 01:08:06 +00:00
module . exports = Launcher ;