From 14c4cbf9763088fc81e00ff7214411d1e5d096b7 Mon Sep 17 00:00:00 2001 From: Narthana Epa Date: Sat, 21 Sep 2024 16:14:42 +1000 Subject: [PATCH 1/2] Append to log files instead of overwriting --- Cargo.lock | 74 +++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/lib.rs | 9 ++-- tests/integration_test.rs | 89 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 161 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d26a14..7d3f54d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,7 +38,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -48,7 +48,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -67,6 +67,12 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "bstr" version = "1.10.0" @@ -78,6 +84,12 @@ dependencies = [ "serde", ] +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.17" @@ -198,6 +210,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "eyre" version = "0.6.12" @@ -208,6 +230,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + [[package]] name = "heck" version = "0.5.0" @@ -232,6 +260,12 @@ version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "memchr" version = "2.7.4" @@ -314,6 +348,20 @@ dependencies = [ "crossbeam", "eyre", "pretty_assertions", + "tempfile", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", ] [[package]] @@ -353,6 +401,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termtree" version = "0.4.1" @@ -389,6 +450,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" diff --git a/Cargo.toml b/Cargo.toml index bd84836..e8bb625 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ eyre = "0.6.12" [dev-dependencies] assert_cmd = "2.0.16" pretty_assertions = "1.4.1" +tempfile = "3.12.0" [lints.clippy] all = "deny" diff --git a/src/lib.rs b/src/lib.rs index 4a2c7f3..6e644d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ use core::panic; use crossbeam::channel::{bounded, select, Receiver, TrySendError}; use eyre::{eyre, Result}; use std::{ - fs::File, + fs::OpenOptions, io::{stderr, stdin, stdout, Read, Write}, path::Path, process::{Command, Stdio}, @@ -18,7 +18,7 @@ fn io_streams( ) -> Result<(Stdio, Vec>)> { match log_path { Some(path) => { - let file = File::create(path)?; + let file = OpenOptions::new().create(true).append(true).open(path)?; Ok((Stdio::piped(), vec![Box::new(writer), Box::new(file)])) } None => Ok((Stdio::inherit(), vec![Box::new(writer)])), @@ -53,7 +53,10 @@ pub fn run( match child.stdin.take() { Some(child_in) => { - let in_file = File::create(stdin_log_path.unwrap())?; + let in_file = OpenOptions::new() + .create(true) + .append(true) + .open(stdin_log_path.unwrap())?; let mut in_writers: Vec> = vec![Box::new(child_in), Box::new(in_file)]; diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 218e106..fc613dc 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,15 +1,90 @@ use assert_cmd::Command; +use eyre::{eyre, Result}; +use pretty_assertions::assert_eq; +use std::{ + fs::File, + io::{Read, Write}, + path::Path, +}; +use tempfile::tempdir; #[test] -fn child_exit() { - let mut runner = Command::cargo_bin("runner").unwrap(); - let assert = runner.args(["--", "echo", "hello"]).assert(); - assert.success().stdout("hello\n"); +fn child_exit() -> Result<()> { + Command::cargo_bin("runner")? + .args(["--", "echo", "hello"]) + .assert() + .success() + .stdout("hello\n"); + Ok(()) } #[test] -fn stdin_close() { +fn stdin_close() -> Result<()> { + Command::cargo_bin("runner")? + .args(["--", "cat"]) + .write_stdin("hello") + .assert() + .success() + .stdout("hello"); + Ok(()) +} + +const TEMP_HEADER: &str = "Temporary File Header"; + +#[test] +fn child_exit_with_files() -> Result<()> { let mut runner = Command::cargo_bin("runner").unwrap(); - let assert = runner.args(["--", "cat"]).write_stdin("hello").assert(); - assert.success().stdout("hello"); + let dir = tempdir()?; + + let in_file_path = dir.path().join("in.log"); + let out_file_path = dir.path().join("out.log"); + let err_file_path = dir.path().join("err.log"); + + [&in_file_path, &out_file_path, &err_file_path] + .into_iter() + .try_for_each(|p| create_temp_data_in_file(p)) + .unwrap(); + + runner + .args([ + "--in-file", + in_file_path + .to_str() + .ok_or_else(|| eyre!("invalid in-file path"))?, + "--out-file", + out_file_path + .to_str() + .ok_or_else(|| eyre!("invalid out-file path"))?, + "--err-file", + err_file_path + .to_str() + .ok_or_else(|| eyre!("invalid err-file path"))?, + "--", + "cat", + ]) + .write_stdin("hello world\n") + .assert() + .success() + .stdout("hello world\n"); + + let in_contents = read_file(&in_file_path)?; + let out_contents = read_file(&out_file_path)?; + let err_contents = read_file(&err_file_path)?; + + assert_eq!(in_contents, format!("{TEMP_HEADER}\nhello world\n")); + assert_eq!(out_contents, format!("{TEMP_HEADER}\nhello world\n")); + assert_eq!(err_contents, format!("{TEMP_HEADER}\n")); + Ok(()) +} + +fn read_file(path: &Path) -> Result { + let mut out = String::new(); + File::open(path)?.read_to_string(&mut out)?; + Ok(out) +} + +fn create_temp_data_in_file(path: &Path) -> Result<()> { + let mut f = File::create_new(path)?; + writeln!(f, "{TEMP_HEADER}")?; + Ok(()) } From 2cc5492f9e3ecfbc9a52eb87614f1d8c5ac60506 Mon Sep 17 00:00:00 2001 From: Narthana Epa Date: Sat, 21 Sep 2024 23:11:35 +1000 Subject: [PATCH 2/2] Add a header to log files --- Cargo.lock | 174 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/lib.rs | 34 ++++++-- src/log_file.rs | 13 +++ src/main.rs | 10 +++ tests/integration_test.rs | 1 + 6 files changed, 225 insertions(+), 8 deletions(-) create mode 100644 src/log_file.rs diff --git a/Cargo.lock b/Cargo.lock index 7d3f54d..a4f5821 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.15" @@ -67,6 +82,12 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + [[package]] name = "bitflags" version = "2.6.0" @@ -84,12 +105,41 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cc" +version = "1.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "clap" version = "4.5.17" @@ -136,6 +186,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossbeam" version = "0.8.4" @@ -242,6 +298,29 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indenter" version = "0.3.3" @@ -254,6 +333,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.158" @@ -266,12 +354,27 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -344,6 +447,7 @@ name = "runner" version = "0.1.0" dependencies = [ "assert_cmd", + "chrono", "clap", "crossbeam", "eyre", @@ -384,6 +488,12 @@ dependencies = [ "syn", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "strsim" version = "0.11.1" @@ -441,6 +551,70 @@ dependencies = [ "libc", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index e8bb625..143628d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +chrono = "0.4.38" clap = { version = "4.5.17", features = ["derive", "env"] } crossbeam = "0.8.4" eyre = "0.6.12" diff --git a/src/lib.rs b/src/lib.rs index 6e644d0..e8256ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,24 +1,30 @@ mod flat_map_err; +mod log_file; use crate::flat_map_err::FlatMapErr; +use chrono::Utc; use core::panic; use crossbeam::channel::{bounded, select, Receiver, TrySendError}; use eyre::{eyre, Result}; use std::{ - fs::OpenOptions, io::{stderr, stdin, stdout, Read, Write}, path::Path, process::{Command, Stdio}, thread::{self, Scope}, }; +fn default_header(name: &str) -> String { + format!("[{}]: Begin runner log of {name}", Utc::now()) +} + fn io_streams( + header: Option<&str>, writer: W, log_path: Option<&Path>, ) -> Result<(Stdio, Vec>)> { match log_path { Some(path) => { - let file = OpenOptions::new().create(true).append(true).open(path)?; + let file = log_file::new(header, path)?; Ok((Stdio::piped(), vec![Box::new(writer), Box::new(file)])) } None => Ok((Stdio::inherit(), vec![Box::new(writer)])), @@ -36,13 +42,23 @@ fn io_streams( pub fn run( cmd: &str, args: &[&str], + no_header: bool, stdin_log_path: Option<&Path>, stdout_log_path: Option<&Path>, stderr_log_path: Option<&Path>, ) -> Result { let in_io = stdout_log_path.map_or(Stdio::inherit(), |_| Stdio::piped()); - let (out_io, mut out_writers) = io_streams(stdout(), stdout_log_path)?; - let (err_io, mut err_writers) = io_streams(stderr(), stderr_log_path)?; + + let (out_header, err_header) = if no_header { + (None, None) + } else { + ( + Some(default_header("stdout")), + Some(default_header("stderr")), + ) + }; + let (out_io, mut out_writers) = io_streams(out_header.as_deref(), stdout(), stdout_log_path)?; + let (err_io, mut err_writers) = io_streams(err_header.as_deref(), stderr(), stderr_log_path)?; let mut child = Command::new(cmd) .args(args) @@ -53,10 +69,12 @@ pub fn run( match child.stdin.take() { Some(child_in) => { - let in_file = OpenOptions::new() - .create(true) - .append(true) - .open(stdin_log_path.unwrap())?; + let header = if no_header { + None + } else { + Some(default_header("stdin")) + }; + let in_file = log_file::new(header.as_deref(), stdin_log_path.unwrap())?; let mut in_writers: Vec> = vec![Box::new(child_in), Box::new(in_file)]; diff --git a/src/log_file.rs b/src/log_file.rs new file mode 100644 index 0000000..d4a46c3 --- /dev/null +++ b/src/log_file.rs @@ -0,0 +1,13 @@ +use std::{ + fs::{File, OpenOptions}, + io::{Result, Write}, + path::Path, +}; + +pub(crate) fn new(header: Option<&str>, path: &Path) -> Result { + let mut file = OpenOptions::new().create(true).append(true).open(path)?; + if let Some(header) = header { + writeln!(file, "{header}")?; + } + Ok(file) +} diff --git a/src/main.rs b/src/main.rs index ed1d291..fa36389 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,9 @@ struct Args { #[arg(short, long, env)] err_file: Option, + #[arg(long, env)] + no_header: bool, + /// The command to run and its arguments. A command must be specified, arguments are space delimited. #[arg(last = true, required = true, num_args = 1..)] exec: Vec, @@ -30,6 +33,7 @@ fn main() -> Result<()> { let code = runner::run( exec[0], &exec[1..], + args.no_header, args.in_file.as_deref(), args.out_file.as_deref(), args.err_file.as_deref(), @@ -53,6 +57,7 @@ mod test { in_file: None, out_file: None, err_file: None, + no_header: false, exec: vec!["echo".to_string(), "hello".to_string()], }, ), @@ -62,6 +67,7 @@ mod test { in_file: Some("in.txt".into()), out_file: None, err_file: None, + no_header: false, exec: vec!["echo".to_string(), "hello".to_string()], }, ), @@ -71,6 +77,7 @@ mod test { in_file: None, out_file: Some("out.txt".into()), err_file: None, + no_header: false, exec: vec!["echo".to_string(), "hello".to_string()], }, ), @@ -80,6 +87,7 @@ mod test { in_file: None, out_file: None, err_file: Some("err.txt".into()), + no_header: false, exec: vec!["echo".to_string(), "hello".to_string()], }, ), @@ -98,6 +106,7 @@ mod test { in_file: Some("in.txt".into()), out_file: Some("out.txt".into()), err_file: None, + no_header: false, exec: vec!["echo".to_string(), "hello".to_string()], }, ), @@ -118,6 +127,7 @@ mod test { in_file: Some("in.txt".into()), out_file: Some("out.txt".into()), err_file: Some("err.txt".into()), + no_header: false, exec: vec!["echo".to_string(), "hello".to_string()], }, ), diff --git a/tests/integration_test.rs b/tests/integration_test.rs index fc613dc..83f15b8 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -59,6 +59,7 @@ fn child_exit_with_files() -> Result<()> { err_file_path .to_str() .ok_or_else(|| eyre!("invalid err-file path"))?, + "--no-header", "--", "cat", ])