2020-05-11 18:00:33 -07: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-09-02 17:30:10 -07:00
import * as os from 'os' ;
2020-08-23 13:46:40 -07:00
import { CRBrowser , CRBrowserContext } from '../chromium/crBrowser' ;
import { CRConnection , CRSession } from '../chromium/crConnection' ;
import { CRExecutionContext } from '../chromium/crExecutionContext' ;
2020-08-24 06:51:51 -07:00
import * as js from '../javascript' ;
import { Page } from '../page' ;
2020-08-23 13:46:40 -07:00
import { TimeoutSettings } from '../../utils/timeoutSettings' ;
2020-08-24 06:51:51 -07:00
import { WebSocketTransport } from '../transport' ;
import * as types from '../types' ;
2020-11-05 14:15:25 -08:00
import { launchProcess , envArrayToObject } from '../processLauncher' ;
2020-08-24 06:51:51 -07:00
import { BrowserContext } from '../browserContext' ;
2020-05-13 20:51:53 -07:00
import type { BrowserWindow } from 'electron' ;
2020-11-05 14:15:25 -08:00
import { Progress , ProgressController , runAbortableTask } from '../progress' ;
2020-06-10 15:12:50 -07:00
import { EventEmitter } from 'events' ;
2020-08-24 06:51:51 -07:00
import { helper } from '../helper' ;
2021-01-29 16:00:56 -08:00
import { BrowserOptions , BrowserProcess , PlaywrightOptions } from '../browser' ;
2020-11-05 14:15:25 -08:00
import * as childProcess from 'child_process' ;
import * as readline from 'readline' ;
2020-12-08 09:35:28 -08:00
import { RecentLogsCollector } from '../../utils/debugLogger' ;
2020-05-11 18:00:33 -07:00
2020-07-20 17:38:06 -07:00
export type ElectronLaunchOptionsBase = {
2020-05-11 18:00:33 -07:00
args? : string [ ] ,
cwd? : string ,
2020-08-18 09:37:40 -07:00
env? : types.EnvArray ,
2020-05-11 18:00:33 -07:00
handleSIGINT? : boolean ,
handleSIGTERM? : boolean ,
handleSIGHUP? : boolean ,
timeout? : number ,
} ;
2020-07-15 18:48:19 -07:00
export interface ElectronPage extends Page {
2020-05-13 20:51:53 -07:00
browserWindow : js.JSHandle < BrowserWindow > ;
_browserWindowId : number ;
}
2020-06-10 15:12:50 -07:00
export class ElectronApplication extends EventEmitter {
2020-08-21 16:26:33 -07:00
static Events = {
Close : 'close' ,
Window : 'window' ,
} ;
2020-05-11 18:00:33 -07:00
private _browserContext : CRBrowserContext ;
private _nodeConnection : CRConnection ;
private _nodeSession : CRSession ;
private _nodeExecutionContext : js.ExecutionContext | undefined ;
2020-07-13 21:46:59 -07:00
_nodeElectronHandle : js.JSHandle < any > | undefined ;
2020-05-13 20:51:53 -07:00
private _windows = new Set < ElectronPage > ( ) ;
2020-05-11 18:00:33 -07:00
private _lastWindowId = 0 ;
readonly _timeoutSettings = new TimeoutSettings ( ) ;
2020-08-17 14:12:31 -07:00
constructor ( browser : CRBrowser , nodeConnection : CRConnection ) {
2020-05-11 18:00:33 -07:00
super ( ) ;
2020-05-20 16:30:04 -07:00
this . _browserContext = browser . _defaultContext as CRBrowserContext ;
2020-08-21 16:26:33 -07:00
this . _browserContext . on ( BrowserContext . Events . Close , ( ) = > {
2020-07-23 11:02:43 -07:00
// Emit application closed after context closed.
2020-08-21 16:26:33 -07:00
Promise . resolve ( ) . then ( ( ) = > this . emit ( ElectronApplication . Events . Close ) ) ;
2020-07-23 11:02:43 -07:00
} ) ;
2020-08-21 16:26:33 -07:00
this . _browserContext . on ( BrowserContext . Events . Page , event = > this . _onPage ( event ) ) ;
2020-05-11 18:00:33 -07:00
this . _nodeConnection = nodeConnection ;
this . _nodeSession = nodeConnection . rootSession ;
}
2020-05-13 20:51:53 -07:00
private async _onPage ( page : ElectronPage ) {
2020-05-11 18:00:33 -07:00
// Needs to be sync.
const windowId = ++ this . _lastWindowId ;
2020-08-21 16:26:33 -07:00
page . on ( Page . Events . Close , ( ) = > {
2020-05-13 20:51:53 -07:00
page . browserWindow . dispose ( ) ;
2020-05-11 18:00:33 -07:00
this . _windows . delete ( page ) ;
} ) ;
2020-09-29 18:01:14 -07:00
page . _browserWindowId = windowId ;
2020-05-11 18:00:33 -07:00
this . _windows . add ( page ) ;
2020-09-29 18:01:14 -07:00
// Below is async.
const handle = await this . _nodeElectronHandle ! . evaluateHandle ( ( { BrowserWindow } , windowId ) = > BrowserWindow . fromId ( windowId ) , windowId ) . catch ( e = > { } ) ;
if ( ! handle )
return ;
page . browserWindow = handle ;
2020-09-14 16:43:17 -07:00
await runAbortableTask ( progress = > page . mainFrame ( ) . _waitForLoadState ( progress , 'domcontentloaded' ) , page . _timeoutSettings . navigationTimeout ( { } ) ) . catch ( e = > { } ) ; // can happen after detach
2020-08-21 16:26:33 -07:00
this . emit ( ElectronApplication . Events . Window , page ) ;
2020-05-11 18:00:33 -07:00
}
async newBrowserWindow ( options : any ) : Promise < Page > {
2020-08-17 14:36:51 -07:00
const windowId = await this . _nodeElectronHandle ! . evaluate ( async ( { BrowserWindow } , options ) = > {
2020-05-11 18:00:33 -07:00
const win = new BrowserWindow ( options ) ;
win . loadURL ( 'about:blank' ) ;
return win . id ;
} , options ) ;
for ( const page of this . _windows ) {
2020-05-13 20:51:53 -07:00
if ( page . _browserWindowId === windowId )
2020-05-11 18:00:33 -07:00
return page ;
}
2020-08-21 16:26:33 -07:00
return await this . _waitForEvent ( ElectronApplication . Events . Window , ( page : ElectronPage ) = > page . _browserWindowId === windowId ) ;
2020-05-11 18:00:33 -07:00
}
context ( ) : BrowserContext {
return this . _browserContext ;
}
async close() {
2020-08-21 16:26:33 -07:00
const closed = this . _waitForEvent ( ElectronApplication . Events . Close ) ;
2020-08-17 14:36:51 -07:00
await this . _nodeElectronHandle ! . evaluate ( ( { app } ) = > app . quit ( ) ) ;
2020-05-11 18:00:33 -07:00
this . _nodeConnection . close ( ) ;
2020-07-23 11:02:43 -07:00
await closed ;
2020-05-11 18:00:33 -07:00
}
2020-08-17 16:19:21 -07:00
private async _waitForEvent ( event : string , predicate? : Function ) : Promise < any > {
2020-09-17 09:32:54 -07:00
const progressController = new ProgressController ( ) ;
2020-08-21 16:26:33 -07:00
if ( event !== ElectronApplication . Events . Close )
2020-06-10 15:12:50 -07:00
this . _browserContext . _closePromise . then ( error = > progressController . abort ( error ) ) ;
2020-09-17 09:32:54 -07:00
return progressController . run ( progress = > helper . waitForEvent ( progress , this , event , predicate ) . promise , this . _timeoutSettings . timeout ( { } ) ) ;
2020-06-10 15:12:50 -07:00
}
2020-05-11 18:00:33 -07:00
async _init ( ) {
2020-09-29 18:01:14 -07:00
this . _nodeSession . on ( 'Runtime.executionContextCreated' , ( event : any ) = > {
if ( event . context . auxData && event . context . auxData . isDefault )
this . _nodeExecutionContext = new js . ExecutionContext ( new CRExecutionContext ( this . _nodeSession , event . context ) ) ;
2020-05-11 18:00:33 -07:00
} ) ;
await this . _nodeSession . send ( 'Runtime.enable' , { } ) . catch ( e = > { } ) ;
2020-09-29 18:01:14 -07:00
this . _nodeElectronHandle = await js . evaluate ( this . _nodeExecutionContext ! , false /* returnByValue */ , ` process.mainModule.require('electron') ` ) ;
2020-05-11 18:00:33 -07:00
}
}
export class Electron {
2021-01-29 16:00:56 -08:00
private _playwrightOptions : PlaywrightOptions ;
constructor ( playwrightOptions : PlaywrightOptions ) {
this . _playwrightOptions = playwrightOptions ;
}
2020-08-17 14:12:31 -07:00
async launch ( executablePath : string , options : ElectronLaunchOptionsBase = { } ) : Promise < ElectronApplication > {
2020-05-11 18:00:33 -07:00
const {
args = [ ] ,
handleSIGINT = true ,
handleSIGTERM = true ,
handleSIGHUP = true ,
} = options ;
2020-09-17 09:32:54 -07:00
const controller = new ProgressController ( ) ;
2020-09-10 15:34:13 -07:00
controller . setLogName ( 'browser' ) ;
return controller . run ( async progress = > {
2020-05-29 14:39:34 -07:00
let app : ElectronApplication | undefined = undefined ;
2020-09-29 18:01:14 -07:00
const electronArguments = [ '--inspect=0' , '--remote-debugging-port=0' , . . . args ] ;
2020-09-02 17:30:10 -07:00
if ( os . platform ( ) === 'linux' ) {
const runningAsRoot = process . geteuid && process . geteuid ( ) === 0 ;
2020-09-02 18:02:11 -07:00
if ( runningAsRoot && electronArguments . indexOf ( '--no-sandbox' ) === - 1 )
2020-09-02 17:30:10 -07:00
electronArguments . push ( '--no-sandbox' ) ;
}
2020-12-08 09:35:28 -08:00
const browserLogsCollector = new RecentLogsCollector ( ) ;
2020-05-29 14:39:34 -07:00
const { launchedProcess , gracefullyClose , kill } = await launchProcess ( {
executablePath ,
args : electronArguments ,
2020-08-18 09:37:40 -07:00
env : options.env ? envArrayToObject ( options . env ) : process . env ,
2020-05-29 14:39:34 -07:00
handleSIGINT ,
handleSIGTERM ,
handleSIGHUP ,
2020-12-08 09:35:28 -08:00
log : ( message : string ) = > {
progress . log ( message ) ;
browserLogsCollector . log ( message ) ;
} ,
2020-11-05 14:15:25 -08:00
stdio : 'pipe' ,
2020-05-29 14:39:34 -07:00
cwd : options.cwd ,
tempDirectories : [ ] ,
attemptToGracefullyClose : ( ) = > app ! . close ( ) ,
2020-09-29 18:01:14 -07:00
onExit : ( ) = > { } ,
2020-05-29 14:39:34 -07:00
} ) ;
2020-11-05 14:15:25 -08:00
const nodeMatch = await waitForLine ( progress , launchedProcess , /^Debugger listening on (ws:\/\/.*)$/ ) ;
2020-05-29 14:39:34 -07:00
const nodeTransport = await WebSocketTransport . connect ( progress , nodeMatch [ 1 ] ) ;
2020-12-08 09:35:28 -08:00
const nodeConnection = new CRConnection ( nodeTransport , helper . debugProtocolLogger ( ) , browserLogsCollector ) ;
2020-05-29 14:39:34 -07:00
2020-11-05 14:15:25 -08:00
const chromeMatch = await waitForLine ( progress , launchedProcess , /^DevTools listening on (ws:\/\/.*)$/ ) ;
2020-05-29 14:39:34 -07:00
const chromeTransport = await WebSocketTransport . connect ( progress , chromeMatch [ 1 ] ) ;
2020-08-14 13:19:12 -07:00
const browserProcess : BrowserProcess = {
onclose : undefined ,
process : launchedProcess ,
close : gracefullyClose ,
kill
} ;
2020-11-11 15:12:10 -08:00
const browserOptions : BrowserOptions = {
2021-01-29 16:00:56 -08:00
. . . this . _playwrightOptions ,
2020-11-11 15:12:10 -08:00
name : 'electron' ,
2021-01-13 12:08:14 -08:00
isChromium : true ,
2020-11-11 15:12:10 -08:00
headful : true ,
persistent : { noDefaultViewport : true } ,
browserProcess ,
protocolLogger : helper.debugProtocolLogger ( ) ,
2020-12-08 09:35:28 -08:00
browserLogsCollector ,
2020-11-11 15:12:10 -08:00
} ;
const browser = await CRBrowser . connect ( chromeTransport , browserOptions ) ;
2020-08-17 14:12:31 -07:00
app = new ElectronApplication ( browser , nodeConnection ) ;
2020-05-29 14:39:34 -07:00
await app . _init ( ) ;
return app ;
2020-09-17 09:32:54 -07:00
} , TimeoutSettings . timeout ( options ) ) ;
2020-05-11 18:00:33 -07:00
}
}
2020-11-05 14:15:25 -08:00
function waitForLine ( progress : Progress , process : childProcess.ChildProcess , regex : RegExp ) : Promise < RegExpMatchArray > {
return new Promise ( ( resolve , reject ) = > {
const rl = readline . createInterface ( { input : process.stderr } ) ;
const failError = new Error ( 'Process failed to launch!' ) ;
const listeners = [
helper . addEventListener ( rl , 'line' , onLine ) ,
helper . addEventListener ( rl , 'close' , reject . bind ( null , failError ) ) ,
helper . addEventListener ( process , 'exit' , reject . bind ( null , failError ) ) ,
// It is Ok to remove error handler because we did not create process and there is another listener.
helper . addEventListener ( process , 'error' , reject . bind ( null , failError ) )
] ;
progress . cleanupWhenAborted ( cleanup ) ;
function onLine ( line : string ) {
const match = line . match ( regex ) ;
if ( ! match )
return ;
cleanup ( ) ;
resolve ( match ) ;
}
function cleanup() {
helper . removeEventListeners ( listeners ) ;
}
} ) ;
}