@@ -85,7 +85,10 @@ public Process LaunchEmulator (string avdName, bool coldBoot = false, List<strin
8585 logger . Invoke ( TraceLevel . Warning , $ "[emulator] { e . Data } ") ;
8686 } ;
8787
88- process . Start ( ) ;
88+ if ( ! process . Start ( ) ) {
89+ process . Dispose ( ) ;
90+ throw new InvalidOperationException ( $ "Failed to start emulator process '{ emulatorPath } '.") ;
91+ }
8992
9093 // Drain redirected streams asynchronously to prevent pipe buffer deadlocks
9194 process . BeginOutputReadLine ( ) ;
@@ -209,51 +212,59 @@ public async Task<EmulatorBootResult> BootEmulatorAsync (
209212 // code 0 immediately. The real emulator continues as a separate process and
210213 // will eventually appear in 'adb devices'. We only treat non-zero exit codes
211214 // as immediate failures; exit code 0 means we continue polling.
212- try {
213- string ? newSerial = null ;
214- bool processExitedWithZero = false ;
215- while ( newSerial == null ) {
216- timeoutCts . Token . ThrowIfCancellationRequested ( ) ;
217-
218- // Detect early process exit for fast failure
219- if ( emulatorProcess . HasExited && ! processExitedWithZero ) {
220- if ( emulatorProcess . ExitCode != 0 ) {
221- emulatorProcess . Dispose ( ) ;
215+ //
216+ // Dispose the Process handle when done — the emulator process keeps running.
217+ using ( emulatorProcess ) {
218+ try {
219+ string ? newSerial = null ;
220+ bool processExitedWithZero = false ;
221+ while ( newSerial == null ) {
222+ timeoutCts . Token . ThrowIfCancellationRequested ( ) ;
223+
224+ // Detect early process exit for fast failure.
225+ // Guard against InvalidOperationException in case no OS process
226+ // is associated with the object (e.g. broken emulator binary).
227+ try {
228+ if ( emulatorProcess . HasExited && ! processExitedWithZero ) {
229+ if ( emulatorProcess . ExitCode != 0 ) {
230+ return new EmulatorBootResult {
231+ Success = false ,
232+ ErrorKind = EmulatorBootErrorKind . LaunchFailed ,
233+ ErrorMessage = $ "Emulator process for '{ deviceOrAvdName } ' exited with code { emulatorProcess . ExitCode } before becoming available.",
234+ } ;
235+ }
236+ // Exit code 0: emulator likely forked (common on macOS).
237+ // The real emulator runs as a separate process — keep polling.
238+ logger . Invoke ( TraceLevel . Verbose , $ "Emulator launcher process exited with code 0 (likely forked). Continuing to poll adb devices.") ;
239+ processExitedWithZero = true ;
240+ }
241+ } catch ( InvalidOperationException ex ) {
222242 return new EmulatorBootResult {
223243 Success = false ,
224244 ErrorKind = EmulatorBootErrorKind . LaunchFailed ,
225- ErrorMessage = $ "Emulator process for '{ deviceOrAvdName } ' exited with code { emulatorProcess . ExitCode } before becoming available. ",
245+ ErrorMessage = $ "Emulator process for '{ deviceOrAvdName } ' is no longer available: { ex . Message } ",
226246 } ;
227247 }
228- // Exit code 0: emulator likely forked (common on macOS).
229- // The real emulator runs as a separate process — keep polling.
230- logger . Invoke ( TraceLevel . Verbose , $ "Emulator launcher process exited with code 0 (likely forked). Continuing to poll adb devices.") ;
231- processExitedWithZero = true ;
232- }
233-
234- await Task . Delay ( options . PollInterval , timeoutCts . Token ) . ConfigureAwait ( false ) ;
235248
236- devices = await adbRunner . ListDevicesAsync ( timeoutCts . Token ) . ConfigureAwait ( false ) ;
237- newSerial = FindRunningAvdSerial ( devices , deviceOrAvdName ) ;
238- }
249+ await Task . Delay ( options . PollInterval , timeoutCts . Token ) . ConfigureAwait ( false ) ;
239250
240- logger . Invoke ( TraceLevel . Info , $ "Emulator appeared as '{ newSerial } ', waiting for full boot...") ;
241- var result = await WaitForFullBootAsync ( adbRunner , newSerial , options , timeoutCts . Token ) . ConfigureAwait ( false ) ;
251+ devices = await adbRunner . ListDevicesAsync ( timeoutCts . Token ) . ConfigureAwait ( false ) ;
252+ newSerial = FindRunningAvdSerial ( devices , deviceOrAvdName ) ;
253+ }
242254
243- // Release the Process handle — the emulator process itself keeps running.
244- // We no longer need stdout/stderr forwarding since boot is complete.
245- emulatorProcess . Dispose ( ) ;
246- return result ;
247- } catch ( OperationCanceledException ) when ( ! cancellationToken . IsCancellationRequested ) {
248- TryKillProcess ( emulatorProcess ) ;
249- return new EmulatorBootResult {
250- Success = false ,
251- ErrorKind = EmulatorBootErrorKind . Timeout ,
252- ErrorMessage = $ "Timed out waiting for emulator '{ deviceOrAvdName } ' to boot within { options . BootTimeout . TotalSeconds } s.",
253- } ;
254- } catch {
255- TryKillProcess ( emulatorProcess ) ;
256- throw ;
255+ logger . Invoke ( TraceLevel . Info , $ "Emulator appeared as '{ newSerial } ', waiting for full boot...") ;
256+ return await WaitForFullBootAsync ( adbRunner , newSerial , options , timeoutCts . Token ) . ConfigureAwait ( false ) ;
257+ } catch ( OperationCanceledException ) when ( ! cancellationToken . IsCancellationRequested ) {
258+ TryKillProcess ( emulatorProcess ) ;
259+ return new EmulatorBootResult {
260+ Success = false ,
261+ ErrorKind = EmulatorBootErrorKind . Timeout ,
262+ ErrorMessage = $ "Timed out waiting for emulator '{ deviceOrAvdName } ' to boot within { options . BootTimeout . TotalSeconds } s.",
263+ } ;
264+ } catch {
265+ TryKillProcess ( emulatorProcess ) ;
266+ throw ;
267+ }
257268 }
258269 }
259270
@@ -276,8 +287,6 @@ void TryKillProcess (Process process)
276287 } catch ( Exception ex ) {
277288 // Best-effort: process may have already exited
278289 logger . Invoke ( TraceLevel . Verbose , $ "Failed to stop emulator process: { ex . Message } ") ;
279- } finally {
280- process . Dispose ( ) ;
281290 }
282291 }
283292
0 commit comments