@@ -407,29 +407,35 @@ async fn run_command_loop(
407407 state. stderr_read_buffer. clear( ) ; // Clear after processing
408408 }
409409
410- // 4. Check for timeout
411- _ = & mut deadline_sleep, if can_check_timeout => {
412- match handle_timeout_event(
413- & mut state. child,
414- state. current_deadline,
415- timeouts
416- ) . await {
417- Ok ( Some ( status) ) => { // Process exited just before kill
418- debug!( "Timeout detected but process already exited." ) ;
419- state. exit_status = Some ( status) ;
420- state. timed_out = false ; // Not actually killed by us
421- }
422- Ok ( None ) => { // Timeout occurred, kill attempted/succeeded.
423- state. timed_out = true ;
424- }
425- Err ( e) => { // Error during kill or subsequent check
426- return Err ( e) ;
427- }
428- }
429- break ; // Exit loop after timeout event
430- }
410+
411+ // 4. Check for timeout
412+ _ = & mut deadline_sleep, if can_check_timeout => {
413+ let now = Instant :: now( ) ;
414+ let triggered_deadline = if now >= timeouts. absolute_deadline {
415+ debug!( "Absolute deadline exceeded. Triggering timeout." ) ;
416+ timeouts. absolute_deadline
417+ } else {
418+ debug!( "Activity timeout likely exceeded. Triggering timeout." ) ;
419+ state. current_deadline
420+ } ;
421+
422+ match handle_timeout_event( & mut state. child, triggered_deadline, timeouts) . await {
423+ Ok ( Some ( status) ) => {
424+ debug!( "Timeout detected but process already exited." ) ;
425+ state. exit_status = Some ( status) ;
426+ state. timed_out = false ; // Not actually killed by us
427+ }
428+ Ok ( None ) => {
429+ state. timed_out = true ; // Timeout occurred, kill attempted/succeeded.
431430 }
432- } // end loop
431+ Err ( e) => {
432+ return Err ( e) ; // Error during kill or subsequent check
433+ }
434+ }
435+ break ; // Exit loop after timeout event
436+ }
437+ }
438+ } // end loop
433439
434440 Ok ( ( ) )
435441}
@@ -1125,4 +1131,88 @@ fn test_run_command_loop_exits_on_timeout() {
11251131 } ) ;
11261132}
11271133
1134+
1135+ #[ test]
1136+ fn test_absolute_deadline_kills_infinite_loop_command ( ) {
1137+ run_async_test ( || async {
1138+ let mut cmd = StdCommand :: new ( "sh" ) ;
1139+ cmd. arg ( "-c" ) . arg ( "while true; do :; done" ) ; // Infinite loop
1140+
1141+ let min_timeout = Duration :: from_secs ( 1 ) ;
1142+ let max_timeout = Duration :: from_secs ( 2 ) ; // Absolute deadline of 2 seconds
1143+ let activity_timeout = Duration :: from_secs ( 10 ) ; // Irrelevant since absolute deadline is shorter
1144+
1145+ let result = run_command_with_timeout ( cmd, min_timeout, max_timeout, activity_timeout)
1146+ . await
1147+ . expect ( "Command failed unexpectedly" ) ;
1148+
1149+ assert ! ( result. stdout. is_empty( ) , "Stdout should be empty" ) ;
1150+ assert ! ( result. stderr. is_empty( ) , "Stderr should be empty" ) ;
1151+ assert ! (
1152+ result. exit_status. is_some( ) ,
1153+ "Exit status should be Some after kill"
1154+ ) ;
1155+ // SIGKILL is signal 9
1156+ assert_eq ! (
1157+ result. exit_status. unwrap( ) . signal( ) ,
1158+ Some ( libc:: SIGKILL as i32 ) ,
1159+ "Should be killed by SIGKILL"
1160+ ) ;
1161+ assert ! ( result. timed_out, "Should have timed out" ) ;
1162+ assert ! (
1163+ result. duration >= max_timeout,
1164+ "Duration should be >= max_timeout"
1165+ ) ;
1166+ assert ! (
1167+ result. duration < max_timeout + Duration :: from_millis( 750 ) ,
1168+ "Duration should allow a small buffer for process group kill and reaping"
1169+ ) ;
1170+ } ) ;
1171+ }
1172+
1173+ #[ test]
1174+ fn test_infinite_output_command ( ) {
1175+ run_async_test ( || async {
1176+ let mut cmd = StdCommand :: new ( "yes" ) ;
1177+ cmd. arg ( "infinite" ) ;
1178+
1179+ let min_timeout = Duration :: from_secs ( 1 ) ;
1180+ let max_timeout = Duration :: from_secs ( 2 ) ; // Absolute deadline of 2 seconds
1181+ let activity_timeout = Duration :: from_secs ( 1 ) ; // Activity timeout of 1 second
1182+
1183+ let result = run_command_with_timeout ( cmd, min_timeout, max_timeout, activity_timeout)
1184+ . await
1185+ . expect ( "Command failed unexpectedly" ) ;
1186+
1187+ assert ! (
1188+ !result. stdout. is_empty( ) ,
1189+ "Stdout should not be empty for infinite output"
1190+ ) ;
1191+ assert ! (
1192+ result. stderr. is_empty( ) ,
1193+ "Stderr should be empty for the `yes` command"
1194+ ) ;
1195+ assert ! (
1196+ result. exit_status. is_some( ) ,
1197+ "Exit status should be Some after timeout"
1198+ ) ;
1199+ // SIGKILL is signal 9
1200+ assert_eq ! (
1201+ result. exit_status. unwrap( ) . signal( ) ,
1202+ Some ( libc:: SIGKILL as i32 ) ,
1203+ "Should be killed by SIGKILL"
1204+ ) ;
1205+ assert ! ( result. timed_out, "Should have timed out" ) ;
1206+ assert ! (
1207+ result. duration >= max_timeout,
1208+ "Duration should be >= max_timeout"
1209+ ) ;
1210+ assert ! (
1211+ result. duration < max_timeout + Duration :: from_millis( 750 ) ,
1212+ "Duration should allow a small buffer for process group kill and reaping"
1213+ ) ;
1214+ } ) ;
1215+ }
1216+
1217+
11281218} // end tests mod
0 commit comments