14
14
* limitations under the License.
15
15
*/
16
16
17
+ import * as fs from 'fs' ;
18
+ import * as os from 'os' ;
19
+ import * as path from 'path' ;
20
+ import * as util from 'util' ;
17
21
import { BrowserContext , PersistentContextOptions , validatePersistentContextOptions } from '../browserContext' ;
18
- import { BrowserServer } from './browserServer' ;
22
+ import { BrowserServer , WebSocketWrapper } from './browserServer' ;
19
23
import * as browserPaths from '../install/browserPaths' ;
20
- import { Logger , RootLogger } from '../logger' ;
24
+ import { Logger , RootLogger , InnerLogger } from '../logger' ;
21
25
import { ConnectionTransport , WebSocketTransport } from '../transport' ;
22
26
import { BrowserBase , BrowserOptions , Browser } from '../browser' ;
23
27
import { assert , helper } from '../helper' ;
24
28
import { TimeoutSettings } from '../timeoutSettings' ;
29
+ import { launchProcess , Env , waitForLine } from './processLauncher' ;
30
+ import { Events } from '../events' ;
31
+ import { TimeoutError } from '../errors' ;
32
+ import { PipeTransport } from './pipeTransport' ;
25
33
26
34
export type BrowserArgOptions = {
27
35
headless ?: boolean ,
@@ -37,23 +45,23 @@ type LaunchOptionsBase = BrowserArgOptions & {
37
45
handleSIGHUP ?: boolean ,
38
46
timeout ?: number ,
39
47
logger ?: Logger ,
40
- env ?: { [ key : string ] : string | number | boolean }
48
+ env ?: Env ,
41
49
} ;
42
50
43
51
export function processBrowserArgOptions ( options : LaunchOptionsBase ) : { devtools : boolean , headless : boolean } {
44
52
const { devtools = false , headless = ! devtools } = options ;
45
53
return { devtools, headless } ;
46
54
}
47
55
48
- export type ConnectOptions = {
56
+ type ConnectOptions = {
49
57
wsEndpoint : string ,
50
58
slowMo ?: number ,
51
59
logger ?: Logger ,
52
60
timeout ?: number ,
53
61
} ;
54
62
export type LaunchType = 'local' | 'server' | 'persistent' ;
55
63
export type LaunchOptions = LaunchOptionsBase & { slowMo ?: number } ;
56
- export type LaunchServerOptions = LaunchOptionsBase & { port ?: number } ;
64
+ type LaunchServerOptions = LaunchOptionsBase & { port ?: number } ;
57
65
58
66
export interface BrowserType {
59
67
executablePath ( ) : string ;
@@ -64,16 +72,20 @@ export interface BrowserType {
64
72
connect ( options : ConnectOptions ) : Promise < Browser > ;
65
73
}
66
74
75
+ const mkdtempAsync = util . promisify ( fs . mkdtemp ) ;
76
+
67
77
export abstract class BrowserTypeBase implements BrowserType {
68
78
private _name : string ;
69
79
private _executablePath : string | undefined ;
80
+ private _webSocketRegexNotPipe : RegExp | null ;
70
81
readonly _browserPath : string ;
71
82
72
- constructor ( packagePath : string , browser : browserPaths . BrowserDescriptor ) {
83
+ constructor ( packagePath : string , browser : browserPaths . BrowserDescriptor , webSocketRegexNotPipe : RegExp | null ) {
73
84
this . _name = browser . name ;
74
85
const browsersPath = browserPaths . browsersPath ( packagePath ) ;
75
86
this . _browserPath = browserPaths . browserDirectory ( browsersPath , browser ) ;
76
87
this . _executablePath = browserPaths . executablePath ( this . _browserPath , browser ) ;
88
+ this . _webSocketRegexNotPipe = webSocketRegexNotPipe ;
77
89
}
78
90
79
91
executablePath ( ) : string {
@@ -183,6 +195,88 @@ export abstract class BrowserTypeBase implements BrowserType {
183
195
return this . _connectToTransport ( transport , { slowMo : options . slowMo , logger } ) ;
184
196
}
185
197
186
- abstract _launchServer ( options : LaunchServerOptions , launchType : LaunchType , logger : RootLogger , deadline : number , userDataDir ?: string ) : Promise < BrowserServer > ;
198
+ private async _launchServer ( options : LaunchServerOptions , launchType : LaunchType , logger : RootLogger , deadline : number , userDataDir ?: string ) : Promise < BrowserServer > {
199
+ const {
200
+ ignoreDefaultArgs = false ,
201
+ args = [ ] ,
202
+ executablePath = null ,
203
+ env = process . env ,
204
+ handleSIGINT = true ,
205
+ handleSIGTERM = true ,
206
+ handleSIGHUP = true ,
207
+ port = 0 ,
208
+ } = options ;
209
+ assert ( ! port || launchType === 'server' , 'Cannot specify a port without launching as a server.' ) ;
210
+
211
+ let temporaryUserDataDir : string | null = null ;
212
+ if ( ! userDataDir ) {
213
+ userDataDir = await mkdtempAsync ( path . join ( os . tmpdir ( ) , `playwright_${ this . _name } dev_profile-` ) ) ;
214
+ temporaryUserDataDir = userDataDir ;
215
+ }
216
+
217
+ const browserArguments = [ ] ;
218
+ if ( ! ignoreDefaultArgs )
219
+ browserArguments . push ( ...this . _defaultArgs ( options , launchType , userDataDir ) ) ;
220
+ else if ( Array . isArray ( ignoreDefaultArgs ) )
221
+ browserArguments . push ( ...this . _defaultArgs ( options , launchType , userDataDir ) . filter ( arg => ignoreDefaultArgs . indexOf ( arg ) === - 1 ) ) ;
222
+ else
223
+ browserArguments . push ( ...args ) ;
224
+
225
+ const executable = executablePath || this . executablePath ( ) ;
226
+ if ( ! executable )
227
+ throw new Error ( `No executable path is specified. Pass "executablePath" option directly.` ) ;
228
+
229
+ // Note: it is important to define these variables before launchProcess, so that we don't get
230
+ // "Cannot access 'browserServer' before initialization" if something went wrong.
231
+ let transport : ConnectionTransport | undefined = undefined ;
232
+ let browserServer : BrowserServer | undefined = undefined ;
233
+ const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess ( {
234
+ executablePath : executable ,
235
+ args : browserArguments ,
236
+ env : this . _amendEnvironment ( env , userDataDir , executable , browserArguments ) ,
237
+ handleSIGINT,
238
+ handleSIGTERM,
239
+ handleSIGHUP,
240
+ logger,
241
+ pipe : ! this . _webSocketRegexNotPipe ,
242
+ tempDir : temporaryUserDataDir || undefined ,
243
+ attemptToGracefullyClose : async ( ) => {
244
+ if ( ( options as any ) . __testHookGracefullyClose )
245
+ await ( options as any ) . __testHookGracefullyClose ( ) ;
246
+ // We try to gracefully close to prevent crash reporting and core dumps.
247
+ // Note that it's fine to reuse the pipe transport, since
248
+ // our connection ignores kBrowserCloseMessageId.
249
+ this . _attemptToGracefullyCloseBrowser ( transport ! ) ;
250
+ } ,
251
+ onkill : ( exitCode , signal ) => {
252
+ if ( browserServer )
253
+ browserServer . emit ( Events . BrowserServer . Close , exitCode , signal ) ;
254
+ } ,
255
+ } ) ;
256
+
257
+ try {
258
+ if ( this . _webSocketRegexNotPipe ) {
259
+ const timeoutError = new TimeoutError ( `Timed out while trying to connect to the browser!` ) ;
260
+ const match = await waitForLine ( launchedProcess , launchedProcess . stdout , this . _webSocketRegexNotPipe , helper . timeUntilDeadline ( deadline ) , timeoutError ) ;
261
+ const innerEndpoint = match [ 1 ] ;
262
+ transport = await WebSocketTransport . connect ( innerEndpoint , logger , deadline ) ;
263
+ } else {
264
+ const stdio = launchedProcess . stdio as unknown as [ NodeJS . ReadableStream , NodeJS . WritableStream , NodeJS . WritableStream , NodeJS . WritableStream , NodeJS . ReadableStream ] ;
265
+ transport = new PipeTransport ( stdio [ 3 ] , stdio [ 4 ] , logger ) ;
266
+ }
267
+ } catch ( e ) {
268
+ // If we can't establish a connection, kill the process and exit.
269
+ helper . killProcess ( launchedProcess ) ;
270
+ throw e ;
271
+ }
272
+ browserServer = new BrowserServer ( options , launchedProcess , gracefullyClose , transport , downloadsPath ,
273
+ launchType === 'server' ? this . _wrapTransportWithWebSocket ( transport , logger , port ) : null ) ;
274
+ return browserServer ;
275
+ }
276
+
277
+ abstract _defaultArgs ( options : BrowserArgOptions , launchType : LaunchType , userDataDir : string ) : string [ ] ;
187
278
abstract _connectToTransport ( transport : ConnectionTransport , options : BrowserOptions ) : Promise < BrowserBase > ;
188
- }
279
+ abstract _wrapTransportWithWebSocket ( transport : ConnectionTransport , logger : InnerLogger , port : number ) : WebSocketWrapper ;
280
+ abstract _amendEnvironment ( env : Env , userDataDir : string , executable : string , browserArguments : string [ ] ) : Env ;
281
+ abstract _attemptToGracefullyCloseBrowser ( transport : ConnectionTransport ) : void ;
282
+ }
0 commit comments