diff --git a/Cargo.lock b/Cargo.lock index a7586ce..d3d0db0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.97" @@ -77,21 +127,101 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clap" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "command_timeout" version = "0.1.3" dependencies = [ "anyhow", + "clap", + "env_logger", "futures", "libc", + "log", "nix", + "serde", "tempfile", "thiserror", "tokio", + "toml", "tracing", "tracing-subscriber", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.10" @@ -99,7 +229,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -215,6 +345,58 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "jiff" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f33145a5cbea837164362c7bd596106eb7c5198f97d1ba6f6ebb3223952e488" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ce13c40ec6956157a3635d97a1ee2df323b263f09ea14165131289cb0f5c19" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -281,7 +463,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -362,6 +544,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -455,7 +652,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -464,6 +661,35 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -504,9 +730,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.100" @@ -528,7 +760,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -576,7 +808,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -590,6 +822,40 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" version = "0.1.41" @@ -657,6 +923,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.1" @@ -709,6 +981,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -773,6 +1054,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index 6c74e83..421b8de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,12 @@ libc = "0.2" # For setpgid and signal constants tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } # For logging setup tempfile = "3" # For creating temporary directories in examples/tests anyhow = "1.0" # For simple error handling in examples -futures = "0.3" # For simulataneous_git_clone example +futures = "0.3" +clap = { version = "4.5.35", features = ["derive"] } +serde = { version = "1.0.219", features = ["derive"] } +toml = "0.8.20" +env_logger = "0.11.8" +log = "0.4.27" # For simulataneous_git_clone example # Dependency on the library itself (needed for building examples within the workspace) # This line might not be strictly necessary if cargo automatically detects it, # but explicitly listing it can sometimes help. diff --git a/README.md b/README.md index 303747c..e087409 100644 --- a/README.md +++ b/README.md @@ -40,20 +40,31 @@ The primary goal is to allow commands to run as long as they are actively making ## Installation -Add this to your `Cargo.toml`: +Just run +```shell +$ git clone https://github.com/cfsmp3/exec_timeout_rs +$ cd exec_timeout_rs +$ cargo install --path . --locked +``` +## Usage +```shell +$ command_timeout -c "curl https://www.google.com/" -f "/path/to/config.toml" +``` +OR +```shell +$ command_timeout +$ Enter your Command - curl https://www.google.com/ +$ Enter your config location - /path/to/config.toml +``` +### Example of config.toml ```toml -[dependencies] -command-timeout = "0.1.0" # Replace with the desired version from crates.io -# Required dependencies if you don't already have them -tokio = { version = "1", features = ["full"] } -tracing = "0.1" -thiserror = "1.0" -nix = { version = "0.29", features = ["signal", "process"] } # Check latest version -libc = "0.2" +minimum_timeout_ms = 500 +maximum_timeout_ms = 10000 +activity_timeout_ms = 2000 ``` -## Usage +## Custom Usage The main entry point is the run_command_with_timeout async function. diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e462d9b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,148 @@ +use clap::Parser; +use serde::Deserialize; +use std::fs; +use std::io::{self, Write}; +use std::path::PathBuf; +use std::process::Command as StdCommand; +use std::time::Duration; +use tracing::{debug, error, info}; +use tracing_subscriber::{fmt, EnvFilter}; + +use command_timeout::{run_command_with_timeout}; + +/// CLI for the command timeout library. +/// +/// Example usage: +/// +/// cargo run -c "curl https://www.google.com/" -conf config.toml +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// External command to execute (e.g., curl https://www.google.com/). + /// If not provided, you will be prompted. + #[arg(short = 'c')] + command: Option, + + /// Path to the configuration file (TOML) that contains timeout settings. + /// If not provided, you will be prompted. + #[arg(short = 'f', long = "conf")] + config: Option, +} + +#[derive(Deserialize, Debug)] +struct Config { + minimum_timeout_ms: u64, + maximum_timeout_ms: u64, + activity_timeout_ms: u64, +} + +fn read_config(path: &PathBuf) -> Result> { + let content = fs::read_to_string(path)?; + let cfg: Config = toml::from_str(&content)?; + Ok(cfg) +} + +fn split_command(command_str: &str) -> (String, Vec) { + let mut parts = command_str.split_whitespace(); + let executable = parts.next().unwrap_or("").to_string(); + let args = parts.map(|s| s.to_string()).collect(); + (executable, args) +} + +fn setup_tracing() { + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + fmt() + .with_env_filter(filter) + .without_time() + .with_level(false) + .with_target(false) + .init(); +} + + + +fn prompt_for_input(prompt: &str) -> Result> { + print!("{}", prompt); + io::stdout().flush()?; + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + Ok(input.trim().to_string()) +} + +#[tokio::main] +async fn main() { + setup_tracing(); + let args = Args::parse(); + + let command_str = if let Some(cmd) = args.command { + cmd + } else { + match prompt_for_input("Enter your Command - ") { + Ok(cmd) if !cmd.is_empty() => cmd, + _ => { + error!("No command provided; exiting."); + std::process::exit(1); + } + } + }; + + let config_path = if let Some(path) = args.config { + path + } else { + match prompt_for_input("Enter your config location - ") { + Ok(p) if !p.is_empty() => PathBuf::from(p), + _ => { + error!("No configuration file provided; exiting."); + std::process::exit(1); + } + } + }; + + let config = match read_config(&config_path) { + Ok(cfg) => cfg, + Err(e) => { + error!("Error reading config file: {}", e); + std::process::exit(1); + } + }; + debug!("Loaded config: {:#?}", config); + + let (executable, args_vec) = split_command(&command_str); + if executable.is_empty() { + error!("Empty command provided; exiting."); + std::process::exit(1); + } + info!("Command to run: {} with args {:?}", executable, args_vec); + + let mut command = StdCommand::new(executable); + command.args(args_vec); + + let minimum_timeout = Duration::from_millis(config.minimum_timeout_ms); + let maximum_timeout = Duration::from_millis(config.maximum_timeout_ms); + let activity_timeout = Duration::from_millis(config.activity_timeout_ms); + + info!("Running command with timeouts:"); + info!(" Minimum Timeout: {:?}", minimum_timeout); + info!(" Maximum Timeout: {:?}", maximum_timeout); + info!(" Activity Timeout: {:?}", activity_timeout); + + match run_command_with_timeout(command, minimum_timeout, maximum_timeout, activity_timeout).await { + Ok(output) => { + info!("--- Command Output ---"); + info!("Timed Out: {}", output.timed_out); + info!("Duration: {:?}", output.duration); + if let Some(status) = output.exit_status { + info!("Exit Status: {}", status); + info!("Exit Code: {:?}", status.code()); + } else { + info!("Exit Status: None (process may have been killed)"); + } + info!("Stdout:\n{}", String::from_utf8_lossy(&output.stdout)); + info!("Stderr:\n{}", String::from_utf8_lossy(&output.stderr)); + } + Err(e) => { + error!("Error running command: {:?}", e); + std::process::exit(1); + } + } +}