diff --git a/Cargo.toml b/Cargo.toml index c69438a0..bf508390 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ members = [ "sort", "tail", "tee", + "time", "touch", "true", "tty", diff --git a/FreeBSD.toml b/FreeBSD.toml index 7099abe5..3da75087 100644 --- a/FreeBSD.toml +++ b/FreeBSD.toml @@ -41,6 +41,7 @@ members = [ "sort", "tail", "tee", + "time", "touch", "true", "tty", diff --git a/Fuchsia.toml b/Fuchsia.toml index 6f770662..912318d2 100644 --- a/Fuchsia.toml +++ b/Fuchsia.toml @@ -41,6 +41,7 @@ members = [ "sort", "tail", "tee", + # "time", "touch", "true", "tty", diff --git a/Haiku.toml b/Haiku.toml index 6f770662..3ef963a9 100644 --- a/Haiku.toml +++ b/Haiku.toml @@ -41,6 +41,7 @@ members = [ "sort", "tail", "tee", + "time", "touch", "true", "tty", diff --git a/Illumos.toml b/Illumos.toml index 7099abe5..3da75087 100644 --- a/Illumos.toml +++ b/Illumos.toml @@ -41,6 +41,7 @@ members = [ "sort", "tail", "tee", + "time", "touch", "true", "tty", diff --git a/Linux.toml b/Linux.toml index 863d08bb..dc41b834 100644 --- a/Linux.toml +++ b/Linux.toml @@ -41,6 +41,7 @@ members = [ "sort", "tail", "tee", + "time", "touch", "true", "tty", diff --git a/MacOS.toml b/MacOS.toml index 7099abe5..3da75087 100644 --- a/MacOS.toml +++ b/MacOS.toml @@ -41,6 +41,7 @@ members = [ "sort", "tail", "tee", + "time", "touch", "true", "tty", diff --git a/NetBSD.toml b/NetBSD.toml index 7099abe5..3da75087 100644 --- a/NetBSD.toml +++ b/NetBSD.toml @@ -41,6 +41,7 @@ members = [ "sort", "tail", "tee", + "time", "touch", "true", "tty", diff --git a/OpenBSD.toml b/OpenBSD.toml index 7099abe5..3da75087 100644 --- a/OpenBSD.toml +++ b/OpenBSD.toml @@ -41,6 +41,7 @@ members = [ "sort", "tail", "tee", + "time", "touch", "true", "tty", diff --git a/Solaris.toml b/Solaris.toml index 7099abe5..3da75087 100644 --- a/Solaris.toml +++ b/Solaris.toml @@ -41,6 +41,7 @@ members = [ "sort", "tail", "tee", + "time", "touch", "true", "tty", diff --git a/Unix.toml b/Unix.toml index 7099abe5..3da75087 100644 --- a/Unix.toml +++ b/Unix.toml @@ -41,6 +41,7 @@ members = [ "sort", "tail", "tee", + "time", "touch", "true", "tty", diff --git a/coreutils_core/src/os.rs b/coreutils_core/src/os.rs index aec92616..0650ad20 100644 --- a/coreutils_core/src/os.rs +++ b/coreutils_core/src/os.rs @@ -3,6 +3,7 @@ pub mod group; pub mod login_name; pub mod passwd; pub mod process; +pub mod resource; pub mod time; pub mod tty; pub mod utsname; diff --git a/coreutils_core/src/os/resource.rs b/coreutils_core/src/os/resource.rs new file mode 100644 index 00000000..7db63a9f --- /dev/null +++ b/coreutils_core/src/os/resource.rs @@ -0,0 +1,109 @@ +//! Module abstracting interactions with getrusage(2) +//! +//! Also holds utility functions for summarizing the data returned by getrusage(2) +use super::TimeVal; +#[cfg(not(target_os = "fuchsia"))] +use libc::getrusage; +use libc::{c_int, rusage, RUSAGE_CHILDREN, RUSAGE_SELF}; + +/// Interface for `RUSAGE_*` constants from libc. +/// +/// TODO This is an incomplete set of constants. It is currently missing +/// `libc::RUSAGE_THREAD` which requires the `_GNU_SOURCE` macro to be defined +/// at build time. +pub enum ResourceConsumer { + Caller = RUSAGE_SELF as isize, + Children = RUSAGE_CHILDREN as isize, +} + +#[derive(Debug)] +pub struct RUsage { + pub timing: Timing, + pub mem: MemoryUsage, + pub io: IOUsage, +} + +#[derive(Debug)] +pub struct Timing { + /// User CPU time used + pub user_time: TimeVal, + /// System CPU time used + pub sys_time: TimeVal, +} + +#[derive(Debug)] +pub struct MemoryUsage { + /// Maximum resident set size + pub max_rss: u64, + /// Number of page reclaims (soft page faults) + pub num_minor_page_flt: u64, + /// Number of page faults (hard page faults) + pub num_major_page_flt: u64, + /// Number of voluntary context switches + pub num_vol_ctx_switch: u64, + /// Number of involuntary context switches + pub num_invol_ctx_switch: u64, + /// Unmaintained on linux: Integral shared memory size + pub shared_mem_size: u64, + /// Unmaintained on linux: Integral unshared data size + pub unshared_data_size: u64, + /// Unmaintained on linux: Integral unshared stack size + pub unshared_stack_size: u64, + /// Unmaintained on linux: Number of swaps + pub num_swaps: u64, +} + +#[derive(Debug)] +pub struct IOUsage { + /// Number of block input operations + pub num_block_in: u64, + /// Number of block output operations + pub num_block_out: u64, + /// Unmaintained on linux: Number of IPC messages recieved + pub num_sock_recv: u64, + /// Unmaintained on linux: Number of IPC messages sent + pub num_sock_send: u64, + /// Unmaintained: Number of signals recieved + pub num_signals: u64, +} + +impl From for RUsage { + fn from(ru: rusage) -> Self { + RUsage { + timing: Timing { user_time: ru.ru_utime, sys_time: ru.ru_stime }, + mem: MemoryUsage { + max_rss: ru.ru_maxrss as u64, + num_minor_page_flt: ru.ru_minflt as u64, + num_major_page_flt: ru.ru_majflt as u64, + num_vol_ctx_switch: ru.ru_nvcsw as u64, + num_invol_ctx_switch: ru.ru_nivcsw as u64, + shared_mem_size: ru.ru_ixrss as u64, + unshared_data_size: ru.ru_idrss as u64, + unshared_stack_size: ru.ru_isrss as u64, + num_swaps: ru.ru_nswap as u64, + }, + io: IOUsage { + num_block_in: ru.ru_inblock as u64, + num_block_out: ru.ru_oublock as u64, + num_sock_recv: ru.ru_msgrcv as u64, + num_sock_send: ru.ru_msgsnd as u64, + num_signals: ru.ru_nsignals as u64, + }, + } + } +} + +/// Safely wrap `libc::getrusage` +pub fn get_rusage(target: ResourceConsumer) -> RUsage { + let mut usage: rusage = unsafe { std::mem::zeroed() }; + + #[cfg(not(target_os = "fuchsia"))] + // Fuchsia doesn't have a getrusage syscall, but provides the rusage struct. + // The default is to abort with an error message so that callers don't end + // up with invalid data. + unsafe { + getrusage(target as c_int, &mut usage); + } + + RUsage::from(usage) +} diff --git a/time/Cargo.toml b/time/Cargo.toml new file mode 100644 index 00000000..aa4854df --- /dev/null +++ b/time/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "time" +version = "0.1.0" +authors = ["Vaibhav Yenamandra "] +license = "MPL-2.0-no-copyleft-exception" +build = "build.rs" +edition = "2018" +description = "time a simple command" + +[dependencies] +clap = { version = "^2.33.0", features = ["wrap_help"] } +coreutils_core = { path = "../coreutils_core" } + +[build-dependencies] +clap = { version = "^2.33.0" } diff --git a/time/build.rs b/time/build.rs new file mode 100644 index 00000000..8e0a5883 --- /dev/null +++ b/time/build.rs @@ -0,0 +1,24 @@ +use std::env; + +use clap::Shell; + +#[path = "src/cli.rs"] +mod cli; + +fn main() { + let mut app = cli::create_app(); + + let out_dir = match env::var("OUT_DIR") { + Ok(dir) => dir, + Err(err) => { + eprintln!("No OUT_DIR: {}", err); + return; + }, + }; + + app.gen_completions("template", Shell::Zsh, out_dir.clone()); + app.gen_completions("template", Shell::Fish, out_dir.clone()); + app.gen_completions("template", Shell::Bash, out_dir.clone()); + app.gen_completions("template", Shell::PowerShell, out_dir.clone()); + app.gen_completions("template", Shell::Elvish, out_dir); +} diff --git a/time/src/cli.rs b/time/src/cli.rs new file mode 100644 index 00000000..f0b528b2 --- /dev/null +++ b/time/src/cli.rs @@ -0,0 +1,29 @@ +use clap::{ + crate_authors, crate_description, crate_name, crate_version, App, AppSettings::ColoredHelp, Arg, +}; + +pub(crate) fn create_app<'a, 'b>() -> App<'a, 'b> { + let app = App::new(crate_name!()) + .version(crate_version!()) + .author(crate_authors!()) + .about(crate_description!()) + .help_message("Display help information.") + .version_message("Display version information.") + .help_short("?") + .settings(&[ColoredHelp]); + + let posix_fmt = Arg::with_name("posix") + .help( + "Display time output in POSIX specified format as:\n\treal %f\n\tuser %f\n\tsys \ + %f\nTimer accuracy is arbitrary, but will always be counted in seconds.", + ) + .short("p") + .takes_value(false); + + let command = Arg::with_name("COMMAND").help("Command or utility to run.").required(true); + + let arguments = + Arg::with_name("ARGUMENT").help("Optional arguments to pass to .").multiple(true); + + app.args(&[posix_fmt, command, arguments]) +} diff --git a/time/src/flags.rs b/time/src/flags.rs new file mode 100644 index 00000000..565b86e9 --- /dev/null +++ b/time/src/flags.rs @@ -0,0 +1,60 @@ +//! Command line options that are supported by `time` + +use crate::{cli::create_app, output::OutputFormatter}; + +// Condense CLI args as a struct +#[derive(Debug)] +pub struct TimeOpts { + /// Formatter to use when printing stats back to CLI + pub printer: OutputFormatter, + /// Command as seen on the CLI + pub command: Vec, +} + +impl TimeOpts { + pub fn from_matches() -> Self { Self::new(create_app().get_matches()) } + + pub fn new(args: clap::ArgMatches) -> Self { + let command = + args.value_of("COMMAND").expect("`COMMAND` value cannot be `None`, it is required."); + + TimeOpts { + printer: if args.is_present("posix") { + OutputFormatter::Posix + } else { + OutputFormatter::Default + }, + command: match args.values_of("ARGUMENT") { + Some(vs) => { + let mut cmd = vec![command.to_owned()]; + cmd.extend(vs.into_iter().map(|item| item.to_owned())); + cmd + }, + None => vec![command.to_owned()], + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::{create_app, OutputFormatter, TimeOpts}; + + #[test] + fn parsing_valid_command_with_args() { + let args = vec!["test-time", "cmd-to-run", "arg1", "arg2", "arg3"]; + let opts = TimeOpts::new(create_app().get_matches_from(args)); + + assert_eq!(4, opts.command.len()); + assert_eq!(vec!["cmd-to-run", "arg1", "arg2", "arg3"], opts.command); + assert_eq!(OutputFormatter::Default, opts.printer); + } + + #[test] + fn parse_valid_command_with_posix_spec() { + let args = vec!["test-time", "cmd-to-run", "arg1", "arg2", "arg3", "-p"]; + let opts = TimeOpts::new(create_app().get_matches_from(args)); + + assert_eq!(OutputFormatter::Posix, opts.printer); + } +} diff --git a/time/src/main.rs b/time/src/main.rs new file mode 100644 index 00000000..b7a3d5ed --- /dev/null +++ b/time/src/main.rs @@ -0,0 +1,19 @@ +mod cli; +mod flags; +mod output; +mod subprocess; + +use coreutils_core::os::resource::{get_rusage, ResourceConsumer}; + +fn main() { + let opts = flags::TimeOpts::from_matches(); + let (exit_status, duration) = match subprocess::timed_run(&opts.command) { + Ok(rv) => rv, + Err(err) => subprocess::exit_with_msg(err), + }; + + let usage = get_rusage(ResourceConsumer::Children); + + eprintln!("{}", opts.printer.format_stats(&usage, &duration)); + std::process::exit(exit_status.code().unwrap_or(1)); +} diff --git a/time/src/output.rs b/time/src/output.rs new file mode 100644 index 00000000..07f6aa93 --- /dev/null +++ b/time/src/output.rs @@ -0,0 +1,30 @@ +//! Output interface for `time` + +use coreutils_core::os::{resource::RUsage, TimeVal}; + +#[derive(Debug, PartialEq)] +pub enum OutputFormatter { + Default, + Posix, +} + +/// Express `coreutils_core::os::TimeVal` into `f64` seconds +fn as_secs_f64(tv: TimeVal) -> f64 { tv.tv_sec as f64 + (tv.tv_usec as f64) / 1_000_000.0 } + +impl OutputFormatter { + pub fn format_stats(self, rusage: &RUsage, duration: &std::time::Duration) -> String { + let wall_time = duration.as_secs_f64(); + let user_time = as_secs_f64(rusage.timing.user_time); + let sys_time = as_secs_f64(rusage.timing.sys_time); + match self { + OutputFormatter::Default => default_formatter(rusage, wall_time, user_time, sys_time), + OutputFormatter::Posix => { + format!("real {:.2}\nuser {:.2}\nsys {:.2}", wall_time, user_time, sys_time) + }, + } + } +} + +pub fn default_formatter(_: &RUsage, wall_time: f64, user_time: f64, sys_time: f64) -> String { + format!("{:.2} real {:.2} user {:.2} sys", wall_time, user_time, sys_time) +} diff --git a/time/src/subprocess.rs b/time/src/subprocess.rs new file mode 100644 index 00000000..13786f47 --- /dev/null +++ b/time/src/subprocess.rs @@ -0,0 +1,56 @@ +/// Module for creating, and interacting with child processes +use std::process::{exit, Command, ExitStatus, Stdio}; +use std::{ + io, + time::{Duration, Instant}, +}; + +type SubprocessTiming = (ExitStatus, Duration); + +/// Wrapper around `std::process::exit` that prints the error's +/// message to stderr before quitting. +/// +/// Will try to propagate the error code set in the err if available +pub fn exit_with_msg(err: std::io::Error) -> ! { + eprintln!("{}", err); + + // Translate the exit code according to POSIX spec + // 1-125: for errors internal to `time` + // 126 : Command was found but could not be invoked (PermissionError) + // 127 : Command was not found + exit(match err.kind() { + io::ErrorKind::PermissionDenied => 126, + io::ErrorKind::NotFound => 127, + // Translate other error code to 0-124 and shift right by 1 + // Internal exit codes are typically arbitrary enough that they be + // considered limited to developer use-only + _ => 1 + (err.raw_os_error().unwrap_or(0) % 125), + }) +} + +/// Wrapper for creating, spawning and waiting on `std::process::Command` +/// Returns the `std::process::ExitStatus` of the `std::process::Command` +/// that was run +pub fn timed_run(cmd_vec: &Vec) -> io::Result { + let mut cmd = Command::new(&cmd_vec[0]); + cmd.args(&cmd_vec[1..]); + cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit()).stderr(Stdio::inherit()); + + let start_time = Instant::now(); + let status = cmd.spawn()?.wait()?; + Ok((status, start_time.elapsed())) +} + +#[cfg(test)] +mod tests { + use super::timed_run; + + #[test] + fn invalid_command_returns_errno_when_set() { + if let Err(err) = timed_run(&vec!["does-not-exist".to_string()]) { + assert!(err.raw_os_error() == Some(2)) + } else { + assert!(false, "Subprocess did not fail as expected") + } + } +}