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-25 08:30:56 -07:00
import { BrowserContext , verifyProxySettings , validateBrowserContextOptions } from '../browserContext' ;
2020-06-10 16:33:27 -07:00
import { BrowserServer } from './browserServer' ;
2020-04-28 17:06:01 -07:00
import * as browserPaths from '../install/browserPaths' ;
2020-06-16 17:11:19 -07:00
import { Loggers , Logger } from '../logger' ;
2020-05-20 16:30:04 -07:00
import { ConnectionTransport , WebSocketTransport } from '../transport' ;
import { BrowserBase , BrowserOptions , Browser } from '../browser' ;
2020-06-11 18:18:33 -07:00
import { assert , helper } 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-16 17:11:19 -07:00
import * as types from '../types' ;
2020-06-05 15:53:30 -07:00
import { TimeoutSettings } from '../timeoutSettings' ;
2020-06-10 16:33:27 -07:00
import { WebSocketServer } from './webSocketServer' ;
2020-06-23 14:51:06 -07:00
import { LoggerSink } from '../loggerSink' ;
2020-07-15 15:24:38 -07:00
import { validateDependencies } from './validateDependencies' ;
2020-01-24 14:49:47 -08:00
2020-06-25 08:30:56 -07:00
type FirefoxPrefsOptions = { firefoxUserPrefs ? : { [ key : string ] : string | number | boolean } } ;
type LaunchOptions = types . LaunchOptions & { logger? : LoggerSink } ;
type ConnectOptions = types . ConnectOptions & { logger? : LoggerSink } ;
2020-05-31 09:28:57 -07:00
2020-01-24 14:49:47 -08:00
2020-06-25 08:30:56 -07:00
export type LaunchNonPersistentOptions = LaunchOptions & FirefoxPrefsOptions ;
type LaunchPersistentOptions = LaunchOptions & types . BrowserContextOptions ;
type LaunchServerOptions = types . LaunchServerOptions & { logger? : LoggerSink } & FirefoxPrefsOptions ;
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-06-25 08:30:56 -07:00
launch ( options? : LaunchNonPersistentOptions ) : Promise < Browser > ;
launchServer ( options? : LaunchServerOptions ) : Promise < BrowserServer > ;
launchPersistentContext ( userDataDir : string , options? : LaunchPersistentOptions ) : 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-06-08 21:45:35 -07:00
const mkdirAsync = util . promisify ( fs . mkdir ) ;
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-07-15 15:24:38 -07:00
private _browserDescriptor : browserPaths.BrowserDescriptor ;
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-07-15 15:24:38 -07:00
this . _browserDescriptor = browser ;
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-06-25 08:30:56 -07:00
async launch ( options : LaunchNonPersistentOptions = { } ) : Promise < Browser > {
2020-05-20 16:30:04 -07:00
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-06-10 20:48:54 -07:00
options = validateLaunchOptions ( options ) ;
2020-06-16 17:11:19 -07:00
const loggers = new Loggers ( options . logger ) ;
const browser = await runAbortableTask ( progress = > this . _innerLaunch ( progress , options , loggers , undefined ) , loggers . browser , TimeoutSettings . timeout ( options ) , ` browserType.launch ` ) ;
2020-05-29 14:39:34 -07:00
return browser ;
2020-05-20 16:30:04 -07:00
}
2020-06-25 08:30:56 -07:00
async launchPersistentContext ( userDataDir : string , options : LaunchPersistentOptions = { } ) : 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-06-10 20:48:54 -07:00
options = validateLaunchOptions ( options ) ;
2020-06-08 21:45:35 -07:00
const persistent = validateBrowserContextOptions ( options ) ;
2020-06-16 17:11:19 -07:00
const loggers = new Loggers ( options . logger ) ;
const browser = await runAbortableTask ( progress = > this . _innerLaunch ( progress , options , loggers , persistent , userDataDir ) , loggers . browser , TimeoutSettings . timeout ( options ) , 'browserType.launchPersistentContext' ) ;
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-06-25 08:30:56 -07:00
async _innerLaunch ( progress : Progress , options : LaunchOptions , logger : Loggers , persistent : types.BrowserContextOptions | 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 = {
2020-07-08 21:36:03 -07:00
name : this._name ,
2020-05-29 14:39:34 -07:00
slowMo : options.slowMo ,
persistent ,
2020-06-10 20:48:54 -07:00
headful : ! options . headless ,
2020-06-16 17:11:19 -07:00
loggers : logger ,
2020-05-29 14:39:34 -07:00
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' ) ;
2020-06-10 20:48:54 -07:00
options = validateLaunchOptions ( options ) ;
2020-06-16 17:11:19 -07:00
const loggers = new Loggers ( options . logger ) ;
2020-06-10 20:48:54 -07:00
const { port = 0 } = options ;
2020-06-04 16:43:48 -07:00
return runAbortableTask ( async progress = > {
2020-06-16 17:11:19 -07:00
const { browserServer , transport } = await this . _launchServer ( progress , options , false , loggers ) ;
browserServer . _webSocketServer = this . _startWebSocketServer ( transport , loggers . browser , port ) ;
2020-05-29 14:39:34 -07:00
return browserServer ;
2020-06-16 17:11:19 -07:00
} , loggers . browser , TimeoutSettings . timeout ( options ) , 'browserType.launchServer' ) ;
2020-05-20 16:30:04 -07:00
}
async connect ( options : ConnectOptions ) : Promise < Browser > {
2020-06-16 17:11:19 -07:00
const loggers = new Loggers ( 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 ( ) ;
2020-07-08 21:36:03 -07:00
const browser = await this . _connectToTransport ( transport , { name : this._name , slowMo : options.slowMo , loggers } ) ;
2020-05-21 09:43:10 -07:00
return browser ;
2020-06-16 17:11:19 -07:00
} , loggers . browser , TimeoutSettings . timeout ( options ) , 'browserType.connect' ) ;
2020-05-21 09:43:10 -07:00
}
2020-06-16 17:11:19 -07:00
private async _launchServer ( progress : Progress , options : LaunchServerOptions , isPersistent : boolean , loggers : Loggers , 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-06-08 21:45:35 -07:00
const tempDirectories = [ ] ;
let downloadsPath : string ;
if ( options . downloadsPath ) {
downloadsPath = options . downloadsPath ;
await mkdirAsync ( options . downloadsPath , { recursive : true } ) ;
} else {
downloadsPath = await mkdtempAsync ( DOWNLOADS_FOLDER ) ;
tempDirectories . push ( 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. ` ) ;
2020-07-15 15:24:38 -07:00
if ( ! executablePath ) {
// We can only validate dependencies for bundled browsers.
await validateDependencies ( this . _browserPath , this . _browserDescriptor ) ;
}
2020-05-22 07:03:42 -07:00
// 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 ,
2020-07-21 13:21:42 -07:00
args : this._amendArguments ( browserArguments ) ,
2020-05-22 07:03:42 -07:00
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 ] ;
2020-06-16 17:11:19 -07:00
transport = new PipeTransport ( stdio [ 3 ] , stdio [ 4 ] , loggers . browser ) ;
2020-05-29 14:39:34 -07:00
}
2020-05-22 16:06:00 -07:00
return { browserServer , downloadsPath , transport } ;
2020-05-22 07:03:42 -07:00
}
2020-06-25 08:30:56 -07:00
abstract _defaultArgs ( options : types.LaunchOptionsBase , isPersistent : boolean , userDataDir : string ) : string [ ] ;
2020-05-20 16:30:04 -07:00
abstract _connectToTransport ( transport : ConnectionTransport , options : BrowserOptions ) : Promise < BrowserBase > ;
2020-06-16 17:11:19 -07:00
abstract _startWebSocketServer ( transport : ConnectionTransport , logger : Logger , port : number ) : WebSocketServer ;
2020-05-22 07:03:42 -07:00
abstract _amendEnvironment ( env : Env , userDataDir : string , executable : string , browserArguments : string [ ] ) : Env ;
2020-07-21 13:21:42 -07:00
abstract _amendArguments ( browserArguments : string [ ] ) : string [ ] ;
2020-05-22 07:03:42 -07:00
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 ;
}
}
2020-06-10 20:48:54 -07:00
2020-06-25 08:30:56 -07:00
function validateLaunchOptions < Options extends types.LaunchOptionsBase > ( options : Options ) : Options {
2020-06-11 18:18:33 -07:00
const { devtools = false , headless = ! helper . isDebugMode ( ) && ! devtools } = options ;
2020-06-10 20:48:54 -07:00
return { . . . options , devtools , headless } ;
}