Skip to content

Commit

Permalink
Provide API to catch all output and return code
Browse files Browse the repository at this point in the history
Fix #56 and also updated some doc links
  • Loading branch information
tao-guo committed Nov 19, 2023
1 parent 84c1cbf commit 4e5cf3e
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 89 deletions.
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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)?;
Expand Down Expand Up @@ -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

Expand All @@ -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)]
Expand All @@ -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)?;
Expand All @@ -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;
Expand Down Expand Up @@ -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
18 changes: 8 additions & 10 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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::*;
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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)?;
Expand All @@ -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::*;
///
Expand All @@ -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![];
Expand Down Expand Up @@ -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:
/// ```
Expand Down
121 changes: 80 additions & 41 deletions src/child.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<CmdChildren>`
/// Calling [`spawn!`](../cmd_lib/macro.spawn.html) macro will return `Result<CmdChildren>`
pub struct CmdChildren {
children: Vec<CmdChild>,
ignore_error: bool,
Expand Down Expand Up @@ -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<FunChildren>`
/// Calling [spawn_with_output!](../cmd_lib/macro.spawn_with_output.html) macro will return `Result<FunChildren>`
pub struct FunChildren {
children: Vec<CmdChild>,
ignore_error: bool,
Expand All @@ -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)
}
}
}
Expand All @@ -107,7 +103,7 @@ impl FunChildren {
/// provided function.
pub fn wait_with_pipe(&mut self, f: &mut dyn FnMut(Box<dyn Read>)) -> 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 {
Expand All @@ -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<u32> {
self.children.iter().filter_map(|x| x.pid()).collect()
Expand Down Expand Up @@ -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);
Expand All @@ -168,27 +175,35 @@ impl CmdChild {
Ok(())
}

fn wait_with_output(self, ignore_error: bool) -> Result<Vec<u8>> {
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 {
Expand All @@ -207,8 +222,7 @@ pub(crate) enum CmdChildHandle {
}

impl CmdChildHandle {
fn wait_with_stderr(self, stderr: Option<PipeReader>, 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();
Expand Down Expand Up @@ -242,7 +256,6 @@ impl CmdChildHandle {
}
CmdChildHandle::SyncFn => {}
}
drop(polling_stderr);
Ok(())
}

Expand Down Expand Up @@ -272,19 +285,31 @@ impl CmdChildHandle {
}
}

struct StderrLogging {
thread: Option<JoinHandle<()>>,
struct StderrThread {
thread: Option<JoinHandle<String>>,
cmd: String,
}

impl StderrLogging {
fn new(cmd: &str, stderr: Option<PipeReader>) -> Self {
impl StderrThread {
fn new(cmd: &str, stderr: Option<PipeReader>, 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(),
Expand All @@ -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();
}
}
Loading

0 comments on commit 4e5cf3e

Please sign in to comment.