Skip to content

Commit ad7edde

Browse files
authored
Enforce Absolute Timeout in run_command_loop
2 parents 7f28974 + 7b438ef commit ad7edde

File tree

1 file changed

+112
-22
lines changed

1 file changed

+112
-22
lines changed

src/lib.rs

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)