diff --git a/Cargo.toml b/Cargo.toml index 76d3eb79f..8cb645c17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -187,6 +187,10 @@ path = "tee/tee.rs" name = "test" path = "test/test.rs" +[[bin]] +name = "timeout" +path = "timeout/timeout.rs" + [[bin]] name = "touch" path = "touch/touch.rs" diff --git a/Makefile b/Makefile index 6f4651d70..88baaf4ca 100644 --- a/Makefile +++ b/Makefile @@ -93,6 +93,7 @@ UNIX_PROGS := \ logname \ mkfifo \ nohup \ + timeout \ tty \ uname \ uptime \ diff --git a/README.md b/README.md index c75155f10..20f04ab5e 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,6 @@ To do - stty - tail (not all features implemented) - test (not all features implemented) -- timeout - unexpand - uniq (in progress) - who diff --git a/src/common/c_types.rs b/src/common/c_types.rs index ecc3bd4a3..1cc014fd6 100644 --- a/src/common/c_types.rs +++ b/src/common/c_types.rs @@ -193,7 +193,6 @@ pub fn get_groups() -> Result, int> { } pub fn group(possible_pw: Option, nflag: bool) { - let groups = match possible_pw { Some(pw) => Ok(get_group_list(pw.pw_name, pw.pw_gid)), None => get_groups(), diff --git a/src/kill/signals.rs b/src/common/signals.rs similarity index 92% rename from src/kill/signals.rs rename to src/common/signals.rs index 70b7a55d1..828f7ad3a 100644 --- a/src/kill/signals.rs +++ b/src/common/signals.rs @@ -23,7 +23,7 @@ Linux Programmer's Manual 15 TERM 16 STKFLT 17 CHLD 18 CONT 19 STOP 20 TSTP 21 TTIN 22 TTOU 23 URG 24 XCPU 25 XFSZ 26 VTALRM 27 PROF 28 WINCH 29 POLL 30 PWR 31 SYS - + */ @@ -139,6 +139,19 @@ pub static ALL_SIGNALS:[Signal<'static>, ..31] = [ Signal{ name: "USR2", value:31 }, ]; +pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option { + if signal_name_or_value == "0" { + return Some(0); + } + for signal in ALL_SIGNALS.iter() { + let long_name = format!("SIG{}", signal.name); + if signal.name == signal_name_or_value || (signal_name_or_value == signal.value.to_string().as_slice()) || (long_name.as_slice() == signal_name_or_value) { + return Some(signal.value); + } + } + None +} + #[inline(always)] pub fn is_signal(num: uint) -> bool { num < ALL_SIGNALS.len() diff --git a/src/common/time.rs b/src/common/time.rs new file mode 100644 index 000000000..77c659e92 --- /dev/null +++ b/src/common/time.rs @@ -0,0 +1,35 @@ +/* + * This file is part of the uutils coreutils package. + * + * (c) Arcterus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +pub fn from_str(string: &str) -> Result { + let len = string.len(); + if len == 0 { + return Err("empty string".to_string()) + } + let slice = string.slice_to(len - 1); + let (numstr, times) = match string.char_at(len - 1) { + 's' | 'S' => (slice, 1u), + 'm' | 'M' => (slice, 60u), + 'h' | 'H' => (slice, 60u * 60), + 'd' | 'D' => (slice, 60u * 60 * 24), + val => { + if !val.is_alphabetic() { + (string, 1) + } else if string == "inf" || string == "infinity" { + ("inf", 1) + } else { + return Err(format!("invalid time interval '{}'", string)) + } + } + }; + match ::std::from_str::from_str::(numstr) { + Some(m) => Ok(m * times as f64), + None => Err(format!("invalid time interval '{}'", string)) + } +} diff --git a/src/common/util.rs b/src/common/util.rs index a0f95593a..95f86a0fe 100644 --- a/src/common/util.rs +++ b/src/common/util.rs @@ -71,6 +71,19 @@ macro_rules! crash_if_err( ) ) +#[macro_export] +macro_rules! return_if_err( + ($exitcode:expr, $exp:expr) => ( + match $exp { + Ok(m) => m, + Err(f) => { + show_error!("{}", f); + return $exitcode; + } + } + ) +) + #[macro_export] macro_rules! safe_write( ($fd:expr, $($args:expr),+) => ( diff --git a/src/kill/kill.rs b/src/kill/kill.rs index aad1caeab..daec1c6b7 100644 --- a/src/kill/kill.rs +++ b/src/kill/kill.rs @@ -32,11 +32,12 @@ use getopts::{ use signals::ALL_SIGNALS; -mod signals; - #[path = "../common/util.rs"] mod util; +#[path = "../common/signals.rs"] +mod signals; + static NAME: &'static str = "kill"; static VERSION: &'static str = "0.0.1"; @@ -183,22 +184,9 @@ fn help(progname: &str, usage: &str) { println!("{}", get_help_text(progname, usage)); } -fn signal_by_name_or_value(signal_name_or_value: &str) -> Option { - if signal_name_or_value == "0" { - return Some(0); - } - for signal in ALL_SIGNALS.iter() { - let long_name = format!("SIG{}", signal.name); - if signal.name == signal_name_or_value || (signal_name_or_value == signal.value.to_string().as_slice()) || (long_name.as_slice() == signal_name_or_value) { - return Some(signal.value); - } - } - None -} - fn kill(signalname: &str, pids: std::vec::Vec) -> int { let mut status = 0; - let optional_signal_value = signal_by_name_or_value(signalname); + let optional_signal_value = signals::signal_by_name_or_value(signalname); let signal_value = match optional_signal_value { Some(x) => x, None => crash!(EXIT_ERR, "unknown signal name {}", signalname) diff --git a/src/sleep/sleep.rs b/src/sleep/sleep.rs index ca3468273..04ec5ef72 100644 --- a/src/sleep/sleep.rs +++ b/src/sleep/sleep.rs @@ -21,6 +21,9 @@ use std::u64; #[path = "../common/util.rs"] mod util; +#[path = "../common/time.rs"] +mod time; + static NAME: &'static str = "sleep"; pub fn uumain(args: Vec) -> int { @@ -66,43 +69,13 @@ specified by the sum of their values.", opts).as_slice()); fn sleep(args: Vec) { let sleep_time = args.iter().fold(0.0, |result, arg| { - let (arg, suffix_time) = match match_suffix(arg.as_slice()) { + let num = match time::from_str(arg.as_slice()) { Ok(m) => m, Err(f) => { - crash!(1, "{}", f.to_string()) + crash!(1, "{}", f) } }; - let num = - if suffix_time == 0 { - 0.0 - } else { - match from_str::(arg.as_slice()) { - Some(m) => m, - None => { - crash!(1, "Invalid time interval '{}'", arg.to_string()) - } - } - }; - result + num * suffix_time as f64 + result + num }); timer::sleep(if sleep_time == f64::INFINITY { u64::MAX } else { (sleep_time * 1000.0) as u64 }); } - -fn match_suffix(arg: &str) -> Result<(String, int), String> { - let result = match (arg).char_at_reverse(0) { - 's' | 'S' => 1, - 'm' | 'M' => 60, - 'h' | 'H' => 60 * 60, - 'd' | 'D' => 60 * 60 * 24, - val => { - if !val.is_alphabetic() { - return Ok((arg.to_string(), 1)) - } else if arg == "inf" || arg == "infinity" { - return Ok(("inf".to_string(), 1)) - } else { - return Err(format!("Invalid time interval '{}'", arg)) - } - } - }; - Ok(((arg).slice_to((arg).len() - 1).to_string(), result)) -} diff --git a/src/timeout/timeout.rs b/src/timeout/timeout.rs new file mode 100644 index 000000000..eab5a464b --- /dev/null +++ b/src/timeout/timeout.rs @@ -0,0 +1,158 @@ +#![crate_name = "timeout"] + +/* + * This file is part of the uutils coreutils package. + * + * (c) Arcterus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#![feature(macro_rules)] + +extern crate getopts; +extern crate libc; + +use std::io::{PathDoesntExist, FileNotFound}; +use std::io::process::{Command, ExitStatus, ExitSignal, InheritFd}; + +#[path = "../common/util.rs"] +mod util; + +#[path = "../common/time.rs"] +mod time; + +#[path = "../common/signals.rs"] +mod signals; + +extern { + pub fn setpgid(_: libc::pid_t, _: libc::pid_t) -> libc::c_int; +} + +static NAME: &'static str = "timeout"; +static VERSION: &'static str = "1.0.0"; + +static ERR_EXIT_STATUS: int = 125; + +pub fn uumain(args: Vec) -> int { + let program = args[0].clone(); + + let opts = [ + getopts::optflag("", "preserve-status", "exit with the same status as COMMAND, even when the command times out"), + getopts::optflag("", "foreground", "when not running timeout directly from a shell prompt, allow COMMAND to read from the TTY and get TTY signals; in this mode, children of COMMAND will not be timed out"), + getopts::optopt("k", "kill-after", "also send a KILL signal if COMMAND is still running this long after the initial signal was sent", "DURATION"), + getopts::optflag("s", "signal", "specify the signal to be sent on timeout; SIGNAL may be a name like 'HUP' or a number; see 'kill -l' for a list of signals"), + getopts::optflag("h", "help", "display this help and exit"), + getopts::optflag("V", "version", "output version information and exit") + ]; + let matches = match getopts::getopts(args.tail(), opts) { + Ok(m) => m, + Err(f) => { + crash!(ERR_EXIT_STATUS, "{}", f) + } + }; + if matches.opt_present("help") { + print!("{} v{} + +Usage: + {} [OPTION] DURATION COMMAND [ARG]... + +{}", NAME, VERSION, program, getopts::usage("Start COMMAND, and kill it if still running after DURATION.", opts)); + } else if matches.opt_present("version") { + println!("{} v{}", NAME, VERSION); + } else if matches.free.len() < 2 { + show_error!("missing an argument"); + show_error!("for help, try '{0:s} --help'", program); + return ERR_EXIT_STATUS; + } else { + let status = matches.opt_present("preserve-status"); + let foreground = matches.opt_present("foreground"); + let kill_after = match matches.opt_str("kill-after") { + Some(tstr) => match time::from_str(tstr.as_slice()) { + Ok(time) => time, + Err(f) => { + show_error!("{}", f); + return ERR_EXIT_STATUS; + } + }, + None => 0f64 + }; + let signal = match matches.opt_str("signal") { + Some(sigstr) => match signals::signal_by_name_or_value(sigstr.as_slice()) { + Some(sig) => sig, + None => { + show_error!("invalid signal '{}'", sigstr); + return ERR_EXIT_STATUS; + } + }, + None => signals::signal_by_name_or_value("TERM").unwrap() + }; + let duration = match time::from_str(matches.free[0].as_slice()) { + Ok(time) => time, + Err(f) => { + show_error!("{}", f); + return ERR_EXIT_STATUS; + } + }; + return timeout(matches.free[1].as_slice(), matches.free.slice_from(2), duration, signal, kill_after, foreground, status); + } + + 0 +} + +fn timeout(cmdname: &str, args: &[String], duration: f64, signal: uint, kill_after: f64, foreground: bool, preserve_status: bool) -> int { + if !foreground { + unsafe { setpgid(0, 0) }; + } + let mut process = match Command::new(cmdname).args(args) + .stdin(InheritFd(0)) + .stdout(InheritFd(1)) + .stderr(InheritFd(2)) + .spawn() { + Ok(p) => p, + Err(err) => { + show_error!("failed to execute process: {}", err); + if err.kind == FileNotFound || err.kind == PathDoesntExist { + // XXX: not sure which to use + return 127; + } else { + // XXX: this may not be 100% correct... + return 126; + } + } + }; + process.set_timeout(Some((duration * 1000f64) as u64)); // FIXME: this ignores the f64... + match process.wait() { + Ok(status) => match status { + ExitStatus(stat) => stat, + ExitSignal(stat) => stat + }, + Err(_) => { + return_if_err!(ERR_EXIT_STATUS, process.signal(signal as int)); + process.set_timeout(Some((kill_after * 1000f64) as u64)); + match process.wait() { + Ok(status) => { + if preserve_status { + match status { + ExitStatus(stat) => stat, + ExitSignal(stat) => stat + } + } else { + 124 + } + } + Err(_) => { + if kill_after == 0f64 { + // XXX: this may not be right + return 124; + } + return_if_err!(ERR_EXIT_STATUS, process.signal(signals::signal_by_name_or_value("KILL").unwrap() as int)); + process.set_timeout(None); + return_if_err!(ERR_EXIT_STATUS, process.wait()); + 137 + } + } + } + } +}