diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs index 772d40fe..95540546 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -57,7 +58,23 @@ public Process LaunchEmulator (string avdName, bool coldBoot = false, List WaitForFullBootAsync ( cancellationToken.ThrowIfCancellationRequested (); return new EmulatorBootResult { Success = false, ErrorKind = EmulatorBootErrorKind.Cancelled, ErrorMessage = "Boot cancelled." }; } + + /// Quotes a string for safe use in a POSIX shell command. + /// Wraps in single quotes and escapes embedded single quotes. + static string ShellQuote (string arg) => "'" + arg.Replace ("'", "'\\''") + "'"; } diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs index 8a6fc862..c94da2ec 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs @@ -97,8 +97,6 @@ public void LaunchEmulator_ThrowsOnWhitespaceAvdName () Assert.Throws (() => runner.LaunchEmulator (" ")); } - // --- BootEmulatorAsync tests (ported from dotnet/android BootAndroidEmulatorTests) --- - [Test] public async Task AlreadyOnlineDevice_PassesThrough () { @@ -205,7 +203,7 @@ public async Task LaunchFailure_ReturnsError () var result = await runner.BootEmulatorAsync ("Pixel_7_API_35", mockAdb, options); Assert.IsFalse (result.Success); - Assert.That (result.ErrorMessage, Does.Contain ("Failed to launch")); + Assert.AreEqual (EmulatorBootErrorKind.LaunchFailed, result.ErrorKind); } [Test] @@ -295,8 +293,6 @@ public async Task MultipleEmulators_FindsCorrectAvd () Assert.AreEqual ("emulator-5556", result.Serial, "Should find the correct AVD among multiple emulators"); } - // --- Tests ported from dotnet/android BootAndroidEmulatorTests --- - [Test] public async Task AlreadyOnlinePhysicalDevice_PassesThrough () { @@ -480,6 +476,36 @@ public void BootEmulatorAsync_EmptyDeviceName_Throws () runner.BootEmulatorAsync ("", mockAdb)); } + [Test] + [Platform ("Linux,MacOsX")] + public void LaunchEmulator_SurvivesSigint () + { + var (tempDir, emuPath) = CreateFakeEmulatorSdk (); + Process? process = null; + try { + var runner = new EmulatorRunner (emuPath); + process = runner.LaunchEmulator ("TestAVD"); + + Assert.IsFalse (process.HasExited, "Process should be running after launch"); + + // Send SIGINT to the emulator process + var killPsi = ProcessUtils.CreateProcessStartInfo ("kill", "-INT", process.Id.ToString ()); + using var kill = new Process { StartInfo = killPsi }; + kill.Start (); + Assert.IsTrue (kill.WaitForExit (5000), "kill command should exit promptly"); + Assert.AreEqual (0, kill.ExitCode, "kill -INT should succeed"); + + // Give the signal a moment to be delivered + Thread.Sleep (500); + + Assert.IsFalse (process.HasExited, "Emulator process should survive SIGINT"); + } finally { + try { process?.Kill (); process?.WaitForExit (5000); } catch { } + process?.Dispose (); + Directory.Delete (tempDir, true); + } + } + [Test] public async Task InvalidEmulatorBinary_ReturnsLaunchFailed () { @@ -512,6 +538,35 @@ public async Task InvalidEmulatorBinary_ReturnsLaunchFailed () } } + [Test] + [Platform ("Linux,MacOsX")] + public void ShellQuote_EscapesSingleQuotes () + { + var tempDir = Path.Combine (Path.GetTempPath (), $"emu-quote-test-{Path.GetRandomFileName ()}"); + var emulatorDir = Path.Combine (tempDir, "emu'dir"); + Directory.CreateDirectory (emulatorDir); + + var emuPath = Path.Combine (emulatorDir, "emulator"); + File.WriteAllText (emuPath, "#!/bin/sh\nsleep 60\n"); + var psi = ProcessUtils.CreateProcessStartInfo ("chmod", "+x", emuPath); + using (var chmod = new Process { StartInfo = psi }) { + chmod.Start (); + Assert.IsTrue (chmod.WaitForExit (5000), "chmod should exit promptly"); + } + + Process? process = null; + try { + var runner = new EmulatorRunner (emuPath); + process = runner.LaunchEmulator ("TestAVD"); + + Assert.IsFalse (process.HasExited, "Process should start even with single-quote in path"); + } finally { + try { process?.Kill (); process?.WaitForExit (5000); } catch { } + process?.Dispose (); + Directory.Delete (tempDir, true); + } + } + // --- Helpers --- static (string tempDir, string emulatorPath) CreateFakeEmulatorSdk ()