use crate::debug::debug;
use crate::errors::{Result, StandbyError};
use crate::signals::{Signal, SignalHandler};
use crate::terminal::TerminalGuard;
use crate::time::parse_duration;
use clap::Parser;
use std::process::Command;
use std::thread;
use std::time::Duration;
#[derive(Parser)]
pub struct TimeoutArgs {
pub duration: String,
pub command: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub args: Vec<String>,
#[arg(short = 's', long, default_value = "TERM")]
pub signal: String,
#[arg(short = 'k', long)]
pub kill_after: Option<String>,
#[arg(long)]
pub preserve_status: bool,
#[arg(long)]
pub foreground: bool,
#[arg(short = 'v', long)]
pub verbose: bool,
}
pub fn execute(args: TimeoutArgs) -> Result<()> {
crate::debug::init_verbose(args.verbose);
let _terminal = TerminalGuard::new();
debug!("Timeout command started");
debug!("Duration: {}", args.duration);
debug!("Command: {} {:?}", args.command, args.args);
let timeout_duration = parse_duration(&args.duration)?;
let timeout_std = timeout_duration.to_std_duration();
debug!("Parsed timeout: {:.3}s", timeout_std.as_secs_f64());
#[cfg(unix)]
if !args.foreground {
setup_tty_signals()?;
}
let mut command = Command::new(&args.command);
command.args(&args.args);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
if !args.foreground {
command.process_group(0);
}
}
let mut child = command.spawn().map_err(|e| {
StandbyError::ProcessError(format!("Failed to spawn command '{}': {}", args.command, e))
})?;
let child_id = child.id();
debug!("Child process spawned with PID: {}", child_id);
let signal = parse_signal(&args.signal)?;
debug!(
"Primary signal: {} ({})",
args.signal,
signal_to_number(&signal)
);
let kill_after = if let Some(k) = args.kill_after {
let duration = parse_duration(&k)?.to_std_duration();
debug!("Kill-after enabled: {:.3}s", duration.as_secs_f64());
Some(duration)
} else {
None
};
let start = std::time::Instant::now();
debug!("Starting timeout wait loop");
loop {
match child.try_wait() {
Ok(Some(status)) => {
if !args.preserve_status {
std::process::exit(status.code().unwrap_or(0));
}
return Ok(());
}
Ok(None) => {
if start.elapsed() >= timeout_std {
let elapsed = start.elapsed();
debug!(
"Timeout reached at {:.3}s, sending signal {} to pid {}",
elapsed.as_secs_f64(),
args.signal,
child_id
);
eprintln!(
"timeout: sending signal {} to pid {}",
args.signal, child_id
);
SignalHandler::send_signal(&child, signal)
.map_err(|_| {
})
.ok();
if let Some(kill_duration) = kill_after {
let kill_start = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => {
if !args.preserve_status {
std::process::exit(status.code().unwrap_or(1));
}
return Ok(());
}
Ok(None) => {
if kill_start.elapsed() >= kill_duration {
debug!(
"Kill-after timeout reached at {:.3}s, sending SIGKILL to pid {}",
kill_start.elapsed().as_secs_f64(),
child_id
);
eprintln!("timeout: sending SIGKILL to pid {}", child_id);
SignalHandler::send_signal(&child, Signal::Kill).ok();
thread::sleep(Duration::from_millis(100));
}
thread::sleep(Duration::from_millis(10));
}
Err(e) => {
return Err(StandbyError::ProcessError(format!(
"Error waiting for process: {}",
e
)));
}
}
}
} else {
match child.wait() {
Ok(status) => {
if !args.preserve_status {
std::process::exit(status.code().unwrap_or(1));
}
return Ok(());
}
Err(e) => {
return Err(StandbyError::ProcessError(format!(
"Error waiting for process: {}",
e
)));
}
}
}
}
thread::sleep(Duration::from_millis(10));
}
Err(e) => {
return Err(StandbyError::ProcessError(format!(
"Failed to wait for process: {}",
e
)));
}
}
}
}
fn parse_signal(signal_str: &str) -> Result<Signal> {
match signal_str.to_uppercase().as_str() {
"TERM" | "15" => Ok(Signal::Term),
"KILL" | "9" => Ok(Signal::Kill),
"INT" | "2" => Ok(Signal::Int),
"STOP" | "19" => Ok(Signal::Stop),
"CONT" | "18" => Ok(Signal::Cont),
"TSTP" | "20" => Ok(Signal::Tstp),
"HUP" | "1" => Ok(Signal::Hup),
_ => Err(StandbyError::InvalidArgument(format!(
"Unknown signal: {} (supported: TERM, KILL, INT, STOP, CONT, TSTP, HUP)",
signal_str
))),
}
}
fn signal_to_number(signal: &Signal) -> i32 {
match signal {
Signal::Hup => 1,
Signal::Int => 2,
Signal::Term => 15,
Signal::Kill => 9,
Signal::Stop => 19,
Signal::Cont => 18,
Signal::Tstp => 20,
}
}
#[cfg(unix)]
fn setup_tty_signals() -> Result<()> {
use nix::sys::signal::{SigHandler, Signal as NixSignal, signal};
unsafe {
signal(NixSignal::SIGTTIN, SigHandler::SigIgn)
.map_err(|e| StandbyError::SignalError(format!("Failed to ignore SIGTTIN: {}", e)))?;
}
unsafe {
signal(NixSignal::SIGTTOU, SigHandler::SigIgn)
.map_err(|e| StandbyError::SignalError(format!("Failed to ignore SIGTTOU: {}", e)))?;
}
Ok(())
}