-
Notifications
You must be signed in to change notification settings - Fork 9
Add CommandExt
for working with std::process::Command
output streams
#535
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
use crate::write::tee; | ||
use crossbeam_utils::thread::ScopedJoinHandle; | ||
use std::io::Write; | ||
use std::{io, process, thread}; | ||
use std::{mem, panic}; | ||
|
||
/// Extension trait for [`process::Command`] that adds functions for use within buildpacks. | ||
pub trait CommandExt { | ||
/// Spawns the command process and sends the output of stdout and stderr to the given writers. | ||
/// | ||
/// This allows for additional flexibility when dealing with these output streams compared to | ||
/// functionality that the stock [`process::Command`] provides. See the [`write`](crate::write) | ||
/// module for [`std::io::Write`] implementations designed for common buildpack tasks. | ||
/// | ||
/// This function will redirect the output unbuffered and in parallel for both streams. This | ||
/// means that it can be used to output data from these streams while the command is running, | ||
/// providing a live view into the process' output. This function will block until both streams | ||
/// have been closed. | ||
/// | ||
/// # Example: | ||
/// ```no_run | ||
/// use libherokubuildpack::command::CommandExt; | ||
/// use libherokubuildpack::write::tee; | ||
/// use std::fs; | ||
/// use std::process::Command; | ||
/// | ||
/// let logfile = fs::File::open("log.txt").unwrap(); | ||
/// let exit_status = Command::new("date") | ||
/// .spawn_and_write_streams(tee(std::io::stdout(), &logfile), std::io::stderr()) | ||
/// .and_then(|mut child| child.wait()) | ||
/// .unwrap(); | ||
/// ``` | ||
fn spawn_and_write_streams<OW: Write + Send, EW: Write + Send>( | ||
&mut self, | ||
stdout_write: OW, | ||
stderr_write: EW, | ||
) -> io::Result<process::Child>; | ||
|
||
/// Spawns the command process and sends the output of stdout and stderr to the given writers. | ||
/// | ||
/// In addition to what [`spawn_and_write_streams`](Self::spawn_and_write_streams) does, this | ||
/// function captures stdout and stderr as `Vec<u8>` and returns them after waiting for the | ||
/// process to finish. This function is meant as a drop-in replacement for existing | ||
/// `Command:output` calls. | ||
/// | ||
/// # Example: | ||
/// ```no_run | ||
/// use libherokubuildpack::command::CommandExt; | ||
/// use libherokubuildpack::write::tee; | ||
/// use std::fs; | ||
/// use std::process::Command; | ||
/// | ||
/// let logfile = fs::File::open("log.txt").unwrap(); | ||
/// let output = Command::new("date") | ||
/// .output_and_write_streams(tee(std::io::stdout(), &logfile), std::io::stderr()) | ||
/// .unwrap(); | ||
/// | ||
/// // Return value can be used as with Command::output, but the streams will also be written to | ||
/// // the given writers. | ||
/// println!( | ||
/// "Process exited with {}, stdout: {:?}, stderr: {:?}", | ||
/// output.status, output.stdout, output.stderr | ||
/// ); | ||
/// ``` | ||
fn output_and_write_streams<OW: Write + Send, EW: Write + Send>( | ||
&mut self, | ||
stdout_write: OW, | ||
stderr_write: EW, | ||
) -> io::Result<process::Output>; | ||
} | ||
|
||
impl CommandExt for process::Command { | ||
schneems marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fn spawn_and_write_streams<OW: Write + Send, EW: Write + Send>( | ||
&mut self, | ||
stdout_write: OW, | ||
stderr_write: EW, | ||
) -> io::Result<process::Child> { | ||
self.stdout(process::Stdio::piped()) | ||
.stderr(process::Stdio::piped()) | ||
.spawn() | ||
.and_then(|child| write_child_process_output(child, stdout_write, stderr_write)) | ||
} | ||
|
||
fn output_and_write_streams<OW: Write + Send, EW: Write + Send>( | ||
&mut self, | ||
stdout_write: OW, | ||
stderr_write: EW, | ||
) -> io::Result<process::Output> { | ||
let mut stdout_buffer = vec![]; | ||
let mut stderr_buffer = vec![]; | ||
|
||
self.spawn_and_write_streams( | ||
tee(&mut stdout_buffer, stdout_write), | ||
tee(&mut stderr_buffer, stderr_write), | ||
) | ||
.and_then(|mut child| child.wait()) | ||
.map(|status| process::Output { | ||
status, | ||
stdout: stdout_buffer, | ||
stderr: stderr_buffer, | ||
}) | ||
} | ||
} | ||
|
||
fn write_child_process_output<OW: Write + Send, EW: Write + Send>( | ||
mut child: process::Child, | ||
mut stdout_writer: OW, | ||
mut stderr_writer: EW, | ||
) -> io::Result<process::Child> { | ||
// Copying the data to the writers happens in separate threads for stdout and stderr to ensure | ||
// they're processed in parallel. Example: imagine the caller uses io::stdout() and io::stderr() | ||
// as the writers so that the user can follow along with the command's output. If we copy stdout | ||
// first and then stderr afterwards, interleaved stdout and stderr messages will no longer be | ||
// interleaved (stderr output is always printed after stdout has been closed). | ||
// | ||
// The rust compiler currently cannot figure out how long a thread will run (doesn't take the | ||
// almost immediate join calls into account) and therefore requires that data used in a thread | ||
// lives forever. To avoid requiring 'static lifetimes for the writers, we use crossbeam's | ||
// scoped threads here. This enables writers that write, for example, to a mutable buffer. | ||
unwind_panic(crossbeam_utils::thread::scope(|scope| { | ||
let stdout_copy_thread = mem::take(&mut child.stdout) | ||
.map(|mut stdout| scope.spawn(move |_| std::io::copy(&mut stdout, &mut stdout_writer))); | ||
|
||
let stderr_copy_thread = mem::take(&mut child.stderr) | ||
.map(|mut stderr| scope.spawn(move |_| std::io::copy(&mut stderr, &mut stderr_writer))); | ||
|
||
let stdout_copy_result = stdout_copy_thread.map_or_else(|| Ok(0), join_and_unwind_panic); | ||
let stderr_copy_result = stderr_copy_thread.map_or_else(|| Ok(0), join_and_unwind_panic); | ||
|
||
// Return the first error from either Result, or the child process value | ||
stdout_copy_result.and(stderr_copy_result).map(|_| child) | ||
})) | ||
Malax marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
fn join_and_unwind_panic<T>(h: ScopedJoinHandle<T>) -> T { | ||
unwind_panic(h.join()) | ||
} | ||
|
||
fn unwind_panic<T>(t: thread::Result<T>) -> T { | ||
match t { | ||
Ok(value) => value, | ||
Err(err) => panic::resume_unwind(err), | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use crate::command::CommandExt; | ||
use std::process::Command; | ||
|
||
#[test] | ||
#[cfg(unix)] | ||
fn test_spawn_and_write_streams() { | ||
let mut stdout_buf = vec![]; | ||
let mut stderr_buf = vec![]; | ||
|
||
Command::new("echo") | ||
.args(["-n", "Hello World!"]) | ||
.spawn_and_write_streams(&mut stdout_buf, &mut stderr_buf) | ||
.and_then(|mut child| child.wait()) | ||
.unwrap(); | ||
|
||
assert_eq!(stdout_buf, "Hello World!".as_bytes()); | ||
assert_eq!(stderr_buf, Vec::<u8>::new()); | ||
} | ||
|
||
#[test] | ||
#[cfg(unix)] | ||
fn test_output_and_write_streams() { | ||
let mut stdout_buf = vec![]; | ||
let mut stderr_buf = vec![]; | ||
|
||
let output = Command::new("echo") | ||
.args(["-n", "Hello World!"]) | ||
.output_and_write_streams(&mut stdout_buf, &mut stderr_buf) | ||
.unwrap(); | ||
|
||
assert_eq!(stdout_buf, "Hello World!".as_bytes()); | ||
assert_eq!(stderr_buf, Vec::<u8>::new()); | ||
|
||
assert_eq!(output.status.code(), Some(0)); | ||
assert_eq!(output.stdout, "Hello World!".as_bytes()); | ||
assert_eq!(output.stderr, Vec::<u8>::new()); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.