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 path from 'path' ;
2020-06-04 16:40:07 -07:00
import { helper , assert , getFromENV , logPolitely } from '../helper' ;
2020-01-23 14:40:37 -08:00
import { CRBrowser } from '../chromium/crBrowser' ;
2020-03-27 15:18:34 -07:00
import * as ws from 'ws' ;
2020-05-22 07:03:42 -07:00
import { Env } from './processLauncher' ;
2020-01-23 17:45:31 -08:00
import { kBrowserCloseMessageId } from '../chromium/crConnection' ;
2020-05-22 16:06:00 -07:00
import { BrowserArgOptions , BrowserTypeBase , processBrowserArgOptions } from './browserType' ;
2020-05-22 07:03:42 -07:00
import { WebSocketWrapper } from './browserServer' ;
2020-05-20 16:30:04 -07:00
import { ConnectionTransport , ProtocolRequest } from '../transport' ;
2020-05-22 07:03:42 -07:00
import { InnerLogger , logError } from '../logger' ;
2020-04-28 17:06:01 -07:00
import { BrowserDescriptor } from '../install/browserPaths' ;
2020-05-26 14:08:32 -07:00
import { CRDevTools } from '../debug/crDevTools' ;
import * as debugSupport from '../debug/debugSupport' ;
2020-05-21 19:16:13 -07:00
import { BrowserOptions } from '../browser' ;
2020-01-07 13:58:23 -08:00
2020-05-20 16:30:04 -07:00
export class Chromium extends BrowserTypeBase {
2020-05-19 14:55:11 -07:00
private _devtools : CRDevTools | undefined ;
2020-06-04 16:40:07 -07:00
private _debugPort : number | undefined ;
2020-05-19 14:55:11 -07:00
2020-04-28 17:06:01 -07:00
constructor ( packagePath : string , browser : BrowserDescriptor ) {
2020-06-04 16:40:07 -07:00
const debugPortStr = getFromENV ( 'PLAYWRIGHT_CHROMIUM_DEBUG_PORT' ) ;
const debugPort : number | undefined = debugPortStr ? + debugPortStr : undefined ;
if ( debugPort !== undefined ) {
if ( Number . isNaN ( debugPort ) )
throw new Error ( ` PLAYWRIGHT_CHROMIUM_DEBUG_PORT must be a number, but is set to " ${ debugPortStr } " ` ) ;
logPolitely ( ` NOTE: Chromium will be launched in debug mode on port ${ debugPort } ` ) ;
}
super ( packagePath , browser , debugPort ? { webSocketRegex : /^DevTools listening on (ws:\/\/.*)$/ , stream : 'stderr' } : null ) ;
this . _debugPort = debugPort ;
2020-05-26 14:08:32 -07:00
if ( debugSupport . isDebugMode ( ) )
2020-05-19 14:55:11 -07:00
this . _devtools = this . _createDevTools ( ) ;
}
private _createDevTools() {
return new CRDevTools ( path . join ( this . _browserPath , 'devtools-preferences.json' ) ) ;
2020-01-28 18:09:07 -08:00
}
2020-05-21 19:16:13 -07:00
async _connectToTransport ( transport : ConnectionTransport , options : BrowserOptions ) : Promise < CRBrowser > {
2020-05-20 16:30:04 -07:00
let devtools = this . _devtools ;
if ( ( options as any ) . __testHookForDevTools ) {
devtools = this . _createDevTools ( ) ;
await ( options as any ) . __testHookForDevTools ( devtools ) ;
}
2020-05-21 19:16:13 -07:00
return CRBrowser . connect ( transport , options , devtools ) ;
2020-02-04 19:41:38 -08:00
}
2020-05-22 07:03:42 -07:00
_amendEnvironment ( env : Env , userDataDir : string , executable : string , browserArguments : string [ ] ) : Env {
2020-05-12 16:43:15 -07:00
const runningAsRoot = process . geteuid && process . geteuid ( ) === 0 ;
2020-05-22 07:03:42 -07:00
assert ( ! runningAsRoot || browserArguments . includes ( '--no-sandbox' ) , 'Cannot launch Chromium as root without --no-sandbox. See https://crbug.com/638180.' ) ;
return env ;
}
2020-05-14 13:22:33 -07:00
2020-05-22 07:03:42 -07:00
_attemptToGracefullyCloseBrowser ( transport : ConnectionTransport ) : void {
const message : ProtocolRequest = { method : 'Browser.close' , id : kBrowserCloseMessageId , params : { } } ;
transport . send ( message ) ;
}
2020-01-07 13:58:23 -08:00
2020-05-22 07:03:42 -07:00
_wrapTransportWithWebSocket ( transport : ConnectionTransport , logger : InnerLogger , port : number ) : WebSocketWrapper {
return wrapTransportWithWebSocket ( transport , logger , port ) ;
2019-12-19 16:53:24 -08:00
}
2020-05-22 16:06:00 -07:00
_defaultArgs ( options : BrowserArgOptions , isPersistent : boolean , userDataDir : string ) : string [ ] {
2020-05-14 13:22:33 -07:00
const { devtools , headless } = processBrowserArgOptions ( options ) ;
const { 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' ) ;
2020-04-18 19:06:42 -07:00
if ( args . find ( arg = > arg . startsWith ( '--remote-debugging-pipe' ) ) )
2020-02-05 16:36:36 -08:00
throw new Error ( 'Playwright manages remote debugging connection itself.' ) ;
2020-05-10 15:23:53 -07:00
if ( args . find ( arg = > ! arg . startsWith ( '-' ) ) )
2020-02-27 14:09:24 -08:00
throw new Error ( 'Arguments can not specify page to be opened' ) ;
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 } ` ) ;
2020-06-04 16:40:07 -07:00
if ( this . _debugPort !== undefined )
chromeArguments . push ( '--remote-debugging-port=' + this . _debugPort ) ;
else
chromeArguments . push ( '--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-05 16:36:36 -08:00
chromeArguments . push ( . . . args ) ;
2020-05-22 16:06:00 -07:00
if ( isPersistent )
2020-05-10 15:23:53 -07:00
chromeArguments . push ( 'about:blank' ) ;
else
2020-03-05 14:46:12 -08:00
chromeArguments . push ( '--no-startup-window' ) ;
2020-01-07 13:58:23 -08:00
return chromeArguments ;
2019-12-19 16:53:24 -08:00
}
}
2020-05-19 15:02:13 -07:00
type SessionData = {
socket : ws ,
children : Set < string > ,
isBrowserSession : boolean ,
parent? : string ,
} ;
2020-04-20 23:24:53 -07:00
function wrapTransportWithWebSocket ( transport : ConnectionTransport , logger : InnerLogger , port : number ) : WebSocketWrapper {
2020-03-27 15:18:34 -07:00
const server = new ws . Server ( { port } ) ;
2020-04-01 14:42:47 -07:00
const guid = helper . guid ( ) ;
2020-03-27 15:18:34 -07:00
const awaitingBrowserTarget = new Map < number , ws > ( ) ;
2020-05-19 15:02:13 -07:00
const sessionToData = new Map < string , SessionData > ( ) ;
2020-03-27 15:18:34 -07:00
const socketToBrowserSession = new Map < ws , { sessionId ? : string , queue ? : ProtocolRequest [ ] } > ( ) ;
let lastSequenceNumber = 1 ;
2020-05-19 15:02:13 -07:00
function addSession ( sessionId : string , socket : ws , parentSessionId? : string ) {
sessionToData . set ( sessionId , {
socket ,
children : new Set ( ) ,
isBrowserSession : ! parentSessionId ,
parent : parentSessionId
} ) ;
if ( parentSessionId )
sessionToData . get ( parentSessionId ) ! . children . add ( sessionId ) ;
}
function removeSession ( sessionId : string ) {
const data = sessionToData . get ( sessionId ) ! ;
for ( const child of data . children )
removeSession ( child ) ;
if ( data . parent )
sessionToData . get ( data . parent ) ! . children . delete ( sessionId ) ;
sessionToData . delete ( sessionId ) ;
}
2020-03-27 15:18:34 -07:00
transport . onmessage = message = > {
if ( typeof message . id === 'number' && awaitingBrowserTarget . has ( message . id ) ) {
const freshSocket = awaitingBrowserTarget . get ( message . id ) ! ;
awaitingBrowserTarget . delete ( message . id ) ;
const sessionId = message . result . sessionId ;
if ( freshSocket . readyState !== ws . CLOSED && freshSocket . readyState !== ws . CLOSING ) {
const { queue } = socketToBrowserSession . get ( freshSocket ) ! ;
for ( const item of queue ! ) {
item . sessionId = sessionId ;
transport . send ( item ) ;
}
socketToBrowserSession . set ( freshSocket , { sessionId } ) ;
2020-05-19 15:02:13 -07:00
addSession ( sessionId , freshSocket ) ;
2020-03-27 15:18:34 -07:00
} else {
transport . send ( {
id : ++ lastSequenceNumber ,
method : 'Target.detachFromTarget' ,
params : { sessionId }
} ) ;
socketToBrowserSession . delete ( freshSocket ) ;
}
return ;
}
// At this point everything we care about has sessionId.
if ( ! message . sessionId )
return ;
2020-05-19 15:02:13 -07:00
const data = sessionToData . get ( message . sessionId ) ;
if ( data && data . socket . readyState !== ws . CLOSING ) {
2020-03-27 15:18:34 -07:00
if ( message . method === 'Target.attachedToTarget' )
2020-05-19 15:02:13 -07:00
addSession ( message . params . sessionId , data . socket , message . sessionId ) ;
2020-03-27 15:18:34 -07:00
if ( message . method === 'Target.detachedFromTarget' )
2020-05-19 15:02:13 -07:00
removeSession ( message . params . sessionId ) ;
2020-03-27 15:18:34 -07:00
// Strip session ids from the browser sessions.
2020-05-19 15:02:13 -07:00
if ( data . isBrowserSession )
2020-03-27 15:18:34 -07:00
delete message . sessionId ;
2020-05-19 15:02:13 -07:00
data . socket . send ( JSON . stringify ( message ) ) ;
2020-03-27 15:18:34 -07:00
}
} ;
transport . onclose = ( ) = > {
for ( const socket of socketToBrowserSession . keys ( ) ) {
socket . removeListener ( 'close' , ( socket as any ) . __closeListener ) ;
socket . close ( undefined , 'Browser disconnected' ) ;
}
server . close ( ) ;
transport . onmessage = undefined ;
transport . onclose = undefined ;
} ;
server . on ( 'connection' , ( socket : ws , req ) = > {
if ( req . url !== '/' + guid ) {
socket . close ( ) ;
return ;
}
socketToBrowserSession . set ( socket , { queue : [ ] } ) ;
transport . send ( {
id : ++ lastSequenceNumber ,
method : 'Target.attachToBrowserTarget' ,
params : { }
} ) ;
awaitingBrowserTarget . set ( lastSequenceNumber , socket ) ;
socket . on ( 'message' , ( message : string ) = > {
const parsedMessage = JSON . parse ( Buffer . from ( message ) . toString ( ) ) as ProtocolRequest ;
// If message has sessionId, pass through.
if ( parsedMessage . sessionId ) {
transport . send ( parsedMessage ) ;
return ;
}
// If message has no sessionId, look it up.
const session = socketToBrowserSession . get ( socket ) ! ;
if ( session . sessionId ) {
// We have it, use it.
parsedMessage . sessionId = session . sessionId ;
transport . send ( parsedMessage ) ;
return ;
}
// Pending session id, queue the message.
session . queue ! . push ( parsedMessage ) ;
} ) ;
2020-04-20 07:52:26 -07:00
socket . on ( 'error' , logError ( logger ) ) ;
2020-03-30 18:18:38 -07:00
2020-03-27 15:18:34 -07:00
socket . on ( 'close' , ( socket as any ) . __closeListener = ( ) = > {
const session = socketToBrowserSession . get ( socket ) ;
if ( ! session || ! session . sessionId )
return ;
2020-05-19 15:02:13 -07:00
removeSession ( session . sessionId ) ;
2020-03-27 15:18:34 -07:00
socketToBrowserSession . delete ( socket ) ;
transport . send ( {
id : ++ lastSequenceNumber ,
method : 'Target.detachFromTarget' ,
params : { sessionId : session.sessionId }
} ) ;
} ) ;
} ) ;
const address = server . address ( ) ;
2020-03-30 13:49:52 -07:00
const wsEndpoint = typeof address === 'string' ? ` ${ address } / ${ guid } ` : ` ws://127.0.0.1: ${ address . port } / ${ guid } ` ;
2020-05-19 15:02:13 -07:00
return new WebSocketWrapper ( wsEndpoint , [ awaitingBrowserTarget , sessionToData , socketToBrowserSession ] ) ;
2020-03-27 15:18:34 -07:00
}
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
2020-05-04 13:43:44 -07:00
'--disable-features=TranslateUI,BlinkGenPropertyTrees,ImprovedCookieControls,SameSiteByDefaultCookies' ,
2020-01-07 13:58:23 -08:00
'--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' ,
] ;