mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	fix(firefox): launch races (#15259)
Potential fixes to avoid startup races: - Wait for "juggler listening" message. - Make sure `transport.onclose` is called when connecting to the transport after the actual pipe closure.
This commit is contained in:
		
							parent
							
								
									f0b3b280a5
								
							
						
					
					
						commit
						0254cd3be7
					
				| @ -38,6 +38,7 @@ import { helper } from './helper'; | |||||||
| import { RecentLogsCollector } from '../common/debugLogger'; | import { RecentLogsCollector } from '../common/debugLogger'; | ||||||
| import type { CallMetadata } from './instrumentation'; | import type { CallMetadata } from './instrumentation'; | ||||||
| import { SdkObject } from './instrumentation'; | import { SdkObject } from './instrumentation'; | ||||||
|  | import { ManualPromise } from '../utils/manualPromise'; | ||||||
| 
 | 
 | ||||||
| export const kNoXServerRunningError = 'Looks like you launched a headed browser without having a XServer running.\n' + | export const kNoXServerRunningError = 'Looks like you launched a headed browser without having a XServer running.\n' + | ||||||
|   'Set either \'headless: true\' or use \'xvfb-run <your-playwright-app>\' before running Playwright.\n\n<3 Playwright Team'; |   'Set either \'headless: true\' or use \'xvfb-run <your-playwright-app>\' before running Playwright.\n\n<3 Playwright Team'; | ||||||
| @ -186,9 +187,8 @@ export abstract class BrowserType extends SdkObject { | |||||||
|       await registryExecutable.validateHostRequirements(this._playwrightOptions.sdkLanguage); |       await registryExecutable.validateHostRequirements(this._playwrightOptions.sdkLanguage); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let wsEndpointCallback: ((wsEndpoint: string) => void) | undefined; |     const waitForWSEndpoint = (options.useWebSocket || options.args?.some(a => a.startsWith('--remote-debugging-port'))) ? new ManualPromise<string>() : undefined; | ||||||
|     const shouldWaitForWSListening = options.useWebSocket || options.args?.some(a => a.startsWith('--remote-debugging-port')); |     const waitForJuggler = this._name === 'firefox' ? new ManualPromise<void>() : undefined; | ||||||
|     const waitForWSEndpoint = shouldWaitForWSListening ? new Promise<string>(f => wsEndpointCallback = f) : undefined; |  | ||||||
|     // Note: it is important to define these variables before launchProcess, so that we don't get
 |     // 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.
 |     // "Cannot access 'browserServer' before initialization" if something went wrong.
 | ||||||
|     let transport: ConnectionTransport | undefined = undefined; |     let transport: ConnectionTransport | undefined = undefined; | ||||||
| @ -201,11 +201,13 @@ export abstract class BrowserType extends SdkObject { | |||||||
|       handleSIGTERM, |       handleSIGTERM, | ||||||
|       handleSIGHUP, |       handleSIGHUP, | ||||||
|       log: (message: string) => { |       log: (message: string) => { | ||||||
|         if (wsEndpointCallback) { |         if (waitForWSEndpoint) { | ||||||
|           const match = message.match(/DevTools listening on (.*)/); |           const match = message.match(/DevTools listening on (.*)/); | ||||||
|           if (match) |           if (match) | ||||||
|             wsEndpointCallback(match[1]); |             waitForWSEndpoint.resolve(match[1]); | ||||||
|         } |         } | ||||||
|  |         if (waitForJuggler && message.includes('Juggler listening to the pipe')) | ||||||
|  |           waitForJuggler.resolve(); | ||||||
|         progress.log(message); |         progress.log(message); | ||||||
|         browserLogsCollector.log(message); |         browserLogsCollector.log(message); | ||||||
|       }, |       }, | ||||||
| @ -220,6 +222,8 @@ export abstract class BrowserType extends SdkObject { | |||||||
|         this._attemptToGracefullyCloseBrowser(transport!); |         this._attemptToGracefullyCloseBrowser(transport!); | ||||||
|       }, |       }, | ||||||
|       onExit: (exitCode, signal) => { |       onExit: (exitCode, signal) => { | ||||||
|  |         // Unblock launch when browser prematurely exits.
 | ||||||
|  |         waitForJuggler?.resolve(); | ||||||
|         if (browserProcess && browserProcess.onclose) |         if (browserProcess && browserProcess.onclose) | ||||||
|           browserProcess.onclose(exitCode, signal); |           browserProcess.onclose(exitCode, signal); | ||||||
|       }, |       }, | ||||||
| @ -244,9 +248,8 @@ export abstract class BrowserType extends SdkObject { | |||||||
|       kill |       kill | ||||||
|     }; |     }; | ||||||
|     progress.cleanupWhenAborted(() => closeOrKill(progress.timeUntilDeadline())); |     progress.cleanupWhenAborted(() => closeOrKill(progress.timeUntilDeadline())); | ||||||
|     let wsEndpoint: string | undefined; |     const wsEndpoint = await waitForWSEndpoint; | ||||||
|     if (shouldWaitForWSListening) |     await waitForJuggler; | ||||||
|       wsEndpoint = await waitForWSEndpoint; |  | ||||||
|     if (options.useWebSocket) { |     if (options.useWebSocket) { | ||||||
|       transport = await WebSocketTransport.connect(progress, wsEndpoint!); |       transport = await WebSocketTransport.connect(progress, wsEndpoint!); | ||||||
|     } else { |     } else { | ||||||
|  | |||||||
| @ -49,10 +49,11 @@ export class CRConnection extends EventEmitter { | |||||||
|     this._transport = transport; |     this._transport = transport; | ||||||
|     this._protocolLogger = protocolLogger; |     this._protocolLogger = protocolLogger; | ||||||
|     this._browserLogsCollector = browserLogsCollector; |     this._browserLogsCollector = browserLogsCollector; | ||||||
|     this._transport.onmessage = this._onMessage.bind(this); |  | ||||||
|     this._transport.onclose = this._onClose.bind(this); |  | ||||||
|     this.rootSession = new CRSession(this, '', 'browser', ''); |     this.rootSession = new CRSession(this, '', 'browser', ''); | ||||||
|     this._sessions.set('', this.rootSession); |     this._sessions.set('', this.rootSession); | ||||||
|  |     this._transport.onmessage = this._onMessage.bind(this); | ||||||
|  |     // onclose should be set last, since it can be immediately called.
 | ||||||
|  |     this._transport.onclose = this._onClose.bind(this); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static fromSession(session: CRSession): CRConnection { |   static fromSession(session: CRSession): CRConnection { | ||||||
|  | |||||||
| @ -58,8 +58,6 @@ export class FFConnection extends EventEmitter { | |||||||
|     this._lastId = 0; |     this._lastId = 0; | ||||||
|     this._callbacks = new Map(); |     this._callbacks = new Map(); | ||||||
| 
 | 
 | ||||||
|     this._transport.onmessage = this._onMessage.bind(this); |  | ||||||
|     this._transport.onclose = this._onClose.bind(this); |  | ||||||
|     this._sessions = new Map(); |     this._sessions = new Map(); | ||||||
|     this._closed = false; |     this._closed = false; | ||||||
| 
 | 
 | ||||||
| @ -68,6 +66,10 @@ export class FFConnection extends EventEmitter { | |||||||
|     this.off = super.removeListener; |     this.off = super.removeListener; | ||||||
|     this.removeListener = super.removeListener; |     this.removeListener = super.removeListener; | ||||||
|     this.once = super.once; |     this.once = super.once; | ||||||
|  | 
 | ||||||
|  |     this._transport.onmessage = this._onMessage.bind(this); | ||||||
|  |     // onclose should be set last, since it can be immediately called.
 | ||||||
|  |     this._transport.onclose = this._onClose.bind(this); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async send<T extends keyof Protocol.CommandParameters>( |   async send<T extends keyof Protocol.CommandParameters>( | ||||||
|  | |||||||
| @ -20,26 +20,37 @@ import { makeWaitForNextTask } from '../utils'; | |||||||
| import { debugLogger } from '../common/debugLogger'; | import { debugLogger } from '../common/debugLogger'; | ||||||
| 
 | 
 | ||||||
| export class PipeTransport implements ConnectionTransport { | export class PipeTransport implements ConnectionTransport { | ||||||
|  |   private _pipeRead: NodeJS.ReadableStream; | ||||||
|   private _pipeWrite: NodeJS.WritableStream; |   private _pipeWrite: NodeJS.WritableStream; | ||||||
|   private _pendingMessage = ''; |   private _pendingMessage = ''; | ||||||
|   private _waitForNextTask = makeWaitForNextTask(); |   private _waitForNextTask = makeWaitForNextTask(); | ||||||
|   private _closed = false; |   private _closed = false; | ||||||
|  |   private _onclose?: () => void; | ||||||
| 
 | 
 | ||||||
|   onmessage?: (message: ProtocolResponse) => void; |   onmessage?: (message: ProtocolResponse) => void; | ||||||
|   onclose?: () => void; |  | ||||||
| 
 | 
 | ||||||
|   constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream) { |   constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream) { | ||||||
|  |     this._pipeRead = pipeRead; | ||||||
|     this._pipeWrite = pipeWrite; |     this._pipeWrite = pipeWrite; | ||||||
|     pipeRead.on('data', buffer => this._dispatch(buffer)); |     pipeRead.on('data', buffer => this._dispatch(buffer)); | ||||||
|     pipeRead.on('close', () => { |     pipeRead.on('close', () => { | ||||||
|       this._closed = true; |       this._closed = true; | ||||||
|       if (this.onclose) |       if (this._onclose) | ||||||
|         this.onclose.call(null); |         this._onclose.call(null); | ||||||
|     }); |     }); | ||||||
|     pipeRead.on('error', e => debugLogger.log('error', e)); |     pipeRead.on('error', e => debugLogger.log('error', e)); | ||||||
|     pipeWrite.on('error', e => debugLogger.log('error', e)); |     pipeWrite.on('error', e => debugLogger.log('error', e)); | ||||||
|     this.onmessage = undefined; |     this.onmessage = undefined; | ||||||
|     this.onclose = undefined; |   } | ||||||
|  | 
 | ||||||
|  |   get onclose() { | ||||||
|  |     return this._onclose; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   set onclose(onclose: undefined | (() => void)) { | ||||||
|  |     this._onclose = onclose; | ||||||
|  |     if (onclose && !this._pipeRead.readable) | ||||||
|  |       onclose(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   send(message: ProtocolRequest) { |   send(message: ProtocolRequest) { | ||||||
|  | |||||||
| @ -47,14 +47,15 @@ export class WKConnection { | |||||||
| 
 | 
 | ||||||
|   constructor(transport: ConnectionTransport, onDisconnect: () => void, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) { |   constructor(transport: ConnectionTransport, onDisconnect: () => void, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) { | ||||||
|     this._transport = transport; |     this._transport = transport; | ||||||
|     this._transport.onmessage = this._dispatchMessage.bind(this); |  | ||||||
|     this._transport.onclose = this._onClose.bind(this); |  | ||||||
|     this._onDisconnect = onDisconnect; |     this._onDisconnect = onDisconnect; | ||||||
|     this._protocolLogger = protocolLogger; |     this._protocolLogger = protocolLogger; | ||||||
|     this._browserLogsCollector = browserLogsCollector; |     this._browserLogsCollector = browserLogsCollector; | ||||||
|     this.browserSession = new WKSession(this, '', kBrowserClosedError, (message: any) => { |     this.browserSession = new WKSession(this, '', kBrowserClosedError, (message: any) => { | ||||||
|       this.rawSend(message); |       this.rawSend(message); | ||||||
|     }); |     }); | ||||||
|  |     this._transport.onmessage = this._dispatchMessage.bind(this); | ||||||
|  |     // onclose should be set last, since it can be immediately called.
 | ||||||
|  |     this._transport.onclose = this._onClose.bind(this); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   nextMessageId(): number { |   nextMessageId(): number { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dmitry Gozman
						Dmitry Gozman