2020-01-08 01:15:07 +01: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-23 02:42:10 +01:00
import { WKBrowser } from '../webkit/wkBrowser' ;
2020-01-08 01:15:07 +01:00
import { PipeTransport } from './pipeTransport' ;
2020-02-26 22:02:15 +01:00
import { launchProcess } from './processLauncher' ;
2020-01-17 07:11:14 +01:00
import * as fs from 'fs' ;
2020-01-08 01:15:07 +01:00
import * as path from 'path' ;
import * as os from 'os' ;
2020-04-01 23:42:47 +02:00
import * as util from 'util' ;
2020-04-20 16:52:26 +02:00
import { helper , assert } from '../helper' ;
2020-01-08 22:55:38 +01:00
import { kBrowserCloseMessageId } from '../webkit/wkConnection' ;
2020-05-14 22:22:33 +02:00
import { LaunchOptions , BrowserArgOptions , LaunchServerOptions , ConnectOptions , AbstractBrowserType , processBrowserArgOptions } from './browserType' ;
2020-04-01 23:42:47 +02:00
import { ConnectionTransport , SequenceNumberMixer , WebSocketTransport } from '../transport' ;
2020-01-23 02:42:10 +01:00
import * as ws from 'ws' ;
2020-04-01 01:34:59 +02:00
import { LaunchType } from '../browser' ;
2020-03-30 22:49:52 +02:00
import { BrowserServer , WebSocketWrapper } from './browserServer' ;
2020-01-25 00:58:04 +01:00
import { Events } from '../events' ;
2020-02-05 21:41:55 +01:00
import { BrowserContext } from '../browserContext' ;
2020-04-21 08:24:53 +02:00
import { InnerLogger , logError , RootLogger } from '../logger' ;
2020-04-29 02:06:01 +02:00
import { BrowserDescriptor } from '../install/browserPaths' ;
2020-01-08 01:15:07 +01:00
2020-04-29 02:06:01 +02:00
export class WebKit extends AbstractBrowserType < WKBrowser > {
constructor ( packagePath : string , browser : BrowserDescriptor ) {
super ( packagePath , browser ) ;
2020-01-29 03:09:07 +01:00
}
2020-04-01 01:34:59 +02:00
async launch ( options : LaunchOptions = { } ) : Promise < WKBrowser > {
assert ( ! ( options as any ) . userDataDir , 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead' ) ;
2020-05-20 09:10:10 +02:00
const browserServer = new BrowserServer ( options ) ;
const { transport , downloadsPath } = await this . _launchServer ( options , 'local' , browserServer ) ;
return await browserServer . _initializeOrClose ( async ( ) = > {
2020-05-14 22:22:33 +02:00
return await WKBrowser . connect ( transport ! , {
slowMo : options.slowMo ,
headful : ! processBrowserArgOptions ( options ) . headless ,
2020-05-20 09:10:10 +02:00
logger : browserServer._logger ,
2020-05-14 22:22:33 +02:00
downloadsPath ,
ownedServer : browserServer
} ) ;
2020-05-12 00:00:13 +02:00
} ) ;
2020-01-08 01:15:07 +01:00
}
2020-04-01 01:34:59 +02:00
async launchServer ( options : LaunchServerOptions = { } ) : Promise < BrowserServer > {
2020-05-20 09:10:10 +02:00
const browserServer = new BrowserServer ( options ) ;
await this . _launchServer ( options , 'server' , browserServer ) ;
return browserServer ;
2020-02-05 04:41:38 +01:00
}
2020-04-01 01:34:59 +02:00
async launchPersistentContext ( userDataDir : string , options : LaunchOptions = { } ) : Promise < BrowserContext > {
2020-05-20 09:10:10 +02:00
const browserServer = new BrowserServer ( options ) ;
const { transport , downloadsPath } = await this . _launchServer ( options , 'persistent' , browserServer , userDataDir ) ;
return await browserServer . _initializeOrClose ( async ( ) = > {
2020-05-14 22:22:33 +02:00
const browser = await WKBrowser . connect ( transport ! , {
slowMo : options.slowMo ,
headful : ! processBrowserArgOptions ( options ) . headless ,
2020-05-20 09:10:10 +02:00
logger : browserServer._logger ,
2020-05-14 22:22:33 +02:00
persistent : true ,
downloadsPath ,
ownedServer : browserServer
} ) ;
2020-05-12 00:00:13 +02:00
const context = browser . _defaultContext ! ;
if ( ! options . ignoreDefaultArgs || Array . isArray ( options . ignoreDefaultArgs ) )
await context . _loadDefaultContext ( ) ;
return context ;
} ) ;
2020-02-05 21:41:55 +01:00
}
2020-05-20 09:10:10 +02:00
private async _launchServer ( options : LaunchServerOptions , launchType : LaunchType , browserServer : BrowserServer , userDataDir? : string ) : Promise < { transport? : ConnectionTransport , downloadsPath : string , logger : RootLogger } > {
2020-01-08 01:15:07 +01:00
const {
ignoreDefaultArgs = false ,
args = [ ] ,
executablePath = null ,
env = process . env ,
handleSIGINT = true ,
handleSIGTERM = true ,
handleSIGHUP = true ,
2020-04-01 01:34:59 +02:00
port = 0 ,
2020-01-08 01:15:07 +01:00
} = options ;
2020-04-01 01:34:59 +02:00
assert ( ! port || launchType === 'server' , 'Cannot specify a port without launching as a server.' ) ;
2020-05-20 09:10:10 +02:00
const logger = browserServer . _logger ;
2020-01-08 01:15:07 +01:00
2020-02-05 21:41:55 +01:00
let temporaryUserDataDir : string | null = null ;
if ( ! userDataDir ) {
2020-01-17 07:11:14 +01:00
userDataDir = await mkdtempAsync ( WEBKIT_PROFILE_PATH ) ;
2020-04-01 23:42:47 +02:00
temporaryUserDataDir = userDataDir ;
2020-01-17 07:11:14 +01:00
}
2020-02-06 01:36:36 +01:00
const webkitArguments = [ ] ;
if ( ! ignoreDefaultArgs )
2020-04-01 23:42:47 +02:00
webkitArguments . push ( . . . this . _defaultArgs ( options , launchType , userDataDir , port ) ) ;
2020-02-06 01:36:36 +01:00
else if ( Array . isArray ( ignoreDefaultArgs ) )
2020-04-01 23:42:47 +02:00
webkitArguments . push ( . . . this . _defaultArgs ( options , launchType , userDataDir , port ) . filter ( arg = > ignoreDefaultArgs . indexOf ( arg ) === - 1 ) ) ;
2020-02-06 01:36:36 +01:00
else
webkitArguments . push ( . . . args ) ;
2020-01-17 07:11:14 +01:00
2020-04-29 02:06:01 +02:00
const webkitExecutable = executablePath || this . executablePath ( ) ;
2020-03-19 19:43:35 +01:00
if ( ! webkitExecutable )
throw new Error ( ` No executable path is specified. ` ) ;
2020-01-08 22:55:38 +01:00
2020-05-14 22:22:33 +02: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 ;
2020-04-03 02:56:14 +02:00
const { launchedProcess , gracefullyClose , downloadsPath } = await launchProcess ( {
2020-03-19 19:43:35 +01:00
executablePath : webkitExecutable ,
2020-01-08 01:15:07 +01:00
args : webkitArguments ,
2020-04-01 23:42:47 +02:00
env : { . . . env , CURL_COOKIE_JAR_PATH : path.join ( userDataDir , 'cookiejar.db' ) } ,
2020-01-08 01:15:07 +01:00
handleSIGINT ,
handleSIGTERM ,
handleSIGHUP ,
2020-04-20 16:52:26 +02:00
logger ,
2020-01-08 01:15:07 +01:00
pipe : true ,
2020-01-17 07:11:14 +01:00
tempDir : temporaryUserDataDir || undefined ,
2020-01-08 22:55:38 +01:00
attemptToGracefullyClose : async ( ) = > {
2020-04-03 01:57:12 +02:00
assert ( transport ) ;
2020-01-08 22:55:38 +01:00
// We try to gracefully close to prevent crash reporting and core dumps.
2020-01-24 02:45:31 +01:00
// Note that it's fine to reuse the pipe transport, since
// our connection ignores kBrowserCloseMessageId.
2020-05-20 09:10:10 +02:00
transport . send ( { method : 'Playwright.close' , params : { } , id : kBrowserCloseMessageId } ) ;
2020-01-08 22:55:38 +01:00
} ,
2020-01-28 22:07:53 +01:00
onkill : ( exitCode , signal ) = > {
2020-05-20 09:10:10 +02:00
browserServer . emit ( Events . BrowserServer . Close , exitCode , signal ) ;
2020-01-25 00:58:04 +01:00
} ,
2020-01-08 01:15:07 +01:00
} ) ;
2020-04-07 16:40:57 +02:00
const stdio = launchedProcess . stdio as unknown as [ NodeJS . ReadableStream , NodeJS . WritableStream , NodeJS . WritableStream , NodeJS . WritableStream , NodeJS . ReadableStream ] ;
2020-04-20 16:52:26 +02:00
transport = new PipeTransport ( stdio [ 3 ] , stdio [ 4 ] , logger ) ;
2020-05-20 09:10:10 +02:00
browserServer . _initialize ( launchedProcess , gracefullyClose , launchType === 'server' ? wrapTransportWithWebSocket ( transport , logger , port || 0 ) : null ) ;
return { transport , downloadsPath , logger } ;
2020-01-08 01:15:07 +01:00
}
2020-02-05 04:41:38 +01:00
async connect ( options : ConnectOptions ) : Promise < WKBrowser > {
2020-05-19 04:00:38 +02:00
const logger = new RootLogger ( options . logger ) ;
2020-05-11 22:49:57 +02:00
return await WebSocketTransport . connect ( options . wsEndpoint , async transport = > {
if ( ( options as any ) . __testHookBeforeCreateBrowser )
await ( options as any ) . __testHookBeforeCreateBrowser ( ) ;
2020-05-19 04:00:38 +02:00
return WKBrowser . connect ( transport , { slowMo : options.slowMo , logger , downloadsPath : '' } ) ;
} , logger ) ;
2020-01-23 02:42:10 +01:00
}
2020-02-26 22:02:15 +01:00
_defaultArgs ( options : BrowserArgOptions = { } , launchType : LaunchType , userDataDir : string , port : number ) : string [ ] {
2020-05-14 22:22:33 +02:00
const { devtools , headless } = processBrowserArgOptions ( options ) ;
const { args = [ ] } = options ;
2020-01-24 23:49:47 +01:00
if ( devtools )
2020-03-24 22:42:20 +01:00
console . warn ( 'devtools parameter as a launch argument in WebKit is not supported. Also starting Web Inspector manually will terminate the execution in WebKit.' ) ;
2020-02-06 01:36:36 +01: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-05-11 00:23:53 +02:00
if ( args . find ( arg = > ! arg . startsWith ( '-' ) ) )
2020-02-27 23:09:24 +01:00
throw new Error ( 'Arguments can not specify page to be opened' ) ;
2020-01-23 21:18:41 +01:00
const webkitArguments = [ '--inspector-pipe' ] ;
if ( headless )
webkitArguments . push ( '--headless' ) ;
2020-02-26 22:02:15 +01:00
if ( launchType === 'persistent' )
webkitArguments . push ( ` --user-data-dir= ${ userDataDir } ` ) ;
else
webkitArguments . push ( ` --no-startup-window ` ) ;
2020-01-08 01:15:07 +01:00
webkitArguments . push ( . . . args ) ;
2020-05-11 00:23:53 +02:00
if ( launchType === 'persistent' )
webkitArguments . push ( 'about:blank' ) ;
2020-01-08 01:15:07 +01:00
return webkitArguments ;
}
}
2020-04-01 23:42:47 +02:00
const mkdtempAsync = util . promisify ( fs . mkdtemp ) ;
2020-01-17 07:11:14 +01:00
const WEBKIT_PROFILE_PATH = path . join ( os . tmpdir ( ) , 'playwright_dev_profile-' ) ;
2020-01-08 01:15:07 +01:00
2020-04-21 08:24:53 +02:00
function wrapTransportWithWebSocket ( transport : ConnectionTransport , logger : InnerLogger , port : number ) : WebSocketWrapper {
2020-02-05 21:41:55 +01:00
const server = new ws . Server ( { port } ) ;
2020-04-01 23:42:47 +02:00
const guid = helper . guid ( ) ;
2020-02-06 21:41:43 +01:00
const idMixer = new SequenceNumberMixer < { id : number , socket : ws } > ( ) ;
const pendingBrowserContextCreations = new Set < number > ( ) ;
const pendingBrowserContextDeletions = new Map < number , string > ( ) ;
const browserContextIds = new Map < string , ws > ( ) ;
const pageProxyIds = new Map < string , ws > ( ) ;
const sockets = new Set < ws > ( ) ;
2020-01-23 02:42:10 +01:00
2020-02-06 21:41:43 +01:00
transport . onmessage = message = > {
2020-03-27 07:30:55 +01:00
if ( typeof message . id === 'number' ) {
if ( message . id === - 9999 )
2020-02-06 21:41:43 +01:00
return ;
// Process command response.
2020-03-27 07:30:55 +01:00
const value = idMixer . take ( message . id ) ;
2020-02-06 21:41:43 +01:00
if ( ! value )
return ;
const { id , socket } = value ;
2020-03-27 23:18:34 +01:00
if ( socket . readyState === ws . CLOSING ) {
2020-02-06 21:41:43 +01:00
if ( pendingBrowserContextCreations . has ( id ) ) {
2020-03-27 07:30:55 +01:00
transport . send ( {
2020-02-06 21:41:43 +01:00
id : ++ SequenceNumberMixer . _lastSequenceNumber ,
2020-03-11 22:08:22 +01:00
method : 'Playwright.deleteContext' ,
2020-03-27 07:30:55 +01:00
params : { browserContextId : message.result.browserContextId }
} ) ;
2020-02-06 21:41:43 +01:00
}
return ;
}
2020-03-27 07:30:55 +01:00
if ( pendingBrowserContextCreations . has ( message . id ) ) {
2020-02-06 21:41:43 +01:00
// Browser.createContext response -> establish context attribution.
2020-03-27 07:30:55 +01:00
browserContextIds . set ( message . result . browserContextId , socket ) ;
pendingBrowserContextCreations . delete ( message . id ) ;
2020-02-06 21:41:43 +01:00
}
2020-03-27 07:30:55 +01:00
const deletedContextId = pendingBrowserContextDeletions . get ( message . id ) ;
2020-02-06 21:41:43 +01:00
if ( deletedContextId ) {
// Browser.deleteContext response -> remove context attribution.
browserContextIds . delete ( deletedContextId ) ;
2020-03-27 07:30:55 +01:00
pendingBrowserContextDeletions . delete ( message . id ) ;
2020-02-06 21:41:43 +01:00
}
2020-03-27 07:30:55 +01:00
message . id = id ;
socket . send ( JSON . stringify ( message ) ) ;
2020-01-23 02:42:10 +01:00
return ;
}
2020-02-06 21:41:43 +01:00
// Process notification response.
2020-03-27 07:30:55 +01:00
const { method , params , pageProxyId } = message ;
2020-02-06 21:41:43 +01:00
if ( pageProxyId ) {
const socket = pageProxyIds . get ( pageProxyId ) ;
if ( ! socket || socket . readyState === ws . CLOSING ) {
// Drop unattributed messages on the floor.
return ;
}
2020-03-27 07:30:55 +01:00
socket . send ( JSON . stringify ( message ) ) ;
2020-01-23 02:42:10 +01:00
return ;
}
2020-03-11 22:08:22 +01:00
if ( method === 'Playwright.pageProxyCreated' ) {
2020-02-06 21:41:43 +01:00
const socket = browserContextIds . get ( params . pageProxyInfo . browserContextId ) ;
if ( ! socket || socket . readyState === ws . CLOSING ) {
// Drop unattributed messages on the floor.
return ;
}
pageProxyIds . set ( params . pageProxyInfo . pageProxyId , socket ) ;
2020-03-27 07:30:55 +01:00
socket . send ( JSON . stringify ( message ) ) ;
2020-02-06 21:41:43 +01:00
return ;
}
2020-03-11 22:08:22 +01:00
if ( method === 'Playwright.pageProxyDestroyed' ) {
2020-02-06 21:41:43 +01:00
const socket = pageProxyIds . get ( params . pageProxyId ) ;
pageProxyIds . delete ( params . pageProxyId ) ;
if ( socket && socket . readyState !== ws . CLOSING )
2020-03-27 07:30:55 +01:00
socket . send ( JSON . stringify ( message ) ) ;
2020-02-06 21:41:43 +01:00
return ;
}
2020-03-11 22:08:22 +01:00
if ( method === 'Playwright.provisionalLoadFailed' ) {
2020-02-06 21:41:43 +01:00
const socket = pageProxyIds . get ( params . pageProxyId ) ;
if ( socket && socket . readyState !== ws . CLOSING )
2020-03-27 07:30:55 +01:00
socket . send ( JSON . stringify ( message ) ) ;
2020-02-06 21:41:43 +01:00
return ;
}
} ;
2020-03-28 18:14:59 +01:00
transport . onclose = ( ) = > {
for ( const socket of sockets ) {
socket . removeListener ( 'close' , ( socket as any ) . __closeListener ) ;
socket . close ( undefined , 'Browser disconnected' ) ;
}
server . close ( ) ;
transport . onmessage = undefined ;
transport . onclose = undefined ;
} ;
2020-02-06 21:41:43 +01:00
server . on ( 'connection' , ( socket : ws , req ) = > {
if ( req . url !== '/' + guid ) {
socket . close ( ) ;
return ;
}
sockets . add ( socket ) ;
socket . on ( 'message' , ( message : string ) = > {
const parsedMessage = JSON . parse ( Buffer . from ( message ) . toString ( ) ) ;
const { id , method , params } = parsedMessage ;
const seqNum = idMixer . generate ( { id , socket } ) ;
2020-03-27 07:30:55 +01:00
transport . send ( { . . . parsedMessage , id : seqNum } ) ;
2020-03-11 22:08:22 +01:00
if ( method === 'Playwright.createContext' )
2020-02-06 21:41:43 +01:00
pendingBrowserContextCreations . add ( seqNum ) ;
2020-03-11 22:08:22 +01:00
if ( method === 'Playwright.deleteContext' )
2020-02-06 21:41:43 +01:00
pendingBrowserContextDeletions . set ( seqNum , params . browserContextId ) ;
} ) ;
2020-04-20 16:52:26 +02:00
socket . on ( 'error' , logError ( logger ) ) ;
2020-03-31 03:18:38 +02:00
2020-03-10 00:53:33 +01:00
socket . on ( 'close' , ( socket as any ) . __closeListener = ( ) = > {
2020-02-06 21:41:43 +01:00
for ( const [ pageProxyId , s ] of pageProxyIds ) {
if ( s === socket )
pageProxyIds . delete ( pageProxyId ) ;
}
for ( const [ browserContextId , s ] of browserContextIds ) {
if ( s === socket ) {
2020-03-27 07:30:55 +01:00
transport . send ( {
2020-02-06 21:41:43 +01:00
id : ++ SequenceNumberMixer . _lastSequenceNumber ,
2020-03-11 22:08:22 +01:00
method : 'Playwright.deleteContext' ,
2020-02-06 21:41:43 +01:00
params : { browserContextId }
2020-03-27 07:30:55 +01:00
} ) ;
2020-02-06 21:41:43 +01:00
browserContextIds . delete ( browserContextId ) ;
}
}
sockets . delete ( socket ) ;
2020-01-23 02:42:10 +01:00
} ) ;
} ) ;
const address = server . address ( ) ;
2020-03-30 22:49:52 +02:00
const wsEndpoint = typeof address === 'string' ? ` ${ address } / ${ guid } ` : ` ws://127.0.0.1: ${ address . port } / ${ guid } ` ;
return new WebSocketWrapper ( wsEndpoint ,
[ pendingBrowserContextCreations , pendingBrowserContextDeletions , browserContextIds , pageProxyIds , sockets ] ) ;
2020-01-23 02:42:10 +01:00
}