Skip to content

Launch emulator in signal-isolated process on Unix to survive Ctrl+C#361

Open
Copilot wants to merge 4 commits intomainfrom
copilot/fix-ctrl-c-emulator-closure
Open

Launch emulator in signal-isolated process on Unix to survive Ctrl+C#361
Copilot wants to merge 4 commits intomainfrom
copilot/fix-ctrl-c-emulator-closure

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 7, 2026

Ctrl+C of dotnet run kills the Android emulator because the terminal sends SIGINT to the entire process group, including the child emulator process.

Changes

  • Signal isolation on Unix: Wrap emulator launch through /bin/sh -c 'trap "" INT; exec ...'. trap '' INT sets SIGINT to SIG_IGN, which POSIX guarantees is preserved across exec. The emulator process ignores SIGINT from the parent terminal while remaining killable via adb emu kill or window close. exec replaces the shell so the Process object tracks the correct PID.

  • Windows unchanged: Ctrl+C propagation works differently on Windows; no wrapper needed.

  • Bug fix: BootEmulatorAsync accessed emulatorProcess.ExitCode after Dispose() — save exit code before disposing.

  • Shell quoting: Added ShellQuote() for proper POSIX single-quote escaping of emulator path and arguments.

// Before: emulator inherits parent's process group and receives SIGINT
psi = ProcessUtils.CreateProcessStartInfo(emulatorPath, args.ToArray());

// After (Unix): emulator ignores SIGINT via SIG_IGN preserved across exec
var shellCmd = new StringBuilder("trap '' INT; exec ");
shellCmd.Append(ShellQuote(emulatorPath));
foreach (var arg in args) {
    shellCmd.Append(' ');
    shellCmd.Append(ShellQuote(arg));
}
psi = ProcessUtils.CreateProcessStartInfo("/bin/sh", "-c", shellCmd.ToString());

Tests

  • LaunchEmulator_SurvivesSigint — sends SIGINT to the launched process, asserts it remains alive
  • ShellQuote_EscapesSingleQuotes — verifies paths containing ' work correctly

…trl+C

On Unix, wrap the emulator launch through /bin/sh with 'trap "" INT; exec ...'
which sets SIGINT to SIG_IGN before exec'ing the emulator. POSIX guarantees
SIG_IGN is preserved across exec, so the emulator process ignores SIGINT
(Ctrl+C) from the parent terminal.

Also fix pre-existing bug where emulatorProcess.ExitCode was accessed after
Dispose() in BootEmulatorAsync.

Agent-Logs-Url: https://github.com/dotnet/android-tools/sessions/9eb96c77-ebf5-45a1-8e00-d54a177d6ef3

Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix Ctrl+C closing emulator when running dotnet app Launch emulator in signal-isolated process on Unix to survive Ctrl+C May 7, 2026
Copilot AI requested a review from jonathanpeppers May 7, 2026 21:18
@jonathanpeppers
Copy link
Copy Markdown
Member

jonathanpeppers commented May 7, 2026

I investigated P/Invoke alternatives and none work cleanly:

Approach Problem
setpgid(childPid, childPid) from parent after Process.Start() Race condition — child may have already exec'd, causing EACCES
signal(SIGINT, SIG_IGN) before Start(), restore after Conflicts with .NET runtime signal handling; parent briefly ignores SIGINT
ProcessStartInfo.CreateNewProcessGroup Throws PlatformNotSupportedException on Unix (all .NET versions including 11)
/usr/bin/setsid <emulator> Not available on macOS
Manual fork()/exec() via P/Invoke Extremely complex, fragile in managed code

The core issue is that .NET's Process class has no pre-exec hook (unlike Python's preexec_fn), so there's no way to call setsid() or signal(SIGINT, SIG_IGN) in the child between fork and exec.

