@@ -581,42 +581,71 @@ fn replay_cache_hit(
581581}
582582
583583/// Phase 6: drain the child's pipes (if piped) and wait for exit, with a
584- /// single error sink — a pipe failure cancels (so the wait kills the child
585- /// instead of orphaning it) and surfaces through the same returned result as
586- /// a wait failure. After the child exits (on every path), `stop_accepting`
587- /// is signalled so the IPC server stops accepting and starts draining.
584+ /// single error sink. Pipe drain and child wait must be driven together: on
585+ /// Windows, descendants can inherit stdout/stderr handles and keep the pipe
586+ /// open after the direct child exits. Polling wait promptly lets the Job
587+ /// Object cleanup run, which terminates those descendants and lets pipe drain
588+ /// reach EOF. After the child exits (on every path), `stop_accepting` is
589+ /// signalled so the IPC server stops accepting and starts draining.
588590async fn run_child (
589591 mut child : ChildHandle ,
590592 sinks : Option < PipeSinks < ' _ > > ,
591593 stop_accepting : Option < & StopAccepting > ,
592594 fast_fail_token : CancellationToken ,
593595) -> anyhow:: Result < ChildOutcome > {
594- let pipe_result: anyhow:: Result < ( ) > = if let Some ( sinks) = sinks {
595- let stdout = child. stdout . take ( ) . expect ( "SpawnStdio::Piped yields a stdout pipe" ) ;
596- let stderr = child. stderr . take ( ) . expect ( "SpawnStdio::Piped yields a stderr pipe" ) ;
597- #[ expect(
598- clippy:: large_futures,
599- reason = "pipe_stdio streams child I/O and creates a large future"
600- ) ]
601- let r = pipe_stdio ( stdout, stderr, sinks, fast_fail_token. clone ( ) ) . await ;
602- r. map_err ( anyhow:: Error :: from)
603- } else {
604- Ok ( ( ) )
596+ let Some ( sinks) = sinks else {
597+ let wait_result = child. wait . await . map_err ( anyhow:: Error :: from) ;
598+ if let Some ( stop_accepting) = stop_accepting {
599+ stop_accepting. signal ( ) ;
600+ }
601+ return wait_result;
605602 } ;
606603
607- let wait_result = match pipe_result {
608- Ok ( ( ) ) => child. wait . await . map_err ( anyhow:: Error :: from) ,
609- Err ( err) => {
610- // Pipe failed — cancel so `child.wait` kills the child instead of
611- // orphaning it. Still signal the server below so it can drain.
612- fast_fail_token. cancel ( ) ;
613- let _ = child. wait . await ;
614- Err ( err)
604+ let stdout = child. stdout . take ( ) . expect ( "SpawnStdio::Piped yields a stdout pipe" ) ;
605+ let stderr = child. stderr . take ( ) . expect ( "SpawnStdio::Piped yields a stderr pipe" ) ;
606+
607+ let mut pipe = Box :: pin ( pipe_stdio ( stdout, stderr, sinks, fast_fail_token. clone ( ) ) ) ;
608+ let mut wait = child. wait ;
609+
610+ tokio:: select! {
611+ pipe_result = & mut pipe => {
612+ match pipe_result. map_err( anyhow:: Error :: from) {
613+ Ok ( ( ) ) => {
614+ let wait_result = wait. await . map_err( anyhow:: Error :: from) ;
615+ if let Some ( stop_accepting) = stop_accepting {
616+ stop_accepting. signal( ) ;
617+ }
618+ wait_result
619+ }
620+ Err ( err) => {
621+ // Pipe failed — cancel so `child.wait` kills the child instead of
622+ // orphaning it. Still signal the server below so it can drain.
623+ fast_fail_token. cancel( ) ;
624+ let _ = wait. await ;
625+ if let Some ( stop_accepting) = stop_accepting {
626+ stop_accepting. signal( ) ;
627+ }
628+ Err ( err)
629+ }
630+ }
615631 }
616- } ;
632+ wait_result = & mut wait => {
633+ let wait_result = wait_result. map_err( anyhow:: Error :: from) ;
634+ if let Some ( stop_accepting) = stop_accepting {
635+ stop_accepting. signal( ) ;
636+ }
617637
618- if let Some ( stop_accepting) = stop_accepting {
619- stop_accepting. signal ( ) ;
638+ match wait_result {
639+ Ok ( outcome) => {
640+ pipe. await . map_err( anyhow:: Error :: from) ?;
641+ Ok ( outcome)
642+ }
643+ Err ( err) => {
644+ fast_fail_token. cancel( ) ;
645+ let _ = pipe. await ;
646+ Err ( err)
647+ }
648+ }
649+ }
620650 }
621- wait_result
622651}
0 commit comments