@@ -9,8 +9,8 @@ mod status;
99
1010use crate :: status:: ExitStatus ;
1111use clap:: { Arg , ArgAction , Command } ;
12- use std:: io:: ErrorKind ;
13- use std:: os:: unix:: process:: { CommandExt , ExitStatusExt } ;
12+ use std:: io:: { ErrorKind , Write } ;
13+ use std:: os:: unix:: process:: ExitStatusExt ;
1414use std:: process:: { self , Child , Stdio } ;
1515use std:: sync:: atomic:: { self , AtomicBool } ;
1616use std:: time:: Duration ;
@@ -21,12 +21,14 @@ use uucore::process::ChildExt;
2121use uucore:: translate;
2222
2323use uucore:: {
24- format_usage, show_error ,
24+ format_usage,
2525 signals:: { signal_by_name_or_value, signal_name_by_value} ,
2626} ;
2727
28- use nix:: sys:: signal:: { Signal , kill} ;
28+ use nix:: sys:: signal:: { SigHandler , Signal , kill} ;
2929use nix:: unistd:: { Pid , getpid, setpgid} ;
30+ #[ cfg( unix) ]
31+ use std:: os:: unix:: process:: CommandExt ;
3032
3133pub mod options {
3234 pub static FOREGROUND : & str = "foreground" ;
@@ -177,32 +179,46 @@ pub fn uu_app() -> Command {
177179 . after_help ( translate ! ( "timeout-after-help" ) )
178180}
179181
180- /// Remove pre-existing SIGCHLD handlers that would make waiting for the child's exit code fail.
181- fn unblock_sigchld ( ) {
182- unsafe {
183- nix:: sys:: signal:: signal (
184- nix:: sys:: signal:: Signal :: SIGCHLD ,
185- nix:: sys:: signal:: SigHandler :: SigDfl ,
186- )
187- . unwrap ( ) ;
188- }
182+ /// Install SIGCHLD handler to ensure waiting for child works even if parent ignored SIGCHLD.
183+ fn install_sigchld ( ) {
184+ extern "C" fn chld ( _: libc:: c_int ) { }
185+ let _ = unsafe { nix:: sys:: signal:: signal ( Signal :: SIGCHLD , SigHandler :: Handler ( chld) ) } ;
189186}
190187
191- /// We should terminate child process when receiving TERM signal .
188+ /// We should terminate child process when receiving termination signals .
192189static SIGNALED : AtomicBool = AtomicBool :: new ( false ) ;
190+ /// Track which signal was received (0 = none/timeout expired naturally).
191+ static RECEIVED_SIGNAL : std:: sync:: atomic:: AtomicI32 = std:: sync:: atomic:: AtomicI32 :: new ( 0 ) ;
192+
193+ /// Install signal handlers for termination signals.
194+ fn install_signal_handlers ( term_signal : usize ) {
195+ extern "C" fn handle_signal ( sig : libc:: c_int ) {
196+ SIGNALED . store ( true , atomic:: Ordering :: Relaxed ) ;
197+ RECEIVED_SIGNAL . store ( sig, atomic:: Ordering :: Relaxed ) ;
198+ }
193199
194- fn catch_sigterm ( ) {
195- use nix:: sys:: signal;
196-
197- extern "C" fn handle_sigterm ( signal : libc:: c_int ) {
198- let signal = signal:: Signal :: try_from ( signal) . unwrap ( ) ;
199- if signal == signal:: Signal :: SIGTERM {
200- SIGNALED . store ( true , atomic:: Ordering :: Relaxed ) ;
200+ let handler = SigHandler :: Handler ( handle_signal) ;
201+ let sigpipe_ignored = uucore:: signals:: sigpipe_was_ignored ( ) ;
202+
203+ for sig in [
204+ Signal :: SIGALRM ,
205+ Signal :: SIGINT ,
206+ Signal :: SIGQUIT ,
207+ Signal :: SIGHUP ,
208+ Signal :: SIGTERM ,
209+ Signal :: SIGPIPE ,
210+ Signal :: SIGUSR1 ,
211+ Signal :: SIGUSR2 ,
212+ ] {
213+ if sig == Signal :: SIGPIPE && sigpipe_ignored {
214+ continue ; // Skip SIGPIPE if it was ignored by parent
201215 }
216+ let _ = unsafe { nix:: sys:: signal:: signal ( sig, handler) } ;
202217 }
203218
204- let handler = signal:: SigHandler :: Handler ( handle_sigterm) ;
205- unsafe { signal:: signal ( signal:: Signal :: SIGTERM , handler) } . unwrap ( ) ;
219+ if let Ok ( sig) = Signal :: try_from ( term_signal as i32 ) {
220+ let _ = unsafe { nix:: sys:: signal:: signal ( sig, handler) } ;
221+ }
206222}
207223
208224/// Report that a signal is being sent if the verbose flag is set.
@@ -213,26 +229,29 @@ fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
213229 } else {
214230 signal_name_by_value ( signal) . unwrap ( ) . to_string ( )
215231 } ;
216- show_error ! (
217- "{}" ,
232+ let mut stderr = std:: io:: stderr ( ) ;
233+ let _ = writeln ! (
234+ stderr,
235+ "timeout: {}" ,
218236 translate!( "timeout-verbose-sending-signal" , "signal" => s, "command" => cmd. quote( ) )
219237 ) ;
238+ let _ = stderr. flush ( ) ;
220239 }
221240}
222241
223242fn send_signal ( process : & mut Child , signal : usize , foreground : bool ) {
224243 // NOTE: GNU timeout doesn't check for errors of signal.
225244 // The subprocess might have exited just after the timeout.
226- // Sending a signal now would return "No such process", but we should still try to kill the children.
227- if foreground {
228- let _ = process . send_signal ( signal ) ;
229- } else {
230- let _ = process. send_signal_group ( signal) ;
231- let kill_signal = signal_by_name_or_value ( "KILL" ) . unwrap ( ) ;
232- let continued_signal = signal_by_name_or_value ( "CONT" ) . unwrap ( ) ;
233- if signal != kill_signal && signal != continued_signal {
234- _ = process. send_signal_group ( continued_signal) ;
235- }
245+ let _ = process. send_signal ( signal ) ;
246+ if signal == 0 || foreground {
247+ return ;
248+ }
249+ let _ = process. send_signal_group ( signal) ;
250+ let kill_signal = signal_by_name_or_value ( "KILL" ) . unwrap ( ) ;
251+ let continued_signal = signal_by_name_or_value ( "CONT" ) . unwrap ( ) ;
252+ if signal != kill_signal && signal != continued_signal {
253+ let _ = process. send_signal ( continued_signal) ;
254+ let _ = process . send_signal_group ( continued_signal ) ;
236255 }
237256}
238257
@@ -330,24 +349,46 @@ fn timeout(
330349 let _ = setpgid ( Pid :: from_raw ( 0 ) , Pid :: from_raw ( 0 ) ) ;
331350 }
332351
333- let mut command = process:: Command :: new ( & cmd[ 0 ] ) ;
334- command
352+ let mut cmd_builder = process:: Command :: new ( & cmd[ 0 ] ) ;
353+ cmd_builder
335354 . args ( & cmd[ 1 ..] )
336355 . stdin ( Stdio :: inherit ( ) )
337356 . stdout ( Stdio :: inherit ( ) )
338357 . stderr ( Stdio :: inherit ( ) ) ;
339358
340- // If stdin was closed before Rust reopened it as /dev/null, close it in child
341- if uucore:: signals:: stdin_was_closed ( ) {
359+ #[ cfg( unix) ]
360+ {
361+ #[ cfg( target_os = "linux" ) ]
362+ let death_sig = Signal :: try_from ( signal as i32 ) . ok ( ) ;
363+ let sigpipe_was_ignored = uucore:: signals:: sigpipe_was_ignored ( ) ;
364+ let stdin_was_closed = uucore:: signals:: stdin_was_closed ( ) ;
365+
342366 unsafe {
343- command. pre_exec ( || {
344- libc:: close ( libc:: STDIN_FILENO ) ;
367+ cmd_builder. pre_exec ( move || {
368+ // Reset terminal signals to default
369+ let _ = nix:: sys:: signal:: signal ( Signal :: SIGTTIN , SigHandler :: SigDfl ) ;
370+ let _ = nix:: sys:: signal:: signal ( Signal :: SIGTTOU , SigHandler :: SigDfl ) ;
371+ // Preserve SIGPIPE ignore status if parent had it ignored
372+ if sigpipe_was_ignored {
373+ let _ = nix:: sys:: signal:: signal ( Signal :: SIGPIPE , SigHandler :: SigIgn ) ;
374+ }
375+ // If stdin was closed before Rust reopened it as /dev/null, close it in child
376+ if stdin_was_closed {
377+ libc:: close ( libc:: STDIN_FILENO ) ;
378+ }
379+ #[ cfg( target_os = "linux" ) ]
380+ if let Some ( sig) = death_sig {
381+ let _ = nix:: sys:: prctl:: set_pdeathsig ( sig) ;
382+ }
345383 Ok ( ( ) )
346384 } ) ;
347385 }
348386 }
349387
350- let process = & mut command. spawn ( ) . map_err ( |err| {
388+ install_sigchld ( ) ;
389+ install_signal_handlers ( signal) ;
390+
391+ let process = & mut cmd_builder. spawn ( ) . map_err ( |err| {
351392 let status_code = match err. kind ( ) {
352393 ErrorKind :: NotFound => ExitStatus :: CommandNotFound . into ( ) ,
353394 ErrorKind :: PermissionDenied => ExitStatus :: CannotInvoke . into ( ) ,
@@ -358,8 +399,7 @@ fn timeout(
358399 translate ! ( "timeout-error-failed-to-execute-process" , "error" => err) ,
359400 )
360401 } ) ?;
361- unblock_sigchld ( ) ;
362- catch_sigterm ( ) ;
402+
363403 // Wait for the child process for the specified time period.
364404 //
365405 // If the process exits within the specified time period (the
@@ -381,41 +421,51 @@ fn timeout(
381421 Err ( exit_code. into ( ) )
382422 }
383423 Ok ( None ) => {
384- report_if_verbose ( signal, & cmd[ 0 ] , verbose) ;
385- send_signal ( process, signal, foreground) ;
386- match kill_after {
387- None => {
388- let status = process. wait ( ) ?;
389- if SIGNALED . load ( atomic:: Ordering :: Relaxed ) {
390- Err ( ExitStatus :: Terminated . into ( ) )
391- } else if preserve_status {
392- if let Some ( ec) = status. code ( ) {
393- Err ( ec. into ( ) )
394- } else if let Some ( sc) = status. signal ( ) {
395- Err ( ExitStatus :: SignalSent ( sc. try_into ( ) . unwrap ( ) ) . into ( ) )
396- } else {
397- Err ( ExitStatus :: CommandTimedOut . into ( ) )
398- }
399- } else {
400- Err ( ExitStatus :: CommandTimedOut . into ( ) )
401- }
402- }
403- Some ( kill_after) => {
404- match wait_or_kill_process (
405- process,
406- & cmd[ 0 ] ,
407- kill_after,
408- preserve_status,
409- foreground,
410- verbose,
411- ) {
412- Ok ( status) => Err ( status. into ( ) ) ,
413- Err ( e) => Err ( USimpleError :: new (
414- ExitStatus :: TimeoutFailed . into ( ) ,
415- e. to_string ( ) ,
416- ) ) ,
417- }
418- }
424+ let received_sig = RECEIVED_SIGNAL . load ( atomic:: Ordering :: Relaxed ) ;
425+ let is_external_signal = received_sig > 0 && received_sig != libc:: SIGALRM ;
426+ let signal_to_send = if is_external_signal {
427+ received_sig as usize
428+ } else {
429+ signal
430+ } ;
431+
432+ report_if_verbose ( signal_to_send, & cmd[ 0 ] , verbose) ;
433+ send_signal ( process, signal_to_send, foreground) ;
434+
435+ if let Some ( kill_after) = kill_after {
436+ return match wait_or_kill_process (
437+ process,
438+ & cmd[ 0 ] ,
439+ kill_after,
440+ preserve_status,
441+ foreground,
442+ verbose,
443+ ) {
444+ Ok ( status) => Err ( status. into ( ) ) ,
445+ Err ( e) => Err ( USimpleError :: new (
446+ ExitStatus :: TimeoutFailed . into ( ) ,
447+ e. to_string ( ) ,
448+ ) ) ,
449+ } ;
450+ }
451+
452+ let status = process. wait ( ) ?;
453+ if is_external_signal {
454+ Err ( ExitStatus :: SignalSent ( received_sig as usize ) . into ( ) )
455+ } else if SIGNALED . load ( atomic:: Ordering :: Relaxed ) {
456+ Err ( ExitStatus :: CommandTimedOut . into ( ) )
457+ } else if preserve_status {
458+ Err ( status
459+ . code ( )
460+ . or_else ( || {
461+ status
462+ . signal ( )
463+ . map ( |s| ExitStatus :: SignalSent ( s as usize ) . into ( ) )
464+ } )
465+ . unwrap_or ( ExitStatus :: CommandTimedOut . into ( ) )
466+ . into ( ) )
467+ } else {
468+ Err ( ExitStatus :: CommandTimedOut . into ( ) )
419469 }
420470 }
421471 Err ( _) => {
0 commit comments