The trap '' INT; exec pattern used here is the well-established POSIX idiom for this:

  1. trap '' INT sets SIGINT to SIG_IGN in the shell
  2. exec replaces the shell with the emulator
  3. POSIX guarantees SIG_IGN is preserved across exec (POSIX.1-2017 Section 2.4.3):

    "Any signals that had been set to be ignored (SIG_IGN) shall remain ignored."

See also: POSIX trap utility, Austin Group Bug #152

@jonathanpeppers jonathanpeppers marked this pull request as ready for review May 7, 2026 21:34
Copilot AI review requested due to automatic review settings May 7, 2026 21:34
@jonathanpeppers
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Android Tools PR Reviewer completed successfully!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Xamarin.Android.Tools.AndroidSdk’s emulator launching behavior on Unix so the Android emulator won’t be terminated by Ctrl+C sent to the parent terminal’s process group, while keeping Windows behavior unchanged. It also tightens emulator boot failure reporting and adds targeted Unix-only tests to validate the SIGINT-survival and shell-quoting behavior.

Changes:

  • On Unix, launch the emulator via /bin/sh -c "trap '' INT; exec ..." and add POSIX-safe single-quote escaping via ShellQuote().
  • Fix BootEmulatorAsync to avoid reading Process.ExitCode after disposing the process handle.
  • Update/extend tests to validate SIGINT survival on Unix and correct handling of emulator paths containing single quotes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Wrap Unix emulator launch in a SIGINT-ignoring shell+exec command; fix exit-code capture before dispose; add POSIX shell quoting helper.
tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs Adjust launch-failure assertion to check ErrorKind; add Unix-only tests for SIGINT survival and quoting of paths containing '.

Comment thread tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs Outdated
Comment thread tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs Outdated
Comment thread tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs Outdated
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ✅ LGTM — Clean, well-reasoned change.

Summary: The SIGINT isolation via trap '' INT; exec is the correct POSIX approach, and the ShellQuote implementation uses the canonical single-quote escaping pattern. The ExitCode-after-Dispose() bug fix is a genuine catch. The test for SIGINT survival is well-designed — it verifies the core behavior by sending a real signal and checking the process remains alive.

Suggestions (non-blocking):

  • 💡 Move the two new tests above the // --- Helpers --- separator to keep test/helper sections distinct
  • ⚠️ Add process kill logic to the finally blocks in both new tests to match the cleanup pattern used by existing tests and avoid potential process leaks in CI
Severity Count
❌ Error 0
⚠️ Warning 1 (test cleanup)
💡 Suggestion 2

Positive callouts:

  • The exec trick preserves the Process PID tracking while achieving signal isolation — avoids the complexity of P/Invoke or process group management
  • Saving ExitCode before Dispose() prevents a real ObjectDisposedException
  • Good test coverage: both the SIGINT survival and the single-quote-in-path edge case

Generated by Android Tools PR Reviewer for issue #361 · ● 2.3M

Comment thread src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment thread src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs
Comment thread tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs
jonathanpeppers and others added 2 commits May 7, 2026 16:47
- Add POSIX spec link to the trap/exec code comment
- Remove section separator comments (// --- Helpers --- etc.)
- Assert kill exit code in SurvivesSigint test to prevent false positives
- Add timeout to chmod.WaitForExit() to prevent CI hangs
- Move process cleanup to finally blocks to avoid leaking processes in CI

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Took origin/main InvalidOperationException guard around HasExited and
the new InvalidEmulatorBinary_ReturnsLaunchFailed test. Kept this
branch's SIGINT-survival and ShellQuote tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines +479 to +481
[Test]
[Platform ("Linux,MacOsX")]
public void LaunchEmulator_SurvivesSigint ()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test passes on macOS:

Image

Copy link
Copy Markdown
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will fix this problem, but I'm open to other solutions than this trap command.

I couldn't get copilot to find an alternative:

@jonathanpeppers jonathanpeppers added the ready-to-review This PR is ready to review/merge. label May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-review This PR is ready to review/merge.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ctrl+C of dotnet run closes the emulator

3 participants