fix(installer): keep the post-EOF wait cancellable
Phase 1 leaves on both pipes reaching EOF without an exit status, so the final wait was the only thing holding the turn -- and it was a bare child.wait(), which stranded the cancel channel against a child that closes its pipes and lingers. Poll cancellation there too.
This commit is contained in:
@@ -256,7 +256,20 @@ where
|
||||
|
||||
let status = match status {
|
||||
Some(s) => s,
|
||||
None => child.wait().await.context("waiting for child to exit")?,
|
||||
// Both pipes reached EOF while the child stayed alive. Reads are done,
|
||||
// but the process is still the authoritative terminal condition — and
|
||||
// it may never exit on its own, so this wait stays cancellable. A bare
|
||||
// `child.wait()` here would strand the caller's cancel channel exactly
|
||||
// when it is the only way out.
|
||||
None => tokio::select! {
|
||||
reaped = child.wait() => reaped.context("waiting for child to exit")?,
|
||||
_ = recv_cancel(cancel_rx) => {
|
||||
tracing::warn!("cancellation received after EOF — killing child");
|
||||
cancelled = true;
|
||||
let _ = child.start_kill();
|
||||
child.wait().await.context("waiting for killed child to exit")?
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok(PumpOutcome {
|
||||
exit_code: status.code(),
|
||||
@@ -785,4 +798,45 @@ info line
|
||||
started.elapsed()
|
||||
);
|
||||
}
|
||||
|
||||
/// The reverse topology of the test above: the pipes die and the *process*
|
||||
/// outlives them. Phase 1 leaves on EOF with no exit status, so the final
|
||||
/// wait is the only thing left holding the turn -- and it has to stay
|
||||
/// cancellable. A bare `child.wait()` there strands the cancel channel
|
||||
/// against a child that may never exit on its own.
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn pump_child_cancellation_returns_after_both_pipes_reach_eof() {
|
||||
// Closes fd 1 and 2, then lingers: both reads hit EOF immediately while
|
||||
// the process stays alive far past any plausible test duration.
|
||||
let mut child = sh("echo bye; exec 1>&- 2>&-; sleep 30");
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
let mut cancel = Some(rx);
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
let _ = tx.send(()).await;
|
||||
});
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let mut lines = Vec::new();
|
||||
let outcome = pump_child(
|
||||
&mut child,
|
||||
|l| lines.push(l.to_string()),
|
||||
|_| {},
|
||||
&mut cancel,
|
||||
Duration::from_millis(300),
|
||||
)
|
||||
.await
|
||||
.expect("pump");
|
||||
let elapsed = started.elapsed();
|
||||
|
||||
assert!(outcome.killed, "cancellation should report killed");
|
||||
assert!(
|
||||
!outcome.abandoned,
|
||||
"both pipes reached EOF, so nothing was abandoned"
|
||||
);
|
||||
assert_eq!(lines, vec!["bye"], "output written before EOF must survive");
|
||||
// Far below the child's 30s: a pass cannot be it exiting on its own.
|
||||
assert!(elapsed < Duration::from_secs(10), "took {elapsed:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user