P0: ProcessManager Child::wait() deadlocks send_stdin and closes pipe prematurely on interactive subprocesses (chartread) #84

Closed
opened 2026-08-25 13:55:11 +01:00 by gronod · 1 comment
Owner

Problem Description

In PR #77 (Issue #63), try_wait polling was replaced with child.wait().await inside a lock on Arc<Mutex<Child>>.
This caused two critical regressions in ProcessManager that broke Stage 3 (chartread):

  1. Child Mutex Deadlock: c.wait().await held &mut self on the Mutex<Child> guard for the entire lifetime of the process. Interactive commands (send_stdin and kill) attempted to acquire the same mutex, blocking Calibrate / Retry / Skip / Cancel actions until the child process had already exited.
  2. Premature Stdin Closure: Calling child.wait() in Tokio explicitly drops child.stdin to avoid parent/child deadlocks. Because the wait task spawned immediately after spawn(), stdin was closed before any user interaction could occur.

Resolution

Implemented in PR #83 and released in v0.2.1:

  • take() standard streams (stdin, stdout, stderr) immediately upon process spawn.
  • Decoupled ChildStdin into an independent stdins map (HashMap<String, Arc<Mutex<ChildStdin>>>).
  • Used tokio::select! between child.wait() and a dedicated oneshot cancellation kill_rx channel.
  • send_stdin accesses only the ChildStdin handle without blocking against child.wait().
  • kill drops stdin immediately and triggers child.start_kill().

Reference

### Problem Description In PR #77 (Issue #63), `try_wait` polling was replaced with `child.wait().await` inside a lock on `Arc<Mutex<Child>>`. This caused two critical regressions in `ProcessManager` that broke Stage 3 (`chartread`): 1. **Child Mutex Deadlock**: `c.wait().await` held `&mut self` on the `Mutex<Child>` guard for the entire lifetime of the process. Interactive commands (`send_stdin` and `kill`) attempted to acquire the same mutex, blocking Calibrate / Retry / Skip / Cancel actions until the child process had already exited. 2. **Premature Stdin Closure**: Calling `child.wait()` in Tokio explicitly drops `child.stdin` to avoid parent/child deadlocks. Because the wait task spawned immediately after `spawn()`, `stdin` was closed before any user interaction could occur. ### Resolution Implemented in PR #83 and released in `v0.2.1`: - `take()` standard streams (`stdin`, `stdout`, `stderr`) immediately upon process spawn. - Decoupled `ChildStdin` into an independent `stdins` map (`HashMap<String, Arc<Mutex<ChildStdin>>>`). - Used `tokio::select!` between `child.wait()` and a dedicated oneshot cancellation `kill_rx` channel. - `send_stdin` accesses only the `ChildStdin` handle without blocking against `child.wait()`. - `kill` drops stdin immediately and triggers `child.start_kill()`. ### Reference - Resolution PR: #83 - Release: `v0.2.1`
gronod added this to the Milestone 7: v0.2 ship milestone 2026-08-25 13:55:11 +01:00
gronod added the Kind/Bug
Priority
Critical
1
labels 2026-08-25 13:55:11 +01:00
Author
Owner

Resolved by PR #83 and included in release v0.2.1. ProcessManager standard input handling has been decoupled from child process waiting tasks.

Resolved by PR #83 and included in release `v0.2.1`. ProcessManager standard input handling has been decoupled from child process waiting tasks.
Sign in to join this conversation.