jdx/mise · error
command output pipes did not close within
Error message
command output pipes did not close within {pipe_drain_timeout:?} What it means
After the command process exits, execute_hashes_async_with_drain_timeout waits up to pipe_drain_timeout for the stdout/stderr pipes to close (all buffered output consumed). If the deadline expires or a remaining-drain recv times out, the process tree is SIGKILLed and this error is thrown. It means grandchildren/descendants still hold the pipes open, so output may be incomplete.
Solutions
- Redirect background children's output to files or /dev/null (e.g. `child --daemon > /dev/null 2>&1 &`) so the pipes close when the parent exits
- Increase the pipe_drain_timeout at the call site if legitimate slow output needs more time
- Fix the task so it waits for or detaches its children properly instead of leaving them holding the pipes
Example fix
// before my-task & # background child keeps stdout/stderr pipe open -> drain timeout // after nohup my-task > /tmp/my-task.log 2>&1 < /dev/null &
Defensive patterns
Strategy: try-catch
Validate before calling
// prefer tasks whose children detach their own output // grep your script for background spawns inheriting stdout: // grep -nE '(^|[^>]*)&( |$)' script.sh
Try / catch
match cmd.execute_hashes_async(MAX).await {
Ok(h) => Ok(h),
Err(e) if e.to_string().contains("pipes did not close") => {
// kill orphans, then retry with children redirected
cleanup_orphans();
cmd.execute_hashes_async(MAX).await
}
Err(e) => Err(e),
} Prevention
- Always redirect background children: `cmd > log 2>&1 < /dev/null &`
- Never leave daemons/watchers holding inherited stdio in tasks
- Set pipe_drain_timeout generously for tasks that legitimately spawn children
- Audit task scripts for `&` background spawns without redirection
When it happens
Trigger: The main command exits but spawned background children or grandchildren inherit stdout/stderr and keep the pipes open beyond pipe_drain_timeout; the recv timeout in the drain loop elapses with remaining time at zero.
Common situations: Tasks that daemonize or launch background servers (watchers, dev servers) that inherit the parent's output pipes; scripts spawning long-lived children without redirecting their output.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- timed out after
- timed out after
- a process it started kept its output open
- command output exceeded
- exited with non-zero status
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/c6dba2ce5504e0e8.
Report an issue: GitHub.
Appendix: source
Thrown at src/cmd.rs:1442
#[cfg(unix)]
signal_process_tree(id, nix::sys::signal::Signal::SIGKILL);
#[cfg(windows)]
kill_process_tree(id);
let _ = wait.await;
return Err(err);
}
}
}
}
let drain_deadline = Instant::now() + pipe_drain_timeout;
loop {
let remaining = drain_deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
#[cfg(unix)]
signal_process_tree(id, nix::sys::signal::Signal::SIGKILL);
#[cfg(windows)]
kill_process_tree(id);
bail!("command output pipes did not close within {pipe_drain_timeout:?}");
}
let output = match tokio::time::timeout(remaining, rx.recv()).await {
Ok(Some(output)) => output,
Ok(None) => break,
Err(_) => {
#[cfg(unix)]
signal_process_tree(id, nix::sys::signal::Signal::SIGKILL);
#[cfg(windows)]
kill_process_tree(id);
bail!("command output pipes did not close within {pipe_drain_timeout:?}");
}
};
if let Err(err) = consume(output) {
#[cfg(unix)]
signal_process_tree(id, nix::sys::signal::Signal::SIGKILL);
#[cfg(windows)]
kill_process_tree(id);
return Err(err);View on GitHub (pinned to 533346cc37)