Skip to content

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+
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

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+
public async Task InvalidEmulatorBinary_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+
var devices = new List<AdbDeviceInfo> ();
497+
var mockAdb = new MockAdbRunner (devices);
498+
499+
var runner = new EmulatorRunner (emuPath);
500+
var options = new EmulatorBootOptions {
501+
BootTimeout = TimeSpan.FromSeconds (5),
502+
PollInterval = TimeSpan.FromMilliseconds (50),
503+
};
504+
505+
var result = await runner.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 (string tempDir, string emulatorPath) CreateFakeEmulatorSdk ()

0 commit comments

Comments
 (0)