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-11 18:18:33 -07:00
import { assert , getFromENV , logPolitely , helper } 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-06-10 20:48:54 -07:00
import { LaunchOptionsBase , BrowserTypeBase } from './browserType' ;
2020-06-10 16:33:27 -07:00
import { ConnectionTransport , ProtocolRequest , ProtocolResponse } from '../transport' ;
import { InnerLogger } from '../logger' ;
2020-04-28 17:06:01 -07:00
import { BrowserDescriptor } from '../install/browserPaths' ;
2020-06-11 18:18:33 -07:00
import { CRDevTools } from '../chromium/crDevTools' ;
2020-05-21 19:16:13 -07:00
import { BrowserOptions } from '../browser' ;
2020-06-10 16:33:27 -07:00
import { WebSocketServer } from './webSocketServer' ;
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-06-11 18:18:33 -07:00
if ( helper . 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-06-10 16:33:27 -07:00
_startWebSocketServer ( transport : ConnectionTransport , logger : InnerLogger , port : number ) : WebSocketServer {
return startWebSocketServer ( transport , logger , port ) ;
2019-12-19 16:53:24 -08:00
}
2020-06-08 21:45:35 -07:00
_defaultArgs ( options : LaunchOptionsBase , isPersistent : boolean , userDataDir : string ) : string [ ] {
2020-06-05 13:50:15 -07:00
const { args = [ ] , proxy } = 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-06-10 20:48:54 -07:00
if ( options . devtools )
2020-01-07 13:58:23 -08:00
chromeArguments . push ( '--auto-open-devtools-for-tabs' ) ;
2020-06-10 20:48:54 -07:00
if ( options . headless ) {
2020-01-07 13:58:23 -08:00
chromeArguments . push (
'--headless' ,
'--hide-scrollbars' ,
'--mute-audio'
) ;
}
2020-06-05 13:50:15 -07:00
if ( proxy ) {
const proxyURL = new URL ( proxy . server ) ;
const isSocks = proxyURL . protocol === 'socks5:' ;
// https://www.chromium.org/developers/design-documents/network-settings
if ( isSocks ) {
// https://www.chromium.org/developers/design-documents/network-stack/socks-proxy
chromeArguments . push ( ` --host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${ proxyURL . hostname } " ` ) ;
}
chromeArguments . push ( ` --proxy-server= ${ proxy . server } ` ) ;
if ( proxy . bypass ) {
const patterns = proxy . bypass . split ( ',' ) . map ( t = > t . trim ( ) ) . map ( t = > t . startsWith ( '.' ) ? '*' + t : t ) ;
chromeArguments . push ( ` --proxy-bypass-list= ${ patterns . join ( ';' ) } ` ) ;
}
}
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-06-10 16:33:27 -07:00
function startWebSocketServer ( transport : ConnectionTransport , logger : InnerLogger , port : number ) : WebSocketServer {
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 [ ] } > ( ) ;
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 ,
2020-06-10 16:33:27 -07:00
parent : parentSessionId ,
2020-05-19 15:02:13 -07:00
} ) ;
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-06-10 16:33:27 -07:00
const server = new WebSocketServer ( transport , logger , port , {
onBrowserResponse ( seqNum : number , source : ws , message : ProtocolResponse ) {
if ( awaitingBrowserTarget . has ( seqNum ) ) {
const freshSocket = awaitingBrowserTarget . get ( seqNum ) ! ;
awaitingBrowserTarget . delete ( seqNum ) ;
2020-03-27 15:18:34 -07:00
2020-06-10 16:33:27 -07:00
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 ;
server . sendMessageToBrowser ( item , source ) ;
}
socketToBrowserSession . set ( freshSocket , { sessionId } ) ;
addSession ( sessionId , freshSocket ) ;
} else {
server . sendMessageToBrowserOneWay ( 'Target.detachFromTarget' , { sessionId } ) ;
socketToBrowserSession . delete ( freshSocket ) ;
2020-03-27 15:18:34 -07:00
}
2020-06-10 16:33:27 -07:00
return ;
2020-03-27 15:18:34 -07:00
}
2020-06-10 16:33:27 -07:00
if ( message . id === - 1 )
return ;
2020-03-27 15:18:34 -07:00
2020-06-10 16:33:27 -07:00
// At this point everything we care about has sessionId.
if ( ! message . sessionId )
return ;
2020-03-27 15:18:34 -07:00
2020-06-10 16:33:27 -07:00
const data = sessionToData . get ( message . sessionId ) ;
if ( data && data . socket . readyState !== ws . CLOSING ) {
if ( data . isBrowserSession )
delete message . sessionId ;
data . socket . send ( JSON . stringify ( message ) ) ;
}
} ,
2020-03-27 15:18:34 -07:00
2020-06-10 16:33:27 -07:00
onBrowserNotification ( message : ProtocolResponse ) {
// At this point everything we care about has sessionId.
if ( ! message . sessionId )
return ;
2020-03-27 15:18:34 -07:00
2020-06-10 16:33:27 -07:00
const data = sessionToData . get ( message . sessionId ) ;
if ( data && data . socket . readyState !== ws . CLOSING ) {
if ( message . method === 'Target.attachedToTarget' )
addSession ( message . params . sessionId , data . socket , message . sessionId ) ;
if ( message . method === 'Target.detachedFromTarget' )
removeSession ( message . params . sessionId ) ;
// Strip session ids from the browser sessions.
if ( data . isBrowserSession )
delete message . sessionId ;
data . socket . send ( JSON . stringify ( message ) ) ;
}
} ,
onClientAttached ( socket : ws ) {
socketToBrowserSession . set ( socket , { queue : [ ] } ) ;
2020-03-27 15:18:34 -07:00
2020-06-10 16:33:27 -07:00
const seqNum = server . sendMessageToBrowser ( {
id : - 1 , // Proxy-initiated request.
method : 'Target.attachToBrowserTarget' ,
params : { }
} , socket ) ;
awaitingBrowserTarget . set ( seqNum , socket ) ;
} ,
onClientRequest ( socket : ws , message : ProtocolRequest ) {
2020-03-27 15:18:34 -07:00
// If message has sessionId, pass through.
2020-06-10 16:33:27 -07:00
if ( message . sessionId ) {
server . sendMessageToBrowser ( message , socket ) ;
2020-03-27 15:18:34 -07:00
return ;
}
// If message has no sessionId, look it up.
const session = socketToBrowserSession . get ( socket ) ! ;
if ( session . sessionId ) {
// We have it, use it.
2020-06-10 16:33:27 -07:00
message . sessionId = session . sessionId ;
server . sendMessageToBrowser ( message , socket ) ;
2020-03-27 15:18:34 -07:00
return ;
}
// Pending session id, queue the message.
2020-06-10 16:33:27 -07:00
session . queue ! . push ( message ) ;
} ,
2020-03-27 15:18:34 -07:00
2020-06-10 16:33:27 -07:00
onClientDetached ( socket : ws ) {
2020-03-27 15:18:34 -07:00
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 ) ;
2020-06-10 16:33:27 -07:00
server . sendMessageToBrowserOneWay ( 'Target.detachFromTarget' , { sessionId : session.sessionId } ) ;
}
2020-03-27 15:18:34 -07:00
} ) ;
2020-06-10 16:33:27 -07:00
return server ;
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' ,
] ;