1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

Merge pull request #2395 from miDeb/timeout/cmd-args

timeout: handle arguments for the command to run
This commit is contained in:
Terts Diepraam 2021-06-14 12:01:31 +02:00 committed by GitHub
commit 05c8883b1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 154 additions and 349 deletions

3
Cargo.lock generated
View file

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "Inflector"
version = "0.11.4"
@ -2597,6 +2599,7 @@ version = "0.0.6"
dependencies = [
"clap",
"libc",
"nix 0.20.0",
"uucore",
"uucore_procs",
]

View file

@ -129,33 +129,24 @@ fn handle_obsolete(mut args: Vec<String>) -> (Vec<String>, Option<String>) {
}
fn table() {
let mut name_width = 0;
/* Compute the maximum width of a signal name. */
for s in &ALL_SIGNALS {
if s.name.len() > name_width {
name_width = s.name.len()
}
}
let name_width = ALL_SIGNALS.iter().map(|n| n.len()).max().unwrap();
for (idx, signal) in ALL_SIGNALS.iter().enumerate() {
print!("{0: >#2} {1: <#8}", idx + 1, signal.name);
//TODO: obtain max signal width here
print!("{0: >#2} {1: <#2$}", idx, signal, name_width + 2);
if (idx + 1) % 7 == 0 {
println!();
}
}
println!()
}
fn print_signal(signal_name_or_value: &str) {
for signal in &ALL_SIGNALS {
if signal.name == signal_name_or_value
|| (format!("SIG{}", signal.name)) == signal_name_or_value
{
println!("{}", signal.value);
for (value, &signal) in ALL_SIGNALS.iter().enumerate() {
if signal == signal_name_or_value || (format!("SIG{}", signal)) == signal_name_or_value {
println!("{}", value);
exit!(EXIT_OK as i32)
} else if signal_name_or_value == signal.value.to_string() {
println!("{}", signal.name);
} else if signal_name_or_value == value.to_string() {
println!("{}", signal);
exit!(EXIT_OK as i32)
}
}
@ -165,8 +156,8 @@ fn print_signal(signal_name_or_value: &str) {
fn print_signals() {
let mut pos = 0;
for (idx, signal) in ALL_SIGNALS.iter().enumerate() {
pos += signal.name.len();
print!("{}", signal.name);
pos += signal.len();
print!("{}", signal);
if idx > 0 && pos > 73 {
println!();
pos = 0;

View file

@ -17,6 +17,7 @@ path = "src/timeout.rs"
[dependencies]
clap = "2.33"
libc = "0.2.42"
nix = "0.20.0"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["process", "signals"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -5,7 +5,7 @@
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid
// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid sigchld
#[macro_use]
extern crate uucore;
@ -17,13 +17,13 @@ use std::io::ErrorKind;
use std::process::{Command, Stdio};
use std::time::Duration;
use uucore::process::ChildExt;
use uucore::signals::signal_by_name_or_value;
use uucore::signals::{signal_by_name_or_value, signal_name_by_value};
use uucore::InvalidEncodingHandling;
static ABOUT: &str = "Start COMMAND, and kill it if still running after DURATION.";
fn get_usage() -> String {
format!("{0} [OPTION]... [FILE]...", executable!())
format!("{0} [OPTION] DURATION COMMAND...", executable!())
}
const ERR_EXIT_STATUS: i32 = 125;
@ -33,22 +33,22 @@ pub mod options {
pub static KILL_AFTER: &str = "kill-after";
pub static SIGNAL: &str = "signal";
pub static PRESERVE_STATUS: &str = "preserve-status";
pub static VERBOSE: &str = "verbose";
// Positional args.
pub static DURATION: &str = "duration";
pub static COMMAND: &str = "command";
pub static ARGS: &str = "args";
}
struct Config {
foreground: bool,
kill_after: Duration,
kill_after: Option<Duration>,
signal: usize,
duration: Duration,
preserve_status: bool,
verbose: bool,
command: String,
command_args: Vec<String>,
command: Vec<String>,
}
impl Config {
@ -66,23 +66,22 @@ impl Config {
_ => uucore::signals::signal_by_name_or_value("TERM").unwrap(),
};
let kill_after: Duration = match options.value_of(options::KILL_AFTER) {
Some(time) => uucore::parse_time::from_str(time).unwrap(),
None => Duration::new(0, 0),
};
let kill_after = options
.value_of(options::KILL_AFTER)
.map(|time| uucore::parse_time::from_str(time).unwrap());
let duration: Duration =
uucore::parse_time::from_str(options.value_of(options::DURATION).unwrap()).unwrap();
let preserve_status: bool = options.is_present(options::PRESERVE_STATUS);
let foreground = options.is_present(options::FOREGROUND);
let verbose = options.is_present(options::VERBOSE);
let command: String = options.value_of(options::COMMAND).unwrap().to_string();
let command_args: Vec<String> = match options.values_of(options::ARGS) {
Some(values) => values.map(|x| x.to_owned()).collect(),
None => vec![],
};
let command = options
.values_of(options::COMMAND)
.unwrap()
.map(String::from)
.collect::<Vec<_>>();
Config {
foreground,
@ -91,7 +90,7 @@ impl Config {
duration,
preserve_status,
command,
command_args,
verbose,
}
}
}
@ -128,6 +127,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.help("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")
.takes_value(true)
)
.arg(
Arg::with_name(options::VERBOSE)
.short("v")
.long(options::VERBOSE)
.help("diagnose to stderr any signal sent upon timeout")
)
.arg(
Arg::with_name(options::DURATION)
.index(1)
@ -137,9 +142,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
Arg::with_name(options::COMMAND)
.index(2)
.required(true)
)
.arg(
Arg::with_name(options::ARGS).multiple(true)
.multiple(true)
)
.setting(AppSettings::TrailingVarArg);
@ -148,31 +151,42 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let config = Config::from(matches);
timeout(
&config.command,
&config.command_args,
config.duration,
config.signal,
config.kill_after,
config.foreground,
config.preserve_status,
config.verbose,
)
}
/// Remove pre-existing SIGCHLD handlers that would make waiting for the child's exit code fail.
fn unblock_sigchld() {
unsafe {
nix::sys::signal::signal(
nix::sys::signal::Signal::SIGCHLD,
nix::sys::signal::SigHandler::SigDfl,
)
.unwrap();
}
}
/// TODO: Improve exit codes, and make them consistent with the GNU Coreutils exit codes.
fn timeout(
cmdname: &str,
args: &[String],
cmd: &[String],
duration: Duration,
signal: usize,
kill_after: Duration,
kill_after: Option<Duration>,
foreground: bool,
preserve_status: bool,
verbose: bool,
) -> i32 {
if !foreground {
unsafe { libc::setpgid(0, 0) };
}
let mut process = match Command::new(cmdname)
.args(args)
let mut process = match Command::new(&cmd[0])
.args(&cmd[1..])
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
@ -190,32 +204,44 @@ fn timeout(
}
}
};
unblock_sigchld();
match process.wait_or_timeout(duration) {
Ok(Some(status)) => status.code().unwrap_or_else(|| status.signal().unwrap()),
Ok(None) => {
if verbose {
show_error!(
"sending signal {} to command '{}'",
signal_name_by_value(signal).unwrap(),
cmd[0]
);
}
return_if_err!(ERR_EXIT_STATUS, process.send_signal(signal));
match process.wait_or_timeout(kill_after) {
Ok(Some(status)) => {
if preserve_status {
status.code().unwrap_or_else(|| status.signal().unwrap())
} else {
124
if let Some(kill_after) = kill_after {
match process.wait_or_timeout(kill_after) {
Ok(Some(status)) => {
if preserve_status {
status.code().unwrap_or_else(|| status.signal().unwrap())
} else {
124
}
}
}
Ok(None) => {
if kill_after == Duration::new(0, 0) {
// XXX: this may not be right
return 124;
Ok(None) => {
if verbose {
show_error!("sending signal KILL to command '{}'", cmd[0]);
}
return_if_err!(
ERR_EXIT_STATUS,
process.send_signal(
uucore::signals::signal_by_name_or_value("KILL").unwrap()
)
);
return_if_err!(ERR_EXIT_STATUS, process.wait());
137
}
return_if_err!(
ERR_EXIT_STATUS,
process
.send_signal(uucore::signals::signal_by_name_or_value("KILL").unwrap())
);
return_if_err!(ERR_EXIT_STATUS, process.wait());
137
Err(_) => 124,
}
Err(_) => 124,
} else {
124
}
}
Err(_) => {

View file

@ -93,6 +93,7 @@ pub trait ChildExt {
fn send_signal(&mut self, signal: usize) -> io::Result<()>;
/// Wait for a process to finish or return after the specified duration.
/// A `timeout` of zero disables the timeout.
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>>;
}
@ -106,6 +107,11 @@ impl ChildExt for Child {
}
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
if timeout == Duration::from_micros(0) {
return self
.wait()
.map(|status| Some(ExitStatus::from_std_status(status)));
}
// .try_wait() doesn't drop stdin, so we do it manually
drop(self.stdin.take());

View file

@ -10,11 +10,6 @@
pub static DEFAULT_SIGNAL: usize = 15;
pub struct Signal<'a> {
pub name: &'a str,
pub value: usize,
}
/*
Linux Programmer's Manual
@ -29,131 +24,10 @@ Linux Programmer's Manual
*/
#[cfg(target_os = "linux")]
pub static ALL_SIGNALS: [Signal<'static>; 31] = [
Signal {
name: "HUP",
value: 1,
},
Signal {
name: "INT",
value: 2,
},
Signal {
name: "QUIT",
value: 3,
},
Signal {
name: "ILL",
value: 4,
},
Signal {
name: "TRAP",
value: 5,
},
Signal {
name: "ABRT",
value: 6,
},
Signal {
name: "BUS",
value: 7,
},
Signal {
name: "FPE",
value: 8,
},
Signal {
name: "KILL",
value: 9,
},
Signal {
name: "USR1",
value: 10,
},
Signal {
name: "SEGV",
value: 11,
},
Signal {
name: "USR2",
value: 12,
},
Signal {
name: "PIPE",
value: 13,
},
Signal {
name: "ALRM",
value: 14,
},
Signal {
name: "TERM",
value: 15,
},
Signal {
name: "STKFLT",
value: 16,
},
Signal {
name: "CHLD",
value: 17,
},
Signal {
name: "CONT",
value: 18,
},
Signal {
name: "STOP",
value: 19,
},
Signal {
name: "TSTP",
value: 20,
},
Signal {
name: "TTIN",
value: 21,
},
Signal {
name: "TTOU",
value: 22,
},
Signal {
name: "URG",
value: 23,
},
Signal {
name: "XCPU",
value: 24,
},
Signal {
name: "XFSZ",
value: 25,
},
Signal {
name: "VTALRM",
value: 26,
},
Signal {
name: "PROF",
value: 27,
},
Signal {
name: "WINCH",
value: 28,
},
Signal {
name: "POLL",
value: 29,
},
Signal {
name: "PWR",
value: 30,
},
Signal {
name: "SYS",
value: 31,
},
pub static ALL_SIGNALS: [&str; 32] = [
"EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV",
"USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU",
"URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "POLL", "PWR", "SYS",
];
/*
@ -198,131 +72,10 @@ No Name Default Action Description
*/
#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
pub static ALL_SIGNALS: [Signal<'static>; 31] = [
Signal {
name: "HUP",
value: 1,
},
Signal {
name: "INT",
value: 2,
},
Signal {
name: "QUIT",
value: 3,
},
Signal {
name: "ILL",
value: 4,
},
Signal {
name: "TRAP",
value: 5,
},
Signal {
name: "ABRT",
value: 6,
},
Signal {
name: "EMT",
value: 7,
},
Signal {
name: "FPE",
value: 8,
},
Signal {
name: "KILL",
value: 9,
},
Signal {
name: "BUS",
value: 10,
},
Signal {
name: "SEGV",
value: 11,
},
Signal {
name: "SYS",
value: 12,
},
Signal {
name: "PIPE",
value: 13,
},
Signal {
name: "ALRM",
value: 14,
},
Signal {
name: "TERM",
value: 15,
},
Signal {
name: "URG",
value: 16,
},
Signal {
name: "STOP",
value: 17,
},
Signal {
name: "TSTP",
value: 18,
},
Signal {
name: "CONT",
value: 19,
},
Signal {
name: "CHLD",
value: 20,
},
Signal {
name: "TTIN",
value: 21,
},
Signal {
name: "TTOU",
value: 22,
},
Signal {
name: "IO",
value: 23,
},
Signal {
name: "XCPU",
value: 24,
},
Signal {
name: "XFSZ",
value: 25,
},
Signal {
name: "VTALRM",
value: 26,
},
Signal {
name: "PROF",
value: 27,
},
Signal {
name: "WINCH",
value: 28,
},
Signal {
name: "INFO",
value: 29,
},
Signal {
name: "USR1",
value: 30,
},
Signal {
name: "USR2",
value: 31,
},
pub static ALL_SIGNALS: [&str; 32] = [
"EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "EMT", "FPE", "KILL", "BUS", "SEGV",
"SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO",
"XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "INFO", "USR1", "USR2",
];
pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option<usize> {
@ -335,56 +88,45 @@ pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option<usize> {
}
let signal_name = signal_name_or_value.trim_start_matches("SIG");
ALL_SIGNALS
.iter()
.find(|s| s.name == signal_name)
.map(|s| s.value)
ALL_SIGNALS.iter().position(|&s| s == signal_name)
}
#[inline(always)]
pub fn is_signal(num: usize) -> bool {
// Named signals start at 1
num <= ALL_SIGNALS.len()
num < ALL_SIGNALS.len()
}
#[test]
fn signals_all_contiguous() {
for (i, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(signal.value, i + 1);
}
}
#[test]
fn signals_all_are_signal() {
for signal in &ALL_SIGNALS {
assert!(is_signal(signal.value));
}
pub fn signal_name_by_value(signal_value: usize) -> Option<&'static str> {
ALL_SIGNALS.get(signal_value).copied()
}
#[test]
fn signal_by_value() {
assert_eq!(signal_by_name_or_value("0"), Some(0));
for signal in &ALL_SIGNALS {
assert_eq!(
signal_by_name_or_value(&signal.value.to_string()),
Some(signal.value)
);
for (value, _signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(signal_by_name_or_value(&value.to_string()), Some(value));
}
}
#[test]
fn signal_by_short_name() {
for signal in &ALL_SIGNALS {
assert_eq!(signal_by_name_or_value(signal.name), Some(signal.value));
for (value, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(signal_by_name_or_value(signal), Some(value));
}
}
#[test]
fn signal_by_long_name() {
for signal in &ALL_SIGNALS {
for (value, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(
signal_by_name_or_value(&format!("SIG{}", signal.name)),
Some(signal.value)
signal_by_name_or_value(&format!("SIG{}", signal)),
Some(value)
);
}
}
#[test]
fn name() {
for (value, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(signal_name_by_value(value), Some(*signal));
}
}

View file

@ -9,3 +9,39 @@ fn test_subcommand_return_code() {
new_ucmd!().arg("1").arg("false").run().status_code(1);
}
#[test]
fn test_command_with_args() {
new_ucmd!()
.args(&["1700", "echo", "-n", "abcd"])
.succeeds()
.stdout_only("abcd");
}
#[test]
fn test_verbose() {
for &verbose_flag in &["-v", "--verbose"] {
new_ucmd!()
.args(&[verbose_flag, ".1", "sleep", "10"])
.fails()
.stderr_only("timeout: sending signal TERM to command 'sleep'");
new_ucmd!()
.args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "10"])
.fails()
.stderr_only("timeout: sending signal EXIT to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'");
}
}
#[test]
fn test_zero_timeout() {
new_ucmd!()
.args(&["-v", "0", "sleep", ".1"])
.succeeds()
.no_stderr()
.no_stdout();
new_ucmd!()
.args(&["-v", "0", "-s0", "-k0", "sleep", ".1"])
.succeeds()
.no_stderr()
.no_stdout();
}