2019-12-19 16:53:24 -08:00
/ * *
* Copyright 2017 Google Inc . All rights reserved .
* Modifications copyright ( c ) Microsoft Corporation .
*
* 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 .
* /
2020-01-07 13:58:23 -08:00
import * as fs from 'fs' ;
import * as os from 'os' ;
import * as path from 'path' ;
import * as util from 'util' ;
2020-02-11 11:33:48 -08:00
import { BrowserFetcher , OnProgressCallback , BrowserFetcherOptions } from '../server/browserFetcher' ;
2020-01-07 12:53:06 -08:00
import { DeviceDescriptors } from '../deviceDescriptors' ;
import * as types from '../types' ;
2020-02-13 13:54:01 -08:00
import { assert , helper } from '../helper' ;
2020-01-23 14:40:37 -08:00
import { CRBrowser } from '../chromium/crBrowser' ;
2020-01-07 13:58:23 -08:00
import * as platform from '../platform' ;
import { TimeoutError } from '../errors' ;
2020-01-07 15:27:45 -08:00
import { launchProcess , waitForLine } from '../server/processLauncher' ;
2020-01-23 17:45:31 -08:00
import { kBrowserCloseMessageId } from '../chromium/crConnection' ;
2020-01-07 15:27:45 -08:00
import { PipeTransport } from './pipeTransport' ;
2020-01-24 14:49:47 -08:00
import { LaunchOptions , BrowserArgOptions , BrowserType } from './browserType' ;
2020-02-05 12:41:55 -08:00
import { ConnectOptions , LaunchType } from '../browser' ;
import { BrowserServer } from './browserServer' ;
2020-01-24 15:58:04 -08:00
import { Events } from '../events' ;
2020-02-04 19:41:38 -08:00
import { ConnectionTransport } from '../transport' ;
2020-02-05 12:41:55 -08:00
import { BrowserContext } from '../browserContext' ;
2020-01-07 13:58:23 -08:00
2020-01-24 14:49:47 -08:00
export class Chromium implements BrowserType {
2020-02-26 15:13:31 -08:00
private _downloadPath : string ;
2019-12-19 16:53:24 -08:00
readonly _revision : string ;
2020-02-26 15:13:31 -08:00
constructor ( downloadPath : string , preferredRevision : string ) {
this . _downloadPath = downloadPath ;
2019-12-19 16:53:24 -08:00
this . _revision = preferredRevision ;
}
2020-01-28 18:09:07 -08:00
name() {
return 'chromium' ;
}
2020-02-04 19:41:38 -08:00
async launch ( options? : LaunchOptions & { slowMo? : number } ) : Promise < CRBrowser > {
2020-02-12 19:32:23 -08:00
if ( options && ( options as any ) . userDataDir )
throw new Error ( 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistent` instead' ) ;
2020-02-05 12:41:55 -08:00
const { browserServer , transport } = await this . _launchServer ( options , 'local' ) ;
2020-02-04 19:41:38 -08:00
const browser = await CRBrowser . connect ( transport ! , options && options . slowMo ) ;
2020-01-23 08:51:43 -08:00
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
2020-02-05 12:41:55 -08:00
browser . close = ( ) = > browserServer . close ( ) ;
( browser as any ) [ '__server__' ] = browserServer ;
2020-01-23 08:51:43 -08:00
return browser ;
2019-12-19 16:53:24 -08:00
}
2020-02-05 12:41:55 -08:00
async launchServer ( options? : LaunchOptions & { port? : number } ) : Promise < BrowserServer > {
return ( await this . _launchServer ( options , 'server' , undefined , options && options . port ) ) . browserServer ;
2020-02-04 19:41:38 -08:00
}
2020-02-05 16:36:36 -08:00
async launchPersistent ( userDataDir : string , options? : LaunchOptions ) : Promise < BrowserContext > {
2020-02-13 13:54:01 -08:00
const { timeout = 30000 } = options || { } ;
2020-02-05 16:36:36 -08:00
const { browserServer , transport } = await this . _launchServer ( options , 'persistent' , userDataDir ) ;
2020-02-05 12:41:55 -08:00
const browser = await CRBrowser . connect ( transport ! ) ;
2020-02-24 14:35:51 -08:00
await helper . waitWithTimeout ( browser . _defaultContext . waitForTarget ( t = > t . type ( ) === 'page' ) , 'first page' , timeout ) ;
2020-02-05 12:41:55 -08:00
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
const browserContext = browser . _defaultContext ;
browserContext . close = ( ) = > browserServer . close ( ) ;
return browserContext ;
}
private async _launchServer ( options : LaunchOptions = { } , launchType : LaunchType , userDataDir? : string , port? : number ) : Promise < { browserServer : BrowserServer , transport? : ConnectionTransport } > {
2020-01-07 13:58:23 -08:00
const {
ignoreDefaultArgs = false ,
args = [ ] ,
dumpio = false ,
executablePath = null ,
env = process . env ,
handleSIGINT = true ,
handleSIGTERM = true ,
handleSIGHUP = true ,
timeout = 30000
} = options ;
2020-02-05 16:36:36 -08:00
let temporaryUserDataDir : string | null = null ;
if ( ! userDataDir ) {
userDataDir = await mkdtempAsync ( CHROMIUM_PROFILE_PATH ) ;
temporaryUserDataDir = userDataDir ! ;
}
2020-01-07 13:58:23 -08:00
const chromeArguments = [ ] ;
if ( ! ignoreDefaultArgs )
2020-02-05 16:36:36 -08:00
chromeArguments . push ( . . . this . _defaultArgs ( options , launchType , userDataDir ! , port || 0 ) ) ;
2020-01-07 13:58:23 -08:00
else if ( Array . isArray ( ignoreDefaultArgs ) )
2020-02-05 16:36:36 -08:00
chromeArguments . push ( . . . this . _defaultArgs ( options , launchType , userDataDir ! , port || 0 ) . filter ( arg = > ignoreDefaultArgs . indexOf ( arg ) === - 1 ) ) ;
2020-01-07 13:58:23 -08:00
else
chromeArguments . push ( . . . args ) ;
let chromeExecutable = executablePath ;
if ( ! executablePath ) {
const { missingText , executablePath } = this . _resolveExecutablePath ( ) ;
if ( missingText )
throw new Error ( missingText ) ;
chromeExecutable = executablePath ;
}
2020-02-05 12:41:55 -08:00
let browserServer : BrowserServer | undefined = undefined ;
2020-01-08 13:55:38 -08:00
const { launchedProcess , gracefullyClose } = await launchProcess ( {
2020-01-13 13:33:25 -08:00
executablePath : chromeExecutable ! ,
2020-01-07 13:58:23 -08:00
args : chromeArguments ,
env ,
handleSIGINT ,
handleSIGTERM ,
handleSIGHUP ,
dumpio ,
2020-02-05 12:41:55 -08:00
pipe : launchType !== 'server' ,
2020-01-13 13:33:25 -08:00
tempDir : temporaryUserDataDir || undefined ,
2020-01-08 13:55:38 -08:00
attemptToGracefullyClose : async ( ) = > {
2020-02-05 12:41:55 -08:00
if ( ! browserServer )
2020-01-08 13:55:38 -08:00
return Promise . reject ( ) ;
// We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since
2020-01-23 17:45:31 -08:00
// our connection ignores kBrowserCloseMessageId.
2020-02-06 12:41:43 -08:00
const t = transport || new platform . WebSocketTransport ( browserWSEndpoint ! ) ;
2020-01-23 17:45:31 -08:00
const message = { method : 'Browser.close' , id : kBrowserCloseMessageId } ;
2020-02-06 12:41:43 -08:00
await t . send ( JSON . stringify ( message ) ) ;
2020-01-08 13:55:38 -08:00
} ,
2020-01-28 13:07:53 -08:00
onkill : ( exitCode , signal ) = > {
2020-02-05 12:41:55 -08:00
if ( browserServer )
browserServer . emit ( Events . BrowserServer . Close , exitCode , signal ) ;
2020-01-24 15:58:04 -08:00
} ,
2020-01-07 13:58:23 -08:00
} ) ;
2020-02-04 19:41:38 -08:00
let transport : ConnectionTransport | undefined ;
let browserWSEndpoint : string | null ;
2020-02-05 12:41:55 -08:00
if ( launchType === 'server' ) {
2020-01-13 10:13:28 -08:00
const timeoutError = new TimeoutError ( ` Timed out after ${ timeout } ms while trying to connect to Chromium! The only Chromium revision guaranteed to work is r ${ this . _revision } ` ) ;
2020-01-08 13:55:38 -08:00
const match = await waitForLine ( launchedProcess , launchedProcess . stderr , /^DevTools listening on (ws:\/\/.*)$/ , timeout , timeoutError ) ;
2020-02-04 19:41:38 -08:00
browserWSEndpoint = match [ 1 ] ;
2020-01-08 13:55:38 -08:00
} else {
2020-02-04 19:41:38 -08:00
transport = new PipeTransport ( launchedProcess . stdio [ 3 ] as NodeJS . WritableStream , launchedProcess . stdio [ 4 ] as NodeJS . ReadableStream ) ;
browserWSEndpoint = null ;
2020-01-07 13:58:23 -08:00
}
2020-02-05 12:41:55 -08:00
browserServer = new BrowserServer ( launchedProcess , gracefullyClose , browserWSEndpoint ) ;
return { browserServer , transport } ;
2019-12-19 16:53:24 -08:00
}
2020-02-04 19:41:38 -08:00
async connect ( options : ConnectOptions ) : Promise < CRBrowser > {
2020-02-06 12:41:43 -08:00
const transport = new platform . WebSocketTransport ( options . wsEndpoint ) ;
2020-02-04 19:41:38 -08:00
return CRBrowser . connect ( transport , options . slowMo ) ;
2019-12-19 16:53:24 -08:00
}
executablePath ( ) : string {
2020-01-07 13:58:23 -08:00
return this . _resolveExecutablePath ( ) . executablePath ;
2019-12-19 16:53:24 -08:00
}
2020-01-07 12:53:06 -08:00
get devices ( ) : types . Devices {
return DeviceDescriptors ;
2019-12-19 16:53:24 -08:00
}
2020-01-09 14:49:22 -08:00
get errors ( ) : { TimeoutError : typeof TimeoutError } {
return { TimeoutError } ;
2019-12-19 16:53:24 -08:00
}
2020-02-05 16:36:36 -08:00
private _defaultArgs ( options : BrowserArgOptions = { } , launchType : LaunchType , userDataDir : string , port : number ) : string [ ] {
2020-01-07 13:58:23 -08:00
const {
devtools = false ,
headless = ! devtools ,
args = [ ] ,
} = options ;
2020-02-05 16:36:36 -08:00
const userDataDirArg = args . find ( arg = > arg . startsWith ( '--user-data-dir' ) ) ;
if ( userDataDirArg )
throw new Error ( 'Pass userDataDir parameter instead of specifying --user-data-dir argument' ) ;
if ( args . find ( arg = > arg . startsWith ( '--remote-debugging-' ) ) )
throw new Error ( 'Playwright manages remote debugging connection itself.' ) ;
2020-01-07 13:58:23 -08:00
const chromeArguments = [ . . . DEFAULT_ARGS ] ;
2020-02-05 16:36:36 -08:00
chromeArguments . push ( ` --user-data-dir= ${ userDataDir } ` ) ;
chromeArguments . push ( launchType === 'server' ? ` --remote-debugging-port= ${ port || 0 } ` : '--remote-debugging-pipe' ) ;
2020-01-07 13:58:23 -08:00
if ( devtools )
chromeArguments . push ( '--auto-open-devtools-for-tabs' ) ;
if ( headless ) {
chromeArguments . push (
'--headless' ,
'--hide-scrollbars' ,
'--mute-audio'
) ;
}
2020-02-25 11:43:17 -08:00
if ( launchType !== 'persistent' )
chromeArguments . push ( '--no-startup-window' ) ;
2020-02-05 16:36:36 -08:00
chromeArguments . push ( . . . args ) ;
2020-01-07 13:58:23 -08:00
if ( args . every ( arg = > arg . startsWith ( '-' ) ) )
chromeArguments . push ( 'about:blank' ) ;
2020-02-05 16:36:36 -08:00
2020-01-07 13:58:23 -08:00
return chromeArguments ;
2019-12-19 16:53:24 -08:00
}
2020-02-11 11:33:48 -08:00
async downloadBrowserIfNeeded ( onProgress? : OnProgressCallback ) {
const fetcher = this . _createBrowserFetcher ( ) ;
const revisionInfo = fetcher . revisionInfo ( ) ;
// Do nothing if the revision is already downloaded.
if ( revisionInfo . local )
return ;
await fetcher . download ( revisionInfo . revision , onProgress ) ;
}
2020-01-14 10:07:26 -08:00
_createBrowserFetcher ( options : BrowserFetcherOptions = { } ) : BrowserFetcher {
2020-01-07 13:58:23 -08:00
const downloadURLs = {
linux : '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip' ,
mac : '%s/chromium-browser-snapshots/Mac/%d/%s.zip' ,
win32 : '%s/chromium-browser-snapshots/Win/%d/%s.zip' ,
win64 : '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip' ,
} ;
const defaultOptions = {
2020-02-26 15:13:31 -08:00
path : path.join ( this . _downloadPath , '.local-chromium' ) ,
2020-01-07 13:58:23 -08:00
host : 'https://storage.googleapis.com' ,
platform : ( ( ) = > {
const platform = os . platform ( ) ;
if ( platform === 'darwin' )
return 'mac' ;
if ( platform === 'linux' )
return 'linux' ;
if ( platform === 'win32' )
return os . arch ( ) === 'x64' ? 'win64' : 'win32' ;
return platform ;
} ) ( )
} ;
options = {
. . . defaultOptions ,
. . . options ,
} ;
2020-01-13 13:33:25 -08:00
assert ( ! ! ( downloadURLs as any ) [ options . platform ! ] , 'Unsupported platform: ' + options . platform ) ;
2020-01-07 13:58:23 -08:00
2020-01-13 13:33:25 -08:00
return new BrowserFetcher ( options . path ! , options . platform ! , this . _revision , ( platform : string , revision : string ) = > {
2020-01-07 13:58:23 -08:00
let archiveName = '' ;
let executablePath = '' ;
if ( platform === 'linux' ) {
archiveName = 'chrome-linux' ;
executablePath = path . join ( archiveName , 'chrome' ) ;
} else if ( platform === 'mac' ) {
archiveName = 'chrome-mac' ;
executablePath = path . join ( archiveName , 'Chromium.app' , 'Contents' , 'MacOS' , 'Chromium' ) ;
} else if ( platform === 'win32' || platform === 'win64' ) {
// Windows archive name changed at r591479.
archiveName = parseInt ( revision , 10 ) > 591479 ? 'chrome-win' : 'chrome-win32' ;
executablePath = path . join ( archiveName , 'chrome.exe' ) ;
}
return {
downloadUrl : util.format ( ( downloadURLs as any ) [ platform ] , options . host , revision , archiveName ) ,
executablePath
} ;
} ) ;
}
_resolveExecutablePath ( ) : { executablePath : string ; missingText : string | null ; } {
2020-01-14 10:07:26 -08:00
const browserFetcher = this . _createBrowserFetcher ( ) ;
2020-01-09 14:49:22 -08:00
const revisionInfo = browserFetcher . revisionInfo ( ) ;
2020-01-31 17:23:39 -08:00
const missingText = ! revisionInfo . local ? ` Chromium revision is not downloaded. Run "npm install" ` : null ;
2020-01-07 13:58:23 -08:00
return { executablePath : revisionInfo.executablePath , missingText } ;
2019-12-19 16:53:24 -08:00
}
}
2020-01-07 13:58:23 -08:00
const mkdtempAsync = platform . promisify ( fs . mkdtemp ) ;
2020-01-13 10:13:28 -08:00
const CHROMIUM_PROFILE_PATH = path . join ( os . tmpdir ( ) , 'playwright_dev_profile-' ) ;
2020-01-07 13:58:23 -08:00
const DEFAULT_ARGS = [
'--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' ,
// BlinkGenPropertyTrees disabled due to crbug.com/937609
'--disable-features=TranslateUI,BlinkGenPropertyTrees' ,
'--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' ,
] ;