diff --git a/README.md b/README.md index d761bda..d75183a 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Output will be like this: ### What this library provides #### Macros to run external commands -- run_cmd! --> CmdResult +- [run_cmd!](https://docs.rs/cmd_lib/latest/cmd_lib/macro.run_cmd.html) -> [CmdResult](https://docs.rs/cmd_lib/latest/cmd_lib/type.CmdResult.html) ```rust let msg = "I love rust"; @@ -101,7 +101,7 @@ run_cmd! { }?; ``` -- run_fun! --> FunResult +- [run_fun!](https://docs.rs/cmd_lib/latest/cmd_lib/macro.run_fun.html) -> [FunResult](https://docs.rs/cmd_lib/latest/cmd_lib/type.FunResult.html) ```rust let version = run_fun!(rustc --version)?; @@ -209,8 +209,9 @@ Ignore errors for command execution. ##### echo Print messages to stdout. - +```console -n do not output the trailing newline +``` ##### error, warn, info, debug, trace @@ -227,7 +228,7 @@ run_cmd!(info "This is an infomation message")?; ``` #### Macros to register your own commands -Declare your function with `#[export_cmd(..)]` attribute, and import it with `use_custom_cmd!` macro: +Declare your function with `#[export_cmd(..)]` attribute, and import it with [`use_custom_cmd!`] macro: ```rust #[export_cmd(my_cmd)] @@ -244,13 +245,15 @@ println!("get result: {}", run_fun!(my_cmd)?); #### Low-level process spawning macros -`spawn!` macro executes the whole command as a child process, returning a handle to it. By +[`spawn!`] macro executes the whole command as a child process, returning a handle to it. By default, stdin, stdout and stderr are inherited from the parent. The process will run in the -background, so you can run other stuff concurrently. You can call `wait()` to wait +background, so you can run other stuff concurrently. You can call [`wait()`](`CmdChildren::wait()`) to wait for the process to finish. -With `spawn_with_output!` you can get output by calling `wait_with_output()`, or even do stream -processing with `wait_with_pipe()`. +With [`spawn_with_output!`] you can get output by calling [`wait_with_output()`](`FunChildren::wait_with_output()`), or even do stream +processing with [`wait_with_pipe()`](`FunChildren::wait_with_pipe()`). + +There are also other useful APIs, and you can check the docs for more details. ```rust let mut proc = spawn!(ping -c 10 192.168.0.1)?; @@ -275,9 +278,9 @@ spawn_with_output!(journalctl)?.wait_with_pipe(&mut |pipe| { #### Macros to define, get and set thread-local global variables -- `tls_init!` to define thread local global variable -- `tls_get!` to get the value -- `tls_set!` to set the value +- [`tls_init!`] to define thread local global variable +- [`tls_get!`] to get the value +- [`tls_set!`] to set the value ```rust tls_init!(DELAY, f64, 1.0); const DELAY_FACTOR: f64 = 0.8; @@ -322,7 +325,7 @@ You can use the [glob](https://github.com/rust-lang-nursery/glob) package instea This library tries very hard to not set global states, so parallel `cargo test` can be executed just fine. The only known APIs not supported in multi-thread environment are the -`tls_init/tls_get/tls_set` macros, and you should only use them for *thread local* variables. +[`tls_init`]/[`tls_get`]/[`tls_set`] macros, and you should only use them for *thread local* variables. License: MIT OR Apache-2.0 diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 60ce06b..cb96ed9 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -2,7 +2,7 @@ use proc_macro2::{Span, TokenStream, TokenTree}; use proc_macro_error::{abort, proc_macro_error}; use quote::{quote, ToTokens}; -/// Mark main function to log error result by default +/// Mark main function to log error result by default. /// /// ``` /// # use cmd_lib::*; @@ -40,7 +40,7 @@ pub fn main( .into() } -/// Export the function as an command to be run by `run_cmd!` or `run_fun!` +/// Export the function as an command to be run by [`run_cmd!`] or [`run_fun!`]. /// /// ``` /// # use cmd_lib::*; @@ -79,7 +79,7 @@ pub fn export_cmd( new_functions.into() } -/// Import user registered custom command +/// Import user registered custom command. /// ``` /// # use cmd_lib::*; /// #[export_cmd(my_cmd)] @@ -118,7 +118,7 @@ pub fn use_custom_cmd(item: proc_macro::TokenStream) -> proc_macro::TokenStream .into() } -/// Run commands, returning result handle to check status +/// Run commands, returning [`CmdResult`](../cmd_lib/type.CmdResult.html) to check status. /// ``` /// # use cmd_lib::run_cmd; /// let msg = "I love rust"; @@ -155,7 +155,7 @@ pub fn run_cmd(input: proc_macro::TokenStream) -> proc_macro::TokenStream { .into() } -/// Run commands, returning result handle to capture output and to check status +/// Run commands, returning [`FunResult`](../cmd_lib/type.FunResult.html) to capture output and to check status. /// ``` /// # use cmd_lib::run_fun; /// let version = run_fun!(rustc --version)?; @@ -177,8 +177,7 @@ pub fn run_fun(input: proc_macro::TokenStream) -> proc_macro::TokenStream { .into() } -/// Run commands with/without pipes as a child process, returning a handle to check the final -/// result +/// Run commands with/without pipes as a child process, returning [`CmdChildren`](../cmd_lib/struct.CmdChildren.html) result. /// ``` /// # use cmd_lib::*; /// @@ -199,8 +198,7 @@ pub fn spawn(input: proc_macro::TokenStream) -> proc_macro::TokenStream { .into() } -/// Run commands with/without pipes as a child process, returning a handle to capture the -/// final output +/// Run commands with/without pipes as a child process, returning [`FunChildren`](../cmd_lib/struct.FunChildren.html) result. /// ``` /// # use cmd_lib::*; /// let mut procs = vec![]; @@ -231,7 +229,7 @@ pub fn spawn_with_output(input: proc_macro::TokenStream) -> proc_macro::TokenStr #[proc_macro] #[proc_macro_error] -/// Log a fatal message at the error level, and exit process +/// Log a fatal message at the error level, and exit process. /// /// e.g: /// ``` diff --git a/src/child.rs b/src/child.rs index c615e6f..d186c74 100644 --- a/src/child.rs +++ b/src/child.rs @@ -1,4 +1,4 @@ -use crate::{info, warn}; +use crate::info; use crate::{process, CmdResult, FunResult}; use os_pipe::PipeReader; use std::io::{BufRead, BufReader, Error, ErrorKind, Read, Result}; @@ -8,7 +8,7 @@ use std::thread::JoinHandle; /// Representation of running or exited children processes, connected with pipes /// optionally. /// -/// Calling `spawn!` macro will return `Result` +/// Calling [`spawn!`](../cmd_lib/macro.spawn.html) macro will return `Result` pub struct CmdChildren { children: Vec, ignore_error: bool, @@ -70,7 +70,7 @@ impl CmdChildren { /// Representation of running or exited children processes with output, connected with pipes /// optionally. /// -/// Calling `spawn_with_output!` macro will return `Result` +/// Calling [spawn_with_output!](../cmd_lib/macro.spawn_with_output.html) macro will return `Result` pub struct FunChildren { children: Vec, ignore_error: bool, @@ -88,17 +88,13 @@ impl FunChildren { Err(e) } Ok(output) => { - let mut s = String::from_utf8_lossy(&output).to_string(); - if s.ends_with('\n') { - s.pop(); - } let ret = CmdChildren::wait_children(&mut self.children); if let Err(e) = ret { if !self.ignore_error { return Err(e); } } - Ok(s) + Ok(output) } } } @@ -107,7 +103,7 @@ impl FunChildren { /// provided function. pub fn wait_with_pipe(&mut self, f: &mut dyn FnMut(Box)) -> CmdResult { let child = self.children.pop().unwrap(); - let polling_stderr = StderrLogging::new(&child.cmd, child.stderr); + let stderr_thread = StderrThread::new(&child.cmd, child.stderr, false); match child.handle { CmdChildHandle::Proc(mut proc) => { if let Some(stdout) = child.stdout { @@ -126,10 +122,20 @@ impl FunChildren { } } }; - drop(polling_stderr); + drop(stderr_thread); CmdChildren::wait_children(&mut self.children) } + /// Waits for the children processes to exit completely, returning the command result, stdout + /// read result and stderr read result. + pub fn wait_with_all(&mut self) -> (CmdResult, FunResult, FunResult) { + // wait for the last child result + let handle = self.children.pop().unwrap(); + let wait_all = handle.wait_with_all(true); + let _ = CmdChildren::wait_children(&mut self.children); + wait_all + } + /// Returns the OS-assigned process identifiers associated with these children processes pub fn pids(&self) -> Vec { self.children.iter().filter_map(|x| x.pid()).collect() @@ -158,8 +164,9 @@ impl CmdChild { } } - fn wait(self, is_last: bool) -> CmdResult { - let res = self.handle.wait_with_stderr(self.stderr, &self.cmd); + fn wait(mut self, is_last: bool) -> CmdResult { + let _stderr_thread = StderrThread::new(&self.cmd, self.stderr.take(), false); + let res = self.handle.wait(&self.cmd); if let Err(e) = res { if is_last || process::pipefail_enabled() { return Err(e); @@ -168,27 +175,35 @@ impl CmdChild { Ok(()) } - fn wait_with_output(self, ignore_error: bool) -> Result> { - let buf = { - if let Some(mut out) = self.stdout { - let mut buf = vec![]; - if let Err(e) = out.read_to_end(&mut buf) { - if !ignore_error { - return Err(process::new_cmd_io_error(&e, &self.cmd)); + fn wait_with_output(self, ignore_error: bool) -> FunResult { + let (res, stdout, _) = self.wait_with_all(false); + if !ignore_error { + res?; + } + stdout + } + + fn wait_with_all(mut self, capture: bool) -> (CmdResult, FunResult, FunResult) { + let mut stderr_thread = StderrThread::new(&self.cmd, self.stderr.take(), capture); + let stdout_output = { + if let Some(mut out) = self.stdout.take() { + let mut s = String::new(); + match out.read_to_string(&mut s) { + Err(e) => Err(e), + Ok(_) => { + if s.ends_with('\n') { + s.pop(); + } + Ok(s) } } - buf } else { - vec![] + Ok("".into()) } }; - let res = self.handle.wait_with_stderr(self.stderr, &self.cmd); - if let Err(e) = res { - if !ignore_error { - return Err(e); - } - } - Ok(buf) + let stderr_output = stderr_thread.join(); + let res = self.handle.wait(&self.cmd); + (res, stdout_output, stderr_output) } fn kill(self) -> CmdResult { @@ -207,8 +222,7 @@ pub(crate) enum CmdChildHandle { } impl CmdChildHandle { - fn wait_with_stderr(self, stderr: Option, cmd: &str) -> CmdResult { - let polling_stderr = StderrLogging::new(cmd, stderr); + fn wait(self, cmd: &str) -> CmdResult { match self { CmdChildHandle::Proc(mut proc) => { let status = proc.wait(); @@ -242,7 +256,6 @@ impl CmdChildHandle { } CmdChildHandle::SyncFn => {} } - drop(polling_stderr); Ok(()) } @@ -272,19 +285,31 @@ impl CmdChildHandle { } } -struct StderrLogging { - thread: Option>, +struct StderrThread { + thread: Option>, cmd: String, } -impl StderrLogging { - fn new(cmd: &str, stderr: Option) -> Self { +impl StderrThread { + fn new(cmd: &str, stderr: Option, capture: bool) -> Self { if let Some(stderr) = stderr { let thread = std::thread::spawn(move || { + let mut output = String::new(); BufReader::new(stderr) .lines() .map_while(Result::ok) - .for_each(|line| info!("{}", line)) + .for_each(|line| { + if !capture { + info!("{line}"); + } else { + output.push_str(&line); + output.push('\n'); + } + }); + if output.ends_with('\n') { + output.pop(); + } + output }); Self { cmd: cmd.into(), @@ -297,14 +322,28 @@ impl StderrLogging { } } } -} -impl Drop for StderrLogging { - fn drop(&mut self) { + fn join(&mut self) -> FunResult { if let Some(thread) = self.thread.take() { - if let Err(e) = thread.join() { - warn!("[{}] logging thread exited with error: {:?}", self.cmd, e); + match thread.join() { + Err(e) => { + return Err(Error::new( + ErrorKind::Other, + format!( + "Running [{}] stderr thread joined with error: {e:?}", + self.cmd + ), + )) + } + Ok(output) => return Ok(output), } } + Ok("".into()) + } +} + +impl Drop for StderrThread { + fn drop(&mut self) { + let _ = self.join(); } } diff --git a/src/lib.rs b/src/lib.rs index f396d90..a8ba0e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,7 +83,7 @@ //! ## What this library provides //! //! ### Macros to run external commands -//! - run_cmd! --> CmdResult +//! - [run_cmd!](https://docs.rs/cmd_lib/latest/cmd_lib/macro.run_cmd.html) -> [CmdResult](https://docs.rs/cmd_lib/latest/cmd_lib/type.CmdResult.html) //! //! ```no_run //! # use cmd_lib::run_cmd; @@ -110,7 +110,7 @@ //! # Ok::<(), std::io::Error>(()) //! ``` //! -//! - run_fun! --> FunResult +//! - [run_fun!](https://docs.rs/cmd_lib/latest/cmd_lib/macro.run_fun.html) -> [FunResult](https://docs.rs/cmd_lib/latest/cmd_lib/type.FunResult.html) //! //! ``` //! # use cmd_lib::run_fun; @@ -230,8 +230,9 @@ //! //! #### echo //! Print messages to stdout. -//! +//! ```console //! -n do not output the trailing newline +//! ``` //! //! #### error, warn, info, debug, trace //! @@ -250,7 +251,7 @@ //! ``` //! //! ### Macros to register your own commands -//! Declare your function with `#[export_cmd(..)]` attribute, and import it with `use_custom_cmd!` macro: +//! Declare your function with `#[export_cmd(..)]` attribute, and import it with [`use_custom_cmd!`] macro: //! //! ``` //! # use cmd_lib::*; @@ -270,13 +271,15 @@ //! //! ### Low-level process spawning macros //! -//! `spawn!` macro executes the whole command as a child process, returning a handle to it. By +//! [`spawn!`] macro executes the whole command as a child process, returning a handle to it. By //! default, stdin, stdout and stderr are inherited from the parent. The process will run in the -//! background, so you can run other stuff concurrently. You can call `wait()` to wait +//! background, so you can run other stuff concurrently. You can call [`wait()`](`CmdChildren::wait()`) to wait //! for the process to finish. //! -//! With `spawn_with_output!` you can get output by calling `wait_with_output()`, or even do stream -//! processing with `wait_with_pipe()`. +//! With [`spawn_with_output!`] you can get output by calling [`wait_with_output()`](`FunChildren::wait_with_output()`), or even do stream +//! processing with [`wait_with_pipe()`](`FunChildren::wait_with_pipe()`). +//! +//! There are also other useful APIs, and you can check the docs for more details. //! //! ```no_run //! # use cmd_lib::*; @@ -304,9 +307,9 @@ //! //! //! ### Macros to define, get and set thread-local global variables -//! - `tls_init!` to define thread local global variable -//! - `tls_get!` to get the value -//! - `tls_set!` to set the value +//! - [`tls_init!`] to define thread local global variable +//! - [`tls_get!`] to get the value +//! - [`tls_set!`] to set the value //! ``` //! # use cmd_lib::{ tls_init, tls_get, tls_set }; //! tls_init!(DELAY, f64, 1.0); @@ -356,15 +359,15 @@ //! //! This library tries very hard to not set global states, so parallel `cargo test` can be executed just fine. //! The only known APIs not supported in multi-thread environment are the -//! `tls_init/tls_get/tls_set` macros, and you should only use them for *thread local* variables. +//! [`tls_init`]/[`tls_get`]/[`tls_set`] macros, and you should only use them for *thread local* variables. //! pub use cmd_lib_macros::{ cmd_die, export_cmd, main, run_cmd, run_fun, spawn, spawn_with_output, use_custom_cmd, }; -/// Return type for run_fun!() macro +/// Return type for [`run_fun!()`] macro. pub type FunResult = std::io::Result; -/// Return type for run_cmd!() macro +/// Return type for [`run_cmd!()`] macro. pub type CmdResult = std::io::Result<()>; pub use child::{CmdChildren, FunChildren}; #[doc(hidden)] diff --git a/src/process.rs b/src/process.rs index 9128f06..6a9e304 100644 --- a/src/process.rs +++ b/src/process.rs @@ -19,7 +19,7 @@ use std::thread; const CD_CMD: &str = "cd"; const IGNORE_CMD: &str = "ignore"; -/// Environment for builtin or custom commands +/// Environment for builtin or custom commands. pub struct CmdEnv { stdin: CmdIn, stdout: CmdOut, @@ -29,32 +29,32 @@ pub struct CmdEnv { current_dir: PathBuf, } impl CmdEnv { - /// Returns the arguments for this command + /// Returns the arguments for this command. pub fn args(&self) -> &[String] { &self.args } - /// Fetches the environment variable key for this command + /// Fetches the environment variable key for this command. pub fn var(&self, key: &str) -> Option<&String> { self.vars.get(key) } - /// Returns the current working directory for this command + /// Returns the current working directory for this command. pub fn current_dir(&self) -> &Path { &self.current_dir } - /// Returns a new handle to the standard input for this command + /// Returns a new handle to the standard input for this command. pub fn stdin(&mut self) -> impl Read + '_ { &mut self.stdin } - /// Returns a new handle to the standard output for this command + /// Returns a new handle to the standard output for this command. pub fn stdout(&mut self) -> impl Write + '_ { &mut self.stdout } - /// Returns a new handle to the standard error for this command + /// Returns a new handle to the standard error for this command. pub fn stderr(&mut self) -> impl Write + '_ { &mut self.stderr } @@ -83,14 +83,14 @@ pub fn export_cmd(cmd: &'static str, func: FnFun) { CMD_MAP.lock().unwrap().insert(OsString::from(cmd), func); } -/// Set debug mode or not, false by default +/// Set debug mode or not, false by default. /// /// Setting environment variable CMD_LIB_DEBUG=0|1 has the same effect pub fn set_debug(enable: bool) { std::env::set_var("CMD_LIB_DEBUG", if enable { "1" } else { "0" }); } -/// Set pipefail or not, true by default +/// Set pipefail or not, true by default. /// /// Setting environment variable CMD_LIB_PIPEFAIL=0|1 has the same effect pub fn set_pipefail(enable: bool) { diff --git a/src/thread_local.rs b/src/thread_local.rs index 3869f5c..95f121d 100644 --- a/src/thread_local.rs +++ b/src/thread_local.rs @@ -1,4 +1,4 @@ -/// Declare a new thread local storage variable +/// Declare a new thread local storage variable. /// ``` /// # use cmd_lib::*; /// use std::collections::HashMap; @@ -15,7 +15,7 @@ macro_rules! tls_init { }; } -/// Get the value of a thread local storage variable +/// Get the value of a thread local storage variable. /// /// ``` /// # use cmd_lib::*; @@ -35,7 +35,7 @@ macro_rules! tls_get { }; } -/// Set the value of a thread local storage variable +/// Set the value of a thread local storage variable. /// ``` /// # use cmd_lib::*; /// # let changes = "";