@@ -656,3 +656,87 @@ fn test_pids() -> io::Result<()> {
656
656
657
657
Ok ( ( ) )
658
658
}
659
+
660
+ #[ cfg( not( windows) ) ]
661
+ fn ps_observes_pid ( pid : u32 ) -> io:: Result < bool > {
662
+ let pid_str = & pid. to_string ( ) [ ..] ;
663
+ let ps_output = cmd ! ( "ps" , "-p" , pid_str)
664
+ . unchecked ( )
665
+ . stdout_capture ( )
666
+ . run ( ) ?;
667
+ let ps_str = String :: from_utf8_lossy ( & ps_output. stdout ) ;
668
+ let ps_lines: Vec < & str > = ps_str. lines ( ) . collect ( ) ;
669
+ // `ps` prints headers on the first line by default.
670
+ assert ! ( ps_lines. len( ) == 1 || ps_lines. len( ) == 2 ) ;
671
+ if ps_lines. len ( ) == 2 {
672
+ assert ! ( ps_lines[ 1 ] . contains( pid_str) ) ;
673
+ // The exit code should agree with the output.
674
+ assert ! ( ps_output. status. success( ) ) ;
675
+ Ok ( true )
676
+ } else {
677
+ assert ! ( !ps_output. status. success( ) ) ;
678
+ Ok ( false )
679
+ }
680
+ }
681
+
682
+ // We don't spawn reaper threads on Windows, and `ps` doesn't exist on Windows either.
683
+ #[ cfg( not( windows) ) ]
684
+ #[ test]
685
+ fn test_zombies_reaped ( ) -> io:: Result < ( ) > {
686
+ let mut child_handles = Vec :: new ( ) ;
687
+ let mut child_pids = Vec :: new ( ) ;
688
+
689
+ // Spawn 10 children that will exit immediately.
690
+ let ( mut stdout_reader, stdout_writer) = os_pipe:: pipe ( ) ?;
691
+ for _ in 0 ..10 {
692
+ let handle = cmd ! ( path_to_exe( "status" ) , "0" )
693
+ . stdout_file ( stdout_writer. try_clone ( ) ?)
694
+ . start ( ) ?;
695
+ child_pids. push ( handle. pids ( ) [ 0 ] ) ;
696
+ child_handles. push ( handle) ;
697
+ }
698
+
699
+ // Spawn 10 children that will wait on stdin to exit. The previous 10 children will probably
700
+ // exit while we're doing this.
701
+ let ( stdin_reader, stdin_writer) = os_pipe:: pipe ( ) ?;
702
+ for _ in 0 ..10 {
703
+ let handle = cmd ! ( path_to_exe( "cat" ) )
704
+ . stdin_file ( stdin_reader. try_clone ( ) ?)
705
+ . stdout_file ( stdout_writer. try_clone ( ) ?)
706
+ . start ( ) ?;
707
+ child_pids. push ( handle. pids ( ) [ 0 ] ) ;
708
+ child_handles. push ( handle) ;
709
+ }
710
+ drop ( stdin_reader) ;
711
+ drop ( stdout_writer) ;
712
+
713
+ // Drop all the handles. The first 10 children will probably get reaped at this point without
714
+ // spawning a thread. The last 10 children definitely have not exited, and each of them will
715
+ // get a waiter thread.
716
+ drop ( child_handles) ;
717
+
718
+ // Drop the stdin writer. Now the last 10 children will begin exiting.
719
+ drop ( stdin_writer) ;
720
+
721
+ // Read the stdout pipe to EOF. This means all the children have exited. It's not a *guarantee*
722
+ // that `ps` won't still observe them, but this plus a few `ps` retries should be good enough.
723
+ // If this test starts spuriously failing, I'll need to double check this assumption.
724
+ let mut stdout_bytes = Vec :: new ( ) ;
725
+ stdout_reader. read_to_end ( & mut stdout_bytes) ?;
726
+ assert_eq ! ( stdout_bytes. len( ) , 0 , "no output expected" ) ;
727
+
728
+ // Assert that all the children get cleaned up. This is a Unix-only test, so we can just shell
729
+ // out to `ps`.
730
+ for ( i, pid) in child_pids. into_iter ( ) . enumerate ( ) {
731
+ eprintln ! ( "checking child #{i} (PID {pid})" ) ;
732
+ // Retry `ps` 100 times for each child, to be as confident as possible that the child has
733
+ // time to get reaped.
734
+ let mut tries = 0 ;
735
+ while ps_observes_pid ( pid) ? {
736
+ tries += 1 ;
737
+ assert ! ( tries < 100 , "PID never went away?" ) ;
738
+ }
739
+ }
740
+
741
+ Ok ( ( ) )
742
+ }
0 commit comments