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 .
* /
import * as path from 'path' ;
import { CRBrowser , CRBrowserContext } from '../chromium/crBrowser' ;
import { CRConnection , CRSession } from '../chromium/crConnection' ;
import { CRExecutionContext } from '../chromium/crExecutionContext' ;
import { TimeoutError } from '../errors' ;
import { Events } from '../events' ;
import { ExtendedEventEmitter } from '../extendedEventEmitter' ;
import { helper } from '../helper' ;
import * as js from '../javascript' ;
import { InnerLogger , Logger , RootLogger } from '../logger' ;
import { Page } from '../page' ;
import { TimeoutSettings } from '../timeoutSettings' ;
import { WebSocketTransport } from '../transport' ;
import * as types from '../types' ;
import { BrowserServer } from './browserServer' ;
import { launchProcess , waitForLine } from './processLauncher' ;
import { BrowserContext } from '../browserContext' ;
2020-05-13 20:51:53 -07:00
import type { BrowserWindow } from 'electron' ;
2020-05-11 18:00:33 -07:00
type ElectronLaunchOptions = {
args? : string [ ] ,
cwd? : string ,
env ? : { [ key : string ] : string | number | boolean } ,
handleSIGINT? : boolean ,
handleSIGTERM? : boolean ,
handleSIGHUP? : boolean ,
timeout? : number ,
logger? : Logger ,
} ;
export const ElectronEvents = {
ElectronApplication : {
Close : 'close' ,
Window : 'window' ,
}
} ;
2020-05-13 20:51:53 -07:00
interface ElectronPage extends Page {
browserWindow : js.JSHandle < BrowserWindow > ;
_browserWindowId : number ;
}
2020-05-11 18:00:33 -07:00
export class ElectronApplication extends ExtendedEventEmitter {
private _logger : InnerLogger ;
private _browserContext : CRBrowserContext ;
private _nodeConnection : CRConnection ;
private _nodeSession : CRSession ;
private _nodeExecutionContext : js.ExecutionContext | undefined ;
private _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 ( ) ;
constructor ( logger : InnerLogger , browser : CRBrowser , nodeConnection : CRConnection ) {
super ( ) ;
this . _logger = logger ;
this . _browserContext = browser . _defaultContext ! ;
this . _browserContext . on ( Events . BrowserContext . Close , ( ) = > this . emit ( ElectronEvents . ElectronApplication . Close ) ) ;
this . _browserContext . on ( Events . BrowserContext . Page , event = > this . _onPage ( event ) ) ;
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 ;
// Can be async.
2020-05-12 15:28:37 -07:00
const handle = await this . _nodeElectronHandle ! . evaluateHandle ( ( { BrowserWindow } , windowId ) = > BrowserWindow . fromId ( windowId ) , windowId ) . catch ( e = > { } ) ;
if ( ! handle )
return ;
2020-05-13 20:51:53 -07:00
page . browserWindow = handle ;
page . _browserWindowId = windowId ;
2020-05-11 18:00:33 -07:00
page . on ( Events . Page . Close , ( ) = > {
2020-05-13 20:51:53 -07:00
page . browserWindow . dispose ( ) ;
2020-05-11 18:00:33 -07:00
this . _windows . delete ( page ) ;
} ) ;
this . _windows . add ( page ) ;
2020-05-12 15:28:37 -07:00
await page . waitForLoadState ( 'domcontentloaded' ) . catch ( e = > { } ) ; // can happen after detach
2020-05-11 18:00:33 -07:00
this . emit ( ElectronEvents . ElectronApplication . Window , page ) ;
}
windows ( ) : Page [ ] {
return [ . . . this . _windows ] ;
}
2020-05-12 08:43:41 -07:00
async firstWindow ( ) : Promise < Page > {
if ( this . _windows . size )
return this . _windows . values ( ) . next ( ) . value ;
return this . waitForEvent ( 'window' ) ;
}
2020-05-11 18:00:33 -07:00
async newBrowserWindow ( options : any ) : Promise < Page > {
const windowId = await this . evaluate ( async ( { BrowserWindow } , options ) = > {
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-05-13 20:51:53 -07:00
return await this . waitForEvent ( ElectronEvents . ElectronApplication . Window , ( page : ElectronPage ) = > page . _browserWindowId === windowId ) ;
2020-05-11 18:00:33 -07:00
}
context ( ) : BrowserContext {
return this . _browserContext ;
}
async close() {
await this . evaluate ( ( { app } ) = > app . quit ( ) ) ;
this . _nodeConnection . close ( ) ;
}
async _init ( ) {
this . _nodeSession . once ( 'Runtime.executionContextCreated' , event = > {
this . _nodeExecutionContext = new js . ExecutionContext ( new CRExecutionContext ( this . _nodeSession , event . context ) , this . _logger ) ;
} ) ;
await this . _nodeSession . send ( 'Runtime.enable' , { } ) . catch ( e = > { } ) ;
this . _nodeElectronHandle = await this . _nodeExecutionContext ! . evaluateHandleInternal ( ( ) = > {
// Resolving the race between the debugger and the boot-time script.
if ( ( global as any ) . _playwrightRun )
return ( global as any ) . _playwrightRun ( ) ;
return new Promise ( f = > ( global as any ) . _playwrightRunCallback = f ) ;
} ) ;
}
async evaluate < R , Arg > ( pageFunction : types.FuncOn < any , Arg , R > , arg : Arg ) : Promise < R > ;
async evaluate < R > ( pageFunction : types.FuncOn < any , void , R > , arg? : any ) : Promise < R > ;
async evaluate < R , Arg > ( pageFunction : types.FuncOn < any , Arg , R > , arg : Arg ) : Promise < R > {
return this . _nodeElectronHandle ! . evaluate ( pageFunction , arg ) ;
}
async evaluateHandle < R , Arg > ( pageFunction : types.FuncOn < any , Arg , R > , arg : Arg ) : Promise < types.SmartHandle < R > > ;
async evaluateHandle < R > ( pageFunction : types.FuncOn < any , void , R > , arg? : any ) : Promise < types.SmartHandle < R > > ;
async evaluateHandle < R , Arg > ( pageFunction : types.FuncOn < any , Arg , R > , arg : Arg ) : Promise < types.SmartHandle < R > > {
return this . _nodeElectronHandle ! . evaluateHandle ( pageFunction , arg ) ;
}
protected _computeDeadline ( options? : types.TimeoutOptions ) : number {
return this . _timeoutSettings . computeDeadline ( options ) ;
}
}
export class Electron {
async launch ( executablePath : string , options : ElectronLaunchOptions = { } ) : Promise < ElectronApplication > {
const {
args = [ ] ,
env = process . env ,
handleSIGINT = true ,
handleSIGTERM = true ,
handleSIGHUP = true ,
} = options ;
2020-05-20 00:10:10 -07:00
const browserServer = new BrowserServer ( options ) ;
2020-05-11 18:00:33 -07:00
let app : ElectronApplication | undefined = undefined ;
const logger = new RootLogger ( options . logger ) ;
const electronArguments = [ '--inspect=0' , '--remote-debugging-port=0' , '--require' , path . join ( __dirname , 'electronLoader.js' ) , . . . args ] ;
const { launchedProcess , gracefullyClose } = await launchProcess ( {
executablePath ,
args : electronArguments ,
env ,
handleSIGINT ,
handleSIGTERM ,
handleSIGHUP ,
logger ,
pipe : true ,
cwd : options.cwd ,
omitDownloads : true ,
attemptToGracefullyClose : ( ) = > app ! . close ( ) ,
onkill : ( exitCode , signal ) = > {
if ( app )
app . emit ( ElectronEvents . ElectronApplication . Close , exitCode , signal ) ;
} ,
} ) ;
2020-05-20 00:10:10 -07:00
const deadline = browserServer . _launchDeadline ;
2020-05-11 18:00:33 -07:00
const timeoutError = new TimeoutError ( ` Timed out while trying to connect to Electron! ` ) ;
const nodeMatch = await waitForLine ( launchedProcess , launchedProcess . stderr , /^Debugger listening on (ws:\/\/.*)$/ , helper . timeUntilDeadline ( deadline ) , timeoutError ) ;
const nodeConnection = await WebSocketTransport . connect ( nodeMatch [ 1 ] , transport = > {
return new CRConnection ( transport , logger ) ;
} ) ;
const chromeMatch = await waitForLine ( launchedProcess , launchedProcess . stderr , /^DevTools listening on (ws:\/\/.*)$/ , helper . timeUntilDeadline ( deadline ) , timeoutError ) ;
const chromeTransport = await WebSocketTransport . connect ( chromeMatch [ 1 ] , transport = > {
return transport ;
2020-05-18 19:00:38 -07:00
} , logger ) ;
2020-05-20 00:10:10 -07:00
browserServer . _initialize ( launchedProcess , gracefullyClose , null ) ;
2020-05-14 13:22:33 -07:00
const browser = await CRBrowser . connect ( chromeTransport , { headful : true , logger , persistent : true , viewport : null , ownedServer : browserServer , downloadsPath : '' } ) ;
2020-05-11 18:00:33 -07:00
app = new ElectronApplication ( logger , browser , nodeConnection ) ;
await app . _init ( ) ;
return app ;
}
}