Implement timeout (resolves #364)

This commit is contained in:
Arcterus 2014-07-21 18:50:53 -07:00
parent 439a8cadd1
commit a38ee8a007
10 changed files with 235 additions and 52 deletions

View file

@ -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"

View file

@ -93,6 +93,7 @@ UNIX_PROGS := \
logname \
mkfifo \
nohup \
timeout \
tty \
uname \
uptime \

View file

@ -165,7 +165,6 @@ To do
- stty
- tail (not all features implemented)
- test (not all features implemented)
- timeout
- unexpand
- uniq (in progress)
- who

View file

@ -193,7 +193,6 @@ pub fn get_groups() -> Result<Vec<gid_t>, int> {
}
pub fn group(possible_pw: Option<c_passwd>, nflag: bool) {
let groups = match possible_pw {
Some(pw) => Ok(get_group_list(pw.pw_name, pw.pw_gid)),
None => get_groups(),

View file

@ -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<uint> {
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()

35
src/common/time.rs Normal file
View file

@ -0,0 +1,35 @@
/*
* This file is part of the uutils coreutils package.
*
* (c) Arcterus <arcterus@mail.com>
*
* 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<f64, String> {
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::<f64>(numstr) {
Some(m) => Ok(m * times as f64),
None => Err(format!("invalid time interval '{}'", string))
}
}

View file

@ -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),+) => (

View file

@ -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<uint> {
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<String>) -> 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)

View file

@ -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<String>) -> int {
@ -66,43 +69,13 @@ specified by the sum of their values.", opts).as_slice());
fn sleep(args: Vec<String>) {
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::<f64>(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))
}

158
src/timeout/timeout.rs Normal file
View file

@ -0,0 +1,158 @@
#![crate_name = "timeout"]
/*
* This file is part of the uutils coreutils package.
*
* (c) Arcterus <arcterus@mail.com>
*
* 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<String>) -> 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
}
}
}
}
}