Reap orphaned interactive commands when the console detaches mid-prompt#271
Reap orphaned interactive commands when the console detaches mid-prompt#271stktung wants to merge 2 commits into
Conversation
Interactive commands (setup, login, profile, use, import, uninstall) block on synchronous Spectre.Console prompts with no timeout or cancellation. Piped stdin is safe (the read throws and exits), but a real TTY that later detaches — terminal closed, launching coding-agent killed, detached pseudo-console — delivers neither EOF nor a signal, so the blocking read never returns and the process is orphaned alive. This is the reported stray kcap.exe left behind after `kcap setup` is exited partway through. Add InteractiveLifetime: a signal + parent-liveness safety net installed for interactive commands, mirroring what WatchCommand already does for the watcher: - Console.CancelKeyPress + POSIX SIGTERM/SIGHUP handlers that exit(130). - A parent-process-liveness watchdog that self-terminates once the launching process is gone (the only thing that reaps the detached-TTY case, which delivers no signal). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
/agentic_review |
Code Review by Qodo
1.
|
| var thread = new Thread(() => { | ||
| while (true) { | ||
| Thread.Sleep(ParentPollInterval); | ||
|
|
||
| if (!ProcessHelpers.IsProcessAlive(ppid)) { | ||
| Environment.Exit(InterruptExitCode); | ||
| } | ||
| } |
There was a problem hiding this comment.
2. Pid reuse miss 🐞 Bug ☼ Reliability
The parent watchdog uses periodic IsProcessAlive(ppid) checks on a numeric PID; if the OS reuses that PID between polls, the watchdog can treat an unrelated process as the parent and fail to exit. This is an edge case, but it’s a known limitation of PID-only polling.
Agent Prompt
## Issue description
The watchdog checks liveness by PID. PID reuse can cause a false “alive” result after the real parent exits, preventing termination.
## Issue Context
`ProcessHelpers.IsProcessAlive` explicitly notes PID reuse as a theoretical hazard for polling-based approaches.
## Fix Focus Areas
- src/Capacitor.Cli/InteractiveLifetime.cs[107-125]
- src/Capacitor.Cli/ProcessHelpers.cs[628-665]
## Suggested fix
On Windows, open a handle to the parent process once at install time (best-effort) and monitor it (e.g., `WaitForSingleObject` with a timeout loop) instead of repeatedly probing by PID. If opening the handle fails, fall back to the current PID polling behavior.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Move InteractiveLifetime.Install() ahead of ResolveServerUrl/update-check so the parent-liveness watchdog is armed before any multi-second startup work. StartParentWatchdog refuses to arm once the parent is already gone, so installing after that work left a window where a launching agent could exit during ResolveServerUrl and leave the first prompt able to orphan the process — the exact 'parent died before first prompt' case this PR targets. Addresses qodo review finding on #271. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Problem
On Windows 11 (and any platform),
kcap setupcan leave a straykcap.exerunning indefinitely after the user abandons it partway through — e.g. exits before confirming the visibility step. Confirmed with a live straykcap setupprocess whose launching parent had already exited, all threads parked in a blocking console read.Root cause
setupand other interactive commands drive synchronous Spectre.Console prompts (ConfirmationPrompt,SelectionPrompt,TextPrompt) that do a blocking console read with no timeout or cancellation. OnlyWatchCommandregistersConsole.CancelKeyPress+PosixSignalRegistrationand a parent-exit watchdog; interactive commands had no interrupt handling.Failed to read input in non-interactive modeand the process exits.Reproducible at any prompt, including the very first
ConfirmationPrompt(so it can happen "before step 1"). Affectssetup,login,profile,use,import,uninstall.Fix
Add
InteractiveLifetime, a shared safety net installed only for interactive commands (explicit allow-list, so hooks/mcp/watch/daemonare untouched). It mirrors whatWatchCommandalready does for the watcher:Console.CancelKeyPress+ POSIXSIGTERM/SIGHUPhandlers that exit deterministically (130).ProcessHelpers.GetParentPid/IsProcessAlive) that self-terminates once the launching process is gone — the only thing that reaps the detached-TTY case on Windows, which delivers no catchable signal from inside a blocking prompt read.Redirected-stdin behaviour is unchanged (still fails fast via the existing thrown exception).
Testing
dotnet buildclean.InteractiveLifetimeTestscover the interactive-command allow-list (prompting commands guarded;watch/daemon/hook/mcp/non-prompting commands not). All unit tests pass.dotnet publish -c Releaseto check IL2026/IL3050 AOT warnings — the native toolchain (vswhere/MSVC linker) isn't available on this dev box. The new code uses only patterns already present and AOT-safe inWatchCommand/ProcessHelpers(PosixSignalRegistration,Thread,Environment.Exit,Interlocked), so no new AOT warnings are expected, but a publish check should be run in CI.Notes
Filing an accompanying GitHub issue was blocked in this environment; the problem statement above stands in for it. This same "no interrupt handling" fragility also explains stuck auth-retry hook processes seen elsewhere — a candidate follow-up to extend the same guard to relevant hook paths.