2020-01-24 14:49:47 -08:00
/ * *
* 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-05-22 07:03:42 -07:00
import * as fs from 'fs' ;
import * as os from 'os' ;
import * as path from 'path' ;
import * as util from 'util' ;
2020-06-05 13:50:15 -07:00
import { BrowserContext , PersistentContextOptions , validatePersistentContextOptions , verifyProxySettings } from '../browserContext' ;
2020-05-22 07:03:42 -07:00
import { BrowserServer , WebSocketWrapper } from './browserServer' ;
2020-04-28 17:06:01 -07:00
import * as browserPaths from '../install/browserPaths' ;
2020-05-22 07:03:42 -07:00
import { Logger , RootLogger , InnerLogger } from '../logger' ;
2020-05-20 16:30:04 -07:00
import { ConnectionTransport , WebSocketTransport } from '../transport' ;
import { BrowserBase , BrowserOptions , Browser } from '../browser' ;
2020-05-29 14:39:34 -07:00
import { assert } from '../helper' ;
2020-05-22 07:03:42 -07:00
import { launchProcess , Env , waitForLine } from './processLauncher' ;
import { Events } from '../events' ;
import { PipeTransport } from './pipeTransport' ;
2020-06-04 16:43:48 -07:00
import { Progress , runAbortableTask } from '../progress' ;
2020-06-05 13:50:15 -07:00
import { ProxySettings } from '../types' ;
2020-06-05 15:53:30 -07:00
import { TimeoutSettings } from '../timeoutSettings' ;
2020-01-24 14:49:47 -08:00
export type BrowserArgOptions = {
headless? : boolean ,
args? : string [ ] ,
devtools? : boolean ,
2020-06-05 13:50:15 -07:00
proxy? : ProxySettings ,
2020-01-24 14:49:47 -08:00
} ;
2020-05-31 09:28:57 -07:00
export type FirefoxUserPrefsOptions = {
firefoxUserPrefs ? : { [ key : string ] : string | number | boolean } ,
} ;
2020-05-20 16:30:04 -07:00
type LaunchOptionsBase = BrowserArgOptions & {
2020-01-24 14:49:47 -08:00
executablePath? : string ,
ignoreDefaultArgs? : boolean | string [ ] ,
handleSIGINT? : boolean ,
handleSIGTERM? : boolean ,
handleSIGHUP? : boolean ,
timeout? : number ,
2020-04-20 23:24:53 -07:00
logger? : Logger ,
2020-05-22 07:03:42 -07:00
env? : Env ,
2020-01-24 14:49:47 -08:00
} ;
2020-05-14 13:22:33 -07:00
export function processBrowserArgOptions ( options : LaunchOptionsBase ) : { devtools : boolean , headless : boolean } {
const { devtools = false , headless = ! devtools } = options ;
return { devtools , headless } ;
}
2020-05-22 07:03:42 -07:00
type ConnectOptions = {
2020-03-31 16:34:59 -07:00
wsEndpoint : string ,
2020-04-20 07:52:26 -07:00
slowMo? : number ,
2020-04-20 23:24:53 -07:00
logger? : Logger ,
2020-05-21 09:43:10 -07:00
timeout? : number ,
2020-03-31 16:34:59 -07:00
} ;
export type LaunchOptions = LaunchOptionsBase & { slowMo? : number } ;
2020-05-22 07:03:42 -07:00
type LaunchServerOptions = LaunchOptionsBase & { port? : number } ;
2020-05-20 16:30:04 -07:00
export interface BrowserType {
2020-01-24 14:49:47 -08:00
executablePath ( ) : string ;
2020-01-28 18:09:07 -08:00
name ( ) : string ;
2020-05-31 09:28:57 -07:00
launch ( options? : LaunchOptions & FirefoxUserPrefsOptions ) : Promise < Browser > ;
launchServer ( options? : LaunchServerOptions & FirefoxUserPrefsOptions ) : Promise < BrowserServer > ;
2020-05-21 15:13:16 -07:00
launchPersistentContext ( userDataDir : string , options? : LaunchOptions & PersistentContextOptions ) : Promise < BrowserContext > ;
2020-02-04 19:41:38 -08:00
connect ( options : ConnectOptions ) : Promise < Browser > ;
2020-01-24 14:49:47 -08:00
}
2020-04-28 17:06:01 -07:00
2020-05-22 07:03:42 -07:00
const mkdtempAsync = util . promisify ( fs . mkdtemp ) ;
2020-05-22 16:06:00 -07:00
const DOWNLOADS_FOLDER = path . join ( os . tmpdir ( ) , 'playwright_downloads-' ) ;
2020-05-22 07:03:42 -07:00
2020-06-04 16:40:07 -07:00
type WebSocketNotPipe = { webSocketRegex : RegExp , stream : 'stdout' | 'stderr' } ;
2020-05-20 16:30:04 -07:00
export abstract class BrowserTypeBase implements BrowserType {
2020-04-28 17:06:01 -07:00
private _name : string ;
2020-04-29 17:19:21 -07:00
private _executablePath : string | undefined ;
2020-06-04 16:40:07 -07:00
private _webSocketNotPipe : WebSocketNotPipe | null ;
2020-05-19 14:55:11 -07:00
readonly _browserPath : string ;
2020-04-28 17:06:01 -07:00
2020-06-04 16:40:07 -07:00
constructor ( packagePath : string , browser : browserPaths.BrowserDescriptor , webSocketOrPipe : WebSocketNotPipe | null ) {
2020-04-28 17:06:01 -07:00
this . _name = browser . name ;
2020-04-29 17:19:21 -07:00
const browsersPath = browserPaths . browsersPath ( packagePath ) ;
2020-05-19 14:55:11 -07:00
this . _browserPath = browserPaths . browserDirectory ( browsersPath , browser ) ;
this . _executablePath = browserPaths . executablePath ( this . _browserPath , browser ) ;
2020-06-04 16:40:07 -07:00
this . _webSocketNotPipe = webSocketOrPipe ;
2020-04-28 17:06:01 -07:00
}
executablePath ( ) : string {
2020-04-29 17:19:21 -07:00
if ( ! this . _executablePath )
throw new Error ( 'Browser is not supported on current platform' ) ;
2020-04-28 17:06:01 -07:00
return this . _executablePath ;
}
name ( ) : string {
return this . _name ;
}
2020-05-20 16:30:04 -07:00
async launch ( options : LaunchOptions = { } ) : Promise < Browser > {
assert ( ! ( options as any ) . userDataDir , 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead' ) ;
2020-05-22 16:06:00 -07:00
assert ( ! ( options as any ) . port , 'Cannot specify a port without launching as a server.' ) ;
2020-05-29 14:39:34 -07:00
const logger = new RootLogger ( options . logger ) ;
2020-06-05 15:53:30 -07:00
const browser = await runAbortableTask ( progress = > this . _innerLaunch ( progress , options , logger , undefined ) , logger , TimeoutSettings . timeout ( options ) ) ;
2020-05-29 14:39:34 -07:00
return browser ;
2020-05-20 16:30:04 -07:00
}
2020-05-21 15:13:16 -07:00
async launchPersistentContext ( userDataDir : string , options : LaunchOptions & PersistentContextOptions = { } ) : Promise < BrowserContext > {
2020-05-22 16:06:00 -07:00
assert ( ! ( options as any ) . port , 'Cannot specify a port without launching as a server.' ) ;
2020-05-21 15:13:16 -07:00
const persistent = validatePersistentContextOptions ( options ) ;
2020-05-21 09:43:10 -07:00
const logger = new RootLogger ( options . logger ) ;
2020-06-05 15:53:30 -07:00
const browser = await runAbortableTask ( progress = > this . _innerLaunch ( progress , options , logger , persistent , userDataDir ) , logger , TimeoutSettings . timeout ( options ) ) ;
2020-05-29 14:39:34 -07:00
return browser . _defaultContext ! ;
2020-05-21 09:43:10 -07:00
}
2020-05-20 16:30:04 -07:00
2020-05-29 14:39:34 -07:00
async _innerLaunch ( progress : Progress , options : LaunchOptions , logger : RootLogger , persistent : PersistentContextOptions | undefined , userDataDir? : string ) : Promise < BrowserBase > {
2020-06-05 13:50:15 -07:00
options . proxy = options . proxy ? verifyProxySettings ( options . proxy ) : undefined ;
2020-05-29 14:39:34 -07:00
const { browserServer , downloadsPath , transport } = await this . _launchServer ( progress , options , ! ! persistent , logger , userDataDir ) ;
if ( ( options as any ) . __testHookBeforeCreateBrowser )
await ( options as any ) . __testHookBeforeCreateBrowser ( ) ;
const browserOptions : BrowserOptions = {
slowMo : options.slowMo ,
persistent ,
headful : ! processBrowserArgOptions ( options ) . headless ,
logger ,
downloadsPath ,
ownedServer : browserServer ,
2020-06-05 13:50:15 -07:00
proxy : options.proxy ,
2020-05-29 14:39:34 -07:00
} ;
copyTestHooks ( options , browserOptions ) ;
2020-05-22 16:06:00 -07:00
const browser = await this . _connectToTransport ( transport , browserOptions ) ;
// We assume no control when using custom arguments, and do not prepare the default context in that case.
2020-05-29 14:39:34 -07:00
const hasCustomArguments = ! ! options . ignoreDefaultArgs && ! Array . isArray ( options . ignoreDefaultArgs ) ;
if ( persistent && ! hasCustomArguments )
2020-05-22 16:06:00 -07:00
await browser . _defaultContext ! . _loadDefaultContext ( ) ;
2020-05-21 09:43:10 -07:00
return browser ;
2020-05-20 16:30:04 -07:00
}
async launchServer ( options : LaunchServerOptions = { } ) : Promise < BrowserServer > {
2020-05-22 16:06:00 -07:00
assert ( ! ( options as any ) . userDataDir , 'userDataDir option is not supported in `browserType.launchServer`. Use `browserType.launchPersistentContext` instead' ) ;
const { port = 0 } = options ;
2020-05-21 09:43:10 -07:00
const logger = new RootLogger ( options . logger ) ;
2020-06-04 16:43:48 -07:00
return runAbortableTask ( async progress = > {
2020-05-29 14:39:34 -07:00
const { browserServer , transport } = await this . _launchServer ( progress , options , false , logger ) ;
browserServer . _webSocketWrapper = this . _wrapTransportWithWebSocket ( transport , logger , port ) ;
return browserServer ;
2020-06-05 15:53:30 -07:00
} , logger , TimeoutSettings . timeout ( options ) ) ;
2020-05-20 16:30:04 -07:00
}
async connect ( options : ConnectOptions ) : Promise < Browser > {
const logger = new RootLogger ( options . logger ) ;
2020-06-04 16:43:48 -07:00
return runAbortableTask ( async progress = > {
2020-05-29 14:39:34 -07:00
const transport = await WebSocketTransport . connect ( progress , options . wsEndpoint ) ;
2020-06-04 16:43:48 -07:00
progress . cleanupWhenAborted ( ( ) = > transport . closeAndWait ( ) ) ;
2020-05-29 14:39:34 -07:00
if ( ( options as any ) . __testHookBeforeCreateBrowser )
await ( options as any ) . __testHookBeforeCreateBrowser ( ) ;
const browser = await this . _connectToTransport ( transport , { slowMo : options.slowMo , logger } ) ;
2020-05-21 09:43:10 -07:00
return browser ;
2020-06-05 15:53:30 -07:00
} , logger , TimeoutSettings . timeout ( options ) ) ;
2020-05-21 09:43:10 -07:00
}
2020-05-29 14:39:34 -07:00
private async _launchServer ( progress : Progress , options : LaunchServerOptions , isPersistent : boolean , logger : RootLogger , userDataDir? : string ) : Promise < { browserServer : BrowserServer , downloadsPath : string , transport : ConnectionTransport } > {
2020-05-22 07:03:42 -07:00
const {
ignoreDefaultArgs = false ,
args = [ ] ,
executablePath = null ,
env = process . env ,
handleSIGINT = true ,
handleSIGTERM = true ,
handleSIGHUP = true ,
} = options ;
2020-05-22 16:06:00 -07:00
const downloadsPath = await mkdtempAsync ( DOWNLOADS_FOLDER ) ;
const tempDirectories = [ downloadsPath ] ;
2020-05-22 07:03:42 -07:00
if ( ! userDataDir ) {
userDataDir = await mkdtempAsync ( path . join ( os . tmpdir ( ) , ` playwright_ ${ this . _name } dev_profile- ` ) ) ;
2020-05-22 16:06:00 -07:00
tempDirectories . push ( userDataDir ) ;
2020-05-22 07:03:42 -07:00
}
const browserArguments = [ ] ;
if ( ! ignoreDefaultArgs )
2020-05-22 16:06:00 -07:00
browserArguments . push ( . . . this . _defaultArgs ( options , isPersistent , userDataDir ) ) ;
2020-05-22 07:03:42 -07:00
else if ( Array . isArray ( ignoreDefaultArgs ) )
2020-05-22 16:06:00 -07:00
browserArguments . push ( . . . this . _defaultArgs ( options , isPersistent , userDataDir ) . filter ( arg = > ignoreDefaultArgs . indexOf ( arg ) === - 1 ) ) ;
2020-05-22 07:03:42 -07:00
else
browserArguments . push ( . . . args ) ;
const executable = executablePath || this . executablePath ( ) ;
if ( ! executable )
throw new Error ( ` No executable path is specified. Pass "executablePath" option directly. ` ) ;
// Note: it is important to define these variables before launchProcess, so that we don't get
// "Cannot access 'browserServer' before initialization" if something went wrong.
let transport : ConnectionTransport | undefined = undefined ;
let browserServer : BrowserServer | undefined = undefined ;
2020-05-27 19:59:03 -07:00
const { launchedProcess , gracefullyClose , kill } = await launchProcess ( {
2020-05-22 07:03:42 -07:00
executablePath : executable ,
args : browserArguments ,
env : this._amendEnvironment ( env , userDataDir , executable , browserArguments ) ,
handleSIGINT ,
handleSIGTERM ,
handleSIGHUP ,
2020-05-29 14:39:34 -07:00
progress ,
2020-06-04 16:40:07 -07:00
pipe : ! this . _webSocketNotPipe ,
2020-05-22 16:06:00 -07:00
tempDirectories ,
2020-05-22 07:03:42 -07:00
attemptToGracefullyClose : async ( ) = > {
if ( ( options as any ) . __testHookGracefullyClose )
await ( options as any ) . __testHookGracefullyClose ( ) ;
// We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since
// our connection ignores kBrowserCloseMessageId.
this . _attemptToGracefullyCloseBrowser ( transport ! ) ;
} ,
2020-05-27 19:59:03 -07:00
onExit : ( exitCode , signal ) = > {
2020-05-22 07:03:42 -07:00
if ( browserServer )
browserServer . emit ( Events . BrowserServer . Close , exitCode , signal ) ;
} ,
} ) ;
2020-05-27 19:59:03 -07:00
browserServer = new BrowserServer ( launchedProcess , gracefullyClose , kill ) ;
2020-06-05 15:53:30 -07:00
progress . cleanupWhenAborted ( ( ) = > browserServer && browserServer . _closeOrKill ( progress . timeUntilDeadline ( ) ) ) ;
2020-05-29 14:39:34 -07:00
2020-06-04 16:40:07 -07:00
if ( this . _webSocketNotPipe ) {
const match = await waitForLine ( progress , launchedProcess , this . _webSocketNotPipe . stream === 'stdout' ? launchedProcess.stdout : launchedProcess.stderr , this . _webSocketNotPipe . webSocketRegex ) ;
2020-05-29 14:39:34 -07:00
const innerEndpoint = match [ 1 ] ;
transport = await WebSocketTransport . connect ( progress , innerEndpoint ) ;
} else {
const stdio = launchedProcess . stdio as unknown as [ NodeJS . ReadableStream , NodeJS . WritableStream , NodeJS . WritableStream , NodeJS . WritableStream , NodeJS . ReadableStream ] ;
transport = new PipeTransport ( stdio [ 3 ] , stdio [ 4 ] , logger ) ;
}
2020-05-22 16:06:00 -07:00
return { browserServer , downloadsPath , transport } ;
2020-05-22 07:03:42 -07:00
}
2020-05-22 16:06:00 -07:00
abstract _defaultArgs ( options : BrowserArgOptions , isPersistent : boolean , userDataDir : string ) : string [ ] ;
2020-05-20 16:30:04 -07:00
abstract _connectToTransport ( transport : ConnectionTransport , options : BrowserOptions ) : Promise < BrowserBase > ;
2020-05-22 07:03:42 -07:00
abstract _wrapTransportWithWebSocket ( transport : ConnectionTransport , logger : InnerLogger , port : number ) : WebSocketWrapper ;
abstract _amendEnvironment ( env : Env , userDataDir : string , executable : string , browserArguments : string [ ] ) : Env ;
abstract _attemptToGracefullyCloseBrowser ( transport : ConnectionTransport ) : void ;
}
2020-05-22 16:06:00 -07:00
function copyTestHooks ( from : object , to : object ) {
for ( const [ key , value ] of Object . entries ( from ) ) {
if ( key . startsWith ( '__testHook' ) )
( to as any ) [ key ] = value ;
}
}