11use std:: collections:: HashMap ;
22use std:: path:: Path ;
33
4+ #[ cfg( unix) ]
5+ use std:: io:: ErrorKind ;
6+
47use pretty_assertions:: assert_eq;
58
69use crate :: spawn_pipe_process;
710use crate :: spawn_pty_process;
11+ #[ cfg( unix) ]
12+ use crate :: SpawnedProcess ;
813
914fn find_python ( ) -> Option < String > {
1015 for candidate in [ "python3" , "python" ] {
@@ -20,6 +25,27 @@ fn find_python() -> Option<String> {
2025 None
2126}
2227
28+ fn setsid_available ( ) -> bool {
29+ if cfg ! ( windows) {
30+ return false ;
31+ }
32+ std:: process:: Command :: new ( "setsid" )
33+ . arg ( "true" )
34+ . status ( )
35+ . map ( |status| status. success ( ) )
36+ . unwrap_or ( false )
37+ }
38+
39+ #[ cfg( unix) ]
40+ fn process_exists ( pid : i32 ) -> bool {
41+ let result = unsafe { libc:: kill ( pid, 0 ) } ;
42+ if result == 0 {
43+ return true ;
44+ }
45+ let err = std:: io:: Error :: last_os_error ( ) ;
46+ err. kind ( ) != ErrorKind :: NotFound
47+ }
48+
2349fn shell_command ( program : & str ) -> ( String , Vec < String > ) {
2450 if cfg ! ( windows) {
2551 let cmd = std:: env:: var ( "COMSPEC" ) . unwrap_or_else ( |_| "cmd.exe" . to_string ( ) ) ;
@@ -190,3 +216,83 @@ async fn pipe_drains_stderr_without_stdout_activity() -> anyhow::Result<()> {
190216
191217 Ok ( ( ) )
192218}
219+
220+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
221+ async fn pipe_terminate_aborts_detached_readers ( ) -> anyhow:: Result < ( ) > {
222+ if !setsid_available ( ) {
223+ eprintln ! ( "setsid not available; skipping pipe_terminate_aborts_detached_readers" ) ;
224+ return Ok ( ( ) ) ;
225+ }
226+
227+ let env_map: HashMap < String , String > = std:: env:: vars ( ) . collect ( ) ;
228+ let script =
229+ "setsid sh -c 'i=0; while [ $i -lt 200 ]; do echo tick; sleep 0.01; i=$((i+1)); done' &" ;
230+ let ( program, args) = shell_command ( script) ;
231+ let mut spawned = spawn_pipe_process ( & program, & args, Path :: new ( "." ) , & env_map, & None ) . await ?;
232+
233+ let _ = tokio:: time:: timeout (
234+ tokio:: time:: Duration :: from_millis ( 500 ) ,
235+ spawned. output_rx . recv ( ) ,
236+ )
237+ . await
238+ . map_err ( |_| anyhow:: anyhow!( "expected detached output before terminate" ) ) ??;
239+
240+ spawned. session . terminate ( ) ;
241+ let mut post_rx = spawned. session . output_receiver ( ) ;
242+
243+ let post_terminate =
244+ tokio:: time:: timeout ( tokio:: time:: Duration :: from_millis ( 200 ) , post_rx. recv ( ) ) . await ;
245+
246+ match post_terminate {
247+ Err ( _) => Ok ( ( ) ) ,
248+ Ok ( Err ( tokio:: sync:: broadcast:: error:: RecvError :: Closed ) ) => Ok ( ( ) ) ,
249+ Ok ( Err ( tokio:: sync:: broadcast:: error:: RecvError :: Lagged ( _) ) ) => {
250+ anyhow:: bail!( "unexpected output after terminate (lagged)" )
251+ }
252+ Ok ( Ok ( chunk) ) => anyhow:: bail!(
253+ "unexpected output after terminate: {:?}" ,
254+ String :: from_utf8_lossy( & chunk)
255+ ) ,
256+ }
257+ }
258+
259+ #[ cfg( unix) ]
260+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
261+ async fn pipe_terminate_kills_process_group_after_exit ( ) -> anyhow:: Result < ( ) > {
262+ let env_map: HashMap < String , String > = std:: env:: vars ( ) . collect ( ) ;
263+ let ( program, args) = shell_command ( "sleep 9999 & echo $!" ) ;
264+ let SpawnedProcess {
265+ session,
266+ output_rx,
267+ exit_rx,
268+ } = spawn_pipe_process ( & program, & args, Path :: new ( "." ) , & env_map, & None ) . await ?;
269+
270+ let ( output, code) = collect_output_until_exit ( output_rx, exit_rx, 2_000 ) . await ;
271+ assert_eq ! ( code, 0 , "expected shell to exit cleanly" ) ;
272+
273+ let pid_line = String :: from_utf8_lossy ( & output)
274+ . lines ( )
275+ . find ( |line| !line. trim ( ) . is_empty ( ) )
276+ . unwrap_or ( "" )
277+ . trim ( )
278+ . to_string ( ) ;
279+ let pid: i32 = pid_line
280+ . parse ( )
281+ . map_err ( |_| anyhow:: anyhow!( "failed to parse background pid from {pid_line:?}" ) ) ?;
282+
283+ session. terminate ( ) ;
284+
285+ let deadline = tokio:: time:: Instant :: now ( ) + tokio:: time:: Duration :: from_millis ( 500 ) ;
286+ while tokio:: time:: Instant :: now ( ) < deadline && process_exists ( pid) {
287+ tokio:: time:: sleep ( tokio:: time:: Duration :: from_millis ( 20 ) ) . await ;
288+ }
289+
290+ if process_exists ( pid) {
291+ unsafe {
292+ libc:: kill ( pid, libc:: SIGKILL ) ;
293+ }
294+ anyhow:: bail!( "background pid still alive after terminate: {pid}" ) ;
295+ }
296+
297+ Ok ( ( ) )
298+ }
0 commit comments