Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Commit 482a9bb

Browse files
Merge pull request #359 from dotnet/copilot/fix-emulator-boot-error
Handle InvalidOperationException in emulator boot and improve Process disposal
2 parents 7d0e083 + 78ee58f commit 482a9bb

2 files changed

Lines changed: 81 additions & 40 deletions

File tree

‎src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs‎

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
thrownewInvalidOperationException($"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-
boolprocessExitedWithZero=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+
boolprocessExitedWithZero=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+
returnnewEmulatorBootResult{
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(InvalidOperationExceptionex){
222242
returnnewEmulatorBootResult{
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-
awaitTask.Delay(options.PollInterval,timeoutCts.Token).ConfigureAwait(false);
235248

236-
devices=awaitadbRunner.ListDevicesAsync(timeoutCts.Token).ConfigureAwait(false);
237-
newSerial=FindRunningAvdSerial(devices,deviceOrAvdName);
238-
}
249+
awaitTask.Delay(options.PollInterval,timeoutCts.Token).ConfigureAwait(false);
239250

240-
logger.Invoke(TraceLevel.Info,$"Emulator appeared as '{newSerial}', waiting for full boot...");
241-
varresult=awaitWaitForFullBootAsync(adbRunner,newSerial,options,timeoutCts.Token).ConfigureAwait(false);
251+
devices=awaitadbRunner.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-
returnresult;
247-
}catch(OperationCanceledException)when(!cancellationToken.IsCancellationRequested){
248-
TryKillProcess(emulatorProcess);
249-
returnnewEmulatorBootResult{
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+
returnawaitWaitForFullBootAsync(adbRunner,newSerial,options,timeoutCts.Token).ConfigureAwait(false);
257+
}catch(OperationCanceledException)when(!cancellationToken.IsCancellationRequested){
258+
TryKillProcess(emulatorProcess);
259+
returnnewEmulatorBootResult{
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(Exceptionex){
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

‎tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,38 @@ public void BootEmulatorAsync_EmptyDeviceName_Throws ()
480480
runner.BootEmulatorAsync("",mockAdb));
481481
}
482482

483+
[Test]
484+
publicasyncTaskInvalidEmulatorBinary_ReturnsLaunchFailed()
485+
{
486+
var(tempDir,emuPath)=CreateFakeEmulatorSdk();
487+
488+
// Overwrite with a script that exits immediately with error code 1
489+
if(OS.IsWindows){
490+
File.WriteAllText(emuPath,"@echo off\r\nexit /b 1\r\n");
491+
}else{
492+
File.WriteAllText(emuPath,"#!/bin/sh\nexit 1\n");
493+
}
494+
495+
try{
496+
vardevices=newList<AdbDeviceInfo>();
497+
varmockAdb=newMockAdbRunner(devices);
498+
499+
varrunner=newEmulatorRunner(emuPath);
500+
varoptions=newEmulatorBootOptions{
501+
BootTimeout=TimeSpan.FromSeconds(5),
502+
PollInterval=TimeSpan.FromMilliseconds(50),
503+
};
504+
505+
varresult=awaitrunner.BootEmulatorAsync("Test_AVD",mockAdb,options);
506+
507+
Assert.IsFalse(result.Success);
508+
Assert.AreEqual(EmulatorBootErrorKind.LaunchFailed,result.ErrorKind);
509+
Assert.That(result.ErrorMessage,Does.Contain("exited with code"));
510+
}finally{
511+
Directory.Delete(tempDir,true);
512+
}
513+
}
514+
483515
// --- Helpers ---
484516

485517
static(stringtempDir,stringemulatorPath)CreateFakeEmulatorSdk()

0 commit comments

Comments
 (0)