diff --git a/Cargo.lock b/Cargo.lock index e967f86..5786a67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -176,6 +176,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "flate2" version = "1.1.1" @@ -360,6 +366,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "log" version = "0.4.27" @@ -540,6 +552,7 @@ dependencies = [ "indexmap", "nix 0.30.1", "remoteprocess", + "tempfile", "tokio", "tracing", "windows", @@ -631,6 +644,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ruzstd" version = "0.7.3" @@ -713,6 +739,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "tokio" version = "1.45.0" diff --git a/Cargo.toml b/Cargo.toml index 920483d..83a3f02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ windows = { version = "0.61.1", optional = true } [dev-dependencies] remoteprocess = "0.5.0" +tempfile = { version = "3.20.0", default-features = false } tokio = { version = "1.38.2", features = ["io-util", "macros", "process", "rt", "rt-multi-thread", "time"] } [features] diff --git a/src/generic_wrap.rs b/src/generic_wrap.rs index ce9c393..a2fe52a 100644 --- a/src/generic_wrap.rs +++ b/src/generic_wrap.rs @@ -91,7 +91,7 @@ macro_rules! Wrap { for (id, wrapper) in wrappers.iter_mut() { #[cfg(feature = "tracing")] ::tracing::debug!(?id, "post_spawn"); - wrapper.post_spawn(&mut child, self)?; + wrapper.post_spawn(command, &mut child, self)?; } let mut child = Box::new( @@ -204,7 +204,7 @@ macro_rules! Wrap { /// how `CreationFlags` on Windows works along with `JobObject`. /// /// Default: no-op. - fn post_spawn(&mut self, _child: &mut $child, _core: &$name) -> Result<()> { + fn post_spawn(&mut self, _command: &mut $command, _child: &mut $child, _core: &$name) -> Result<()> { Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 666c45e..eddc115 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ //! //! ```rust,no_run //! # fn main() -> std::io::Result<()> { -//! use std::process::Command; //! use process_wrap::std::*; //! //! let mut command = StdCommandWrap::with_new("watch", |command| { command.arg("ls"); }); @@ -150,6 +149,289 @@ //! If your functionality is order-dependent, make sure to specify so in your documentation! By //! default does nothing: no wrapping is performed and the input `child` is returned as-is. //! +//! ## An Example Logging Wrapper +//! +//! Let's implement a logging wrapper that redirects a `Command`'s `stdout` and `stderr` into a +//! text file. We can use `std::io::pipe` to merge `stdout` and `stderr` into one channel, then +//! `std::io::copy` in a background thread to non-blockingly stream that data to disk as it comes +//! in. +//! +//! ```rust +//! # use process_wrap::std::{StdCommandWrap, StdCommandWrapper}; +//! # use std::{fs::File, io, path::PathBuf, process::Command, thread}; +//! #[derive(Debug)] +//! struct LogFile { +//! path: PathBuf, +//! } +//! +//! impl LogFile { +//! fn new(path: impl Into) -> Self { +//! Self { path: path.into() } +//! } +//! } +//! +//! impl StdCommandWrapper for LogFile { +//! fn pre_spawn(&mut self, command: &mut Command, _core: &StdCommandWrap) -> io::Result<()> { +//! let mut logfile = File::create(&self.path)?; +//! let (mut rx, tx) = io::pipe()?; +//! +//! thread::spawn(move || { +//! io::copy(&mut rx, &mut logfile).unwrap(); +//! }); +//! +//! command.stdout(tx.try_clone()?).stderr(tx); +//! Ok(()) +//! } +//! } +//! ``` +//! +//! That's a great start, but it's actually introduced a resource leak: if the main thread of your +//! program exits before that background one does, then the background thread won't get a chance to +//! call `logfile`'s `Drop` implementation which closes the file. The file handle will be left open! +//! To fix this, we'll need to keep track of the background thread's `ThreadHandle` and `.join()` it +//! when calling `.wait()` on the `ChildWrapper`. +//! +//! ```rust +//! # use process_wrap::std::{StdChildWrapper, StdCommandWrap, StdCommandWrapper}; +//! # use std::{ +//! # fs::File, +//! # io, mem, +//! # path::PathBuf, +//! # process::{Command, ExitStatus}, +//! # thread::{self, JoinHandle}, +//! # }; +//! #[derive(Debug)] +//! struct LogFile { +//! path: PathBuf, +//! thread: Option>, +//! } +//! +//! impl LogFile { +//! fn new(path: impl Into) -> Self { +//! Self { +//! path: path.into(), +//! thread: None, +//! } +//! } +//! } +//! +//! impl StdCommandWrapper for LogFile { +//! fn pre_spawn(&mut self, command: &mut Command, _core: &StdCommandWrap) -> io::Result<()> { +//! let mut logfile = File::create(&self.path)?; +//! let (mut rx, tx) = io::pipe()?; +//! +//! self.thread = Some(thread::spawn(move || { +//! io::copy(&mut rx, &mut logfile).unwrap(); +//! })); +//! +//! command.stdout(tx.try_clone()?).stderr(tx); +//! Ok(()) +//! } +//! +//! fn wrap_child( +//! &mut self, +//! child: Box, +//! _core: &StdCommandWrap, +//! ) -> io::Result> { +//! let wrapped_child = LogFileChild { +//! inner: child, +//! thread: mem::take(&mut self.thread), +//! }; +//! Ok(Box::new(wrapped_child)) +//! } +//! } +//! +//! #[derive(Debug)] +//! struct LogFileChild { +//! inner: Box, +//! thread: Option>, +//! } +//! +//! impl StdChildWrapper for LogFileChild { +//! fn inner(&self) -> &dyn StdChildWrapper { +//! &*self.inner +//! } +//! +//! fn inner_mut(&mut self) -> &mut dyn StdChildWrapper { +//! &mut *self.inner +//! } +//! +//! fn into_inner(self: Box) -> Box { +//! self.inner +//! } +//! +//! fn wait(&mut self) -> io::Result { +//! let exit_status = self.inner.wait(); +//! +//! if let Some(thread) = mem::take(&mut self.thread) { +//! thread.join().unwrap(); +//! } +//! +//! exit_status +//! } +//! } +//! ``` +//! +//! Now we're cleaning up after ourselves, but there is one last issue: if you actually call +//! `.wait()`, then your program will deadlock! This is because `io::copy` copies data until `rx` +//! returns an EOF, but that only happens after *all* copies of `tx` are dropped. Currently, our +//! `Command` is holding onto `tx` even after calling `.spawn()`, so unless we manually drop the +//! `Command` (freeing both copies of `tx`) before calling `.wait()`, our program will deadlock! +//! We can fix this by telling `Command` to drop `tx` right after spawning the child — by this +//! point, the `ChildWrapper` will have already inherited the copies of `tx` that it needs, so +//! dropping `tx` from `Command` should be totally safe. We'll get `Command` to "drop" `tx` by +//! setting its `stdin` and `stdout` to `Stdio::null()` in `CommandWrapper::post_spawn()`. +//! +//! ```rust +//! # use process_wrap::std::{StdCommandWrap, StdCommandWrapper}; +//! # use std::{ +//! # io, +//! # path::PathBuf, +//! # process::{Child, Command, Stdio}, +//! # thread::JoinHandle, +//! # }; +//! # #[derive(Debug)] +//! # struct LogFile { +//! # path: PathBuf, +//! # thread: Option>, +//! # } +//! # +//! impl StdCommandWrapper for LogFile { +//! // ... snip ... +//! fn post_spawn( +//! &mut self, +//! command: &mut Command, +//! _child: &mut Child, +//! _core: &StdCommandWrap, +//! ) -> io::Result<()> { +//! command.stdout(Stdio::null()).stderr(Stdio::null()); +//! +//! Ok(()) +//! } +//! // ... snip ... +//! } +//! ``` +//! +//! Finally, we can test that our new command-wrapper works: +//! +//! ```rust +//! # use process_wrap::std::{StdChildWrapper, StdCommandWrap, StdCommandWrapper}; +//! # use std::{ +//! # error::Error, +//! # fs::{self, File}, +//! # io, mem, +//! # path::PathBuf, +//! # process::{Child, Command, ExitStatus, Stdio}, +//! # thread::{self, JoinHandle}, +//! # }; +//! # use tempfile::NamedTempFile; +//! # #[derive(Debug)] +//! # struct LogFile { +//! # path: PathBuf, +//! # thread: Option>, +//! # } +//! # +//! # impl LogFile { +//! # fn new(path: impl Into) -> Self { +//! # Self { +//! # path: path.into(), +//! # thread: None, +//! # } +//! # } +//! # } +//! # +//! # impl StdCommandWrapper for LogFile { +//! # fn pre_spawn(&mut self, command: &mut Command, _core: &StdCommandWrap) -> io::Result<()> { +//! # let mut logfile = File::create(&self.path)?; +//! # let (mut rx, tx) = io::pipe()?; +//! # +//! # self.thread = Some(thread::spawn(move || { +//! # io::copy(&mut rx, &mut logfile).unwrap(); +//! # })); +//! # +//! # command.stdout(tx.try_clone()?).stderr(tx); +//! # Ok(()) +//! # } +//! # +//! # fn post_spawn( +//! # &mut self, +//! # command: &mut Command, +//! # _child: &mut Child, +//! # _core: &StdCommandWrap, +//! # ) -> io::Result<()> { +//! # command.stdout(Stdio::null()).stderr(Stdio::null()); +//! # +//! # Ok(()) +//! # } +//! # +//! # fn wrap_child( +//! # &mut self, +//! # child: Box, +//! # _core: &StdCommandWrap, +//! # ) -> io::Result> { +//! # let wrapped_child = LogFileChild { +//! # inner: child, +//! # thread: mem::take(&mut self.thread), +//! # }; +//! # Ok(Box::new(wrapped_child)) +//! # } +//! # } +//! # +//! # #[derive(Debug)] +//! # struct LogFileChild { +//! # inner: Box, +//! # thread: Option>, +//! # } +//! # +//! # impl StdChildWrapper for LogFileChild { +//! # fn inner(&self) -> &dyn StdChildWrapper { +//! # &*self.inner +//! # } +//! # +//! # fn inner_mut(&mut self) -> &mut dyn StdChildWrapper { +//! # &mut *self.inner +//! # } +//! # +//! # fn into_inner(self: Box) -> Box { +//! # self.inner +//! # } +//! # +//! # fn wait(&mut self) -> io::Result { +//! # let exit_status = self.inner.wait(); +//! # +//! # if let Some(thread) = mem::take(&mut self.thread) { +//! # thread.join().unwrap(); +//! # } +//! # +//! # exit_status +//! # } +//! # } +//! # +//! fn main() -> Result<(), Box> { +//! #[cfg(windows)] +//! let mut command = StdCommandWrap::with_new("cmd", |command| { +//! command.args(["/c", "echo Hello && echo World 1>&2"]); +//! }); +//! #[cfg(unix)] +//! let mut command = StdCommandWrap::with_new("sh", |command| { +//! command.args(["-c", "echo Hello && echo World 1>&2"]); +//! }); +//! +//! let logfile = NamedTempFile::new()?; +//! let logfile_path = logfile.path(); +//! +//! command.wrap(LogFile::new(logfile_path)).spawn()?.wait()?; +//! +//! let logfile_lines: Vec = fs::read_to_string(logfile_path)? +//! .lines() +//! .map(|l| l.trim().into()) +//! .collect(); +//! assert_eq!(logfile_lines, vec!["Hello", "World"]); +//! +//! Ok(()) +//! } +//! ``` +//! //! # Features //! //! ## Frontends