-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
stdout and stderr realtime logic elevated into own feature
- Loading branch information
Showing
12 changed files
with
368 additions
and
142 deletions.
There are no files selected for viewing
This file contains 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 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,3 @@ | ||
mod spawn_builder; | ||
|
||
pub use spawn_builder::*; |
This file contains 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,189 @@ | ||
use std::{future::Future, process::Stdio, sync::Arc}; | ||
|
||
use futures::future::BoxFuture; | ||
|
||
/// Extension trait for getting the spawn extension builder from both std and tokio commands. | ||
pub trait CmdSpawnExt { | ||
/// The type of the spawn extension builder. | ||
type Spawner<'a> | ||
where | ||
Self: 'a; | ||
|
||
/// Get a spawn extension builder that provides the ability to: | ||
/// - Listen to stdout and/or stderr line by line in real-time via callbacks. | ||
fn spawn_builder(&mut self) -> Self::Spawner<'_>; | ||
} | ||
|
||
impl CmdSpawnExt for std::process::Command { | ||
type Spawner<'a> = CmdSpawnBuilderSync<'a>; | ||
|
||
fn spawn_builder(&mut self) -> Self::Spawner<'_> { | ||
CmdSpawnBuilderSync { | ||
command: self, | ||
on_stdout: None, | ||
on_stderr: None, | ||
} | ||
} | ||
} | ||
|
||
impl CmdSpawnExt for tokio::process::Command { | ||
type Spawner<'a> = CmdSpawnBuilderAsync<'a>; | ||
|
||
fn spawn_builder(&mut self) -> Self::Spawner<'_> { | ||
CmdSpawnBuilderAsync { | ||
command: self, | ||
on_stdout: None, | ||
on_stderr: None, | ||
} | ||
} | ||
} | ||
|
||
/// Synchronous command spawn extension builder. For [`std::process::Command`]. | ||
pub struct CmdSpawnBuilderSync<'a> { | ||
command: &'a mut std::process::Command, | ||
on_stdout: Option<Box<dyn Fn(String) + Sync + Send + 'static>>, | ||
on_stderr: Option<Arc<Box<dyn Fn(String) + Sync + Send + 'static>>>, | ||
} | ||
|
||
impl<'a> CmdSpawnBuilderSync<'a> { | ||
/// Set a callback to be called for each line of stdout. | ||
pub fn on_stdout(mut self, on_stdout: impl Fn(String) + Sync + Send + 'static) -> Self { | ||
self.on_stdout = Some(Box::new(on_stdout)); | ||
self.command.stdout(Stdio::piped()); | ||
self | ||
} | ||
|
||
/// Set a callback to be called for each line of stderr. | ||
pub fn on_stderr(mut self, on_stderr: impl Fn(String) + Sync + Send + 'static) -> Self { | ||
self.on_stderr = Some(Arc::new(Box::new(on_stderr))); | ||
self.command.stderr(Stdio::piped()); | ||
self | ||
} | ||
|
||
/// Spawn the command. | ||
pub fn spawn(self) -> std::io::Result<std::process::Child> { | ||
let mut child = self.command.spawn()?; | ||
|
||
use std::io::BufRead; | ||
|
||
// Capture and print stdout in a separate thread | ||
if let Some(on_stdout) = self.on_stdout { | ||
let on_stderr = self.on_stderr.as_ref().map(|on_stderr| on_stderr.clone()); | ||
if let Some(stdout) = child.stdout.take() { | ||
let stdout_reader = std::io::BufReader::new(stdout); | ||
std::thread::spawn(move || { | ||
for line in stdout_reader.lines() { | ||
match line { | ||
Ok(line) => on_stdout(line), | ||
Err(e) => { | ||
let msg = format!("Error reading stdout: {:?}", e); | ||
if let Some(on_stderr) = on_stderr.as_ref() { | ||
on_stderr(msg); | ||
} else { | ||
on_stdout(msg); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
|
||
// Capture and print stderr in a separate thread | ||
if let Some(on_stderr) = self.on_stderr { | ||
if let Some(stderr) = child.stderr.take() { | ||
let stderr_reader = std::io::BufReader::new(stderr); | ||
std::thread::spawn(move || { | ||
for line in stderr_reader.lines() { | ||
match line { | ||
Ok(line) => on_stderr(line), | ||
Err(e) => on_stderr(format!("Error reading stderr: {:?}", e)), | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
|
||
Ok(child) | ||
} | ||
} | ||
|
||
/// Asynchronous command spawn extension builder. For [`tokio::process::Command`]. | ||
pub struct CmdSpawnBuilderAsync<'a> { | ||
command: &'a mut tokio::process::Command, | ||
on_stdout: Option<Box<dyn Fn(String) -> BoxFuture<'static, ()> + Send + 'static>>, | ||
on_stderr: Option<Box<dyn Fn(String) -> BoxFuture<'static, ()> + Send + 'static>>, | ||
} | ||
|
||
impl<'a> CmdSpawnBuilderAsync<'a> { | ||
/// Set a callback to be called for each line of stdout. | ||
pub fn on_stdout<Fut: Future<Output = ()> + Send + 'static>( | ||
mut self, | ||
on_stdout: impl Fn(String) -> Fut + Send + 'static, | ||
) -> Self { | ||
self.on_stdout = Some(Box::new(move |s| Box::pin(on_stdout(s)))); | ||
self.command.stdout(Stdio::piped()); | ||
self | ||
} | ||
|
||
/// Set a callback to be called for each line of stderr. | ||
pub fn on_stderr<Fut: Future<Output = ()> + Send + 'static>( | ||
mut self, | ||
on_stderr: impl Fn(String) -> Fut + Send + 'static, | ||
) -> Self { | ||
self.on_stderr = Some(Box::new(move |s| Box::pin(on_stderr(s)))); | ||
self.command.stderr(Stdio::piped()); | ||
self | ||
} | ||
|
||
/// Spawn the command. | ||
pub fn spawn(self) -> std::io::Result<tokio::process::Child> { | ||
use tokio::io::AsyncBufReadExt; | ||
|
||
let mut child = self.command.spawn()?; | ||
|
||
// Capture and print stdout in a separate thread | ||
if let Some(on_stdout) = self.on_stdout { | ||
if let Some(stdout) = child.stdout.take() { | ||
let stdout_reader = tokio::io::BufReader::new(stdout); | ||
tokio::spawn(async move { | ||
let mut lines = stdout_reader.lines(); | ||
loop { | ||
match lines.next_line().await { | ||
Ok(v) => match v { | ||
Some(line) => on_stdout(line).await, | ||
None => break, | ||
}, | ||
Err(e) => { | ||
on_stdout(format!("Error reading stdout: {:?}", e)).await; | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
|
||
// Capture and print stderr in a separate thread | ||
if let Some(on_stderr) = self.on_stderr { | ||
if let Some(stderr) = child.stderr.take() { | ||
let stderr_reader = tokio::io::BufReader::new(stderr); | ||
tokio::spawn(async move { | ||
let mut lines = stderr_reader.lines(); | ||
loop { | ||
match lines.next_line().await { | ||
Ok(v) => match v { | ||
Some(line) => on_stderr(line).await, | ||
None => break, | ||
}, | ||
Err(e) => on_stderr(format!("Error reading stderr: {:?}", e)).await, | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
|
||
Ok(child) | ||
} | ||
} | ||
|
||
// TESTING: implicitly tested during log tests which use it to extract logs I think. |
This file contains 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,41 @@ | ||
use std::path::Path; | ||
|
||
/// Chmod an open file, noop on Windows. | ||
/// | ||
/// ENTER AS HEX! | ||
/// | ||
/// E.g. `0o755` rather than `755`. | ||
pub fn chmod_sync(mode: u32, filepath: &Path) -> Result<(), std::io::Error> { | ||
#[cfg(unix)] | ||
{ | ||
use std::os::unix::fs::PermissionsExt; | ||
std::fs::set_permissions(filepath, std::fs::Permissions::from_mode(mode))?; | ||
} | ||
Ok(()) | ||
} | ||
|
||
/// Good default, chmod an open file to be executable by all, writeable by owner. | ||
pub fn chmod_executable_sync(filepath: &Path) -> Result<(), std::io::Error> { | ||
chmod_sync(0o755, filepath) | ||
} | ||
|
||
/// Chmod an open file, noop on Windows. | ||
/// | ||
/// ENTER AS HEX! | ||
/// | ||
/// E.g. `0o755` rather than `755`. | ||
pub async fn chmod_async(mode: u32, filepath: &Path) -> Result<(), std::io::Error> { | ||
#[cfg(unix)] | ||
{ | ||
use std::os::unix::fs::PermissionsExt; | ||
tokio::fs::set_permissions(&filepath, std::fs::Permissions::from_mode(mode)).await?; | ||
} | ||
Ok(()) | ||
} | ||
|
||
/// Good default (755). | ||
/// | ||
/// chmod an open file to be executable by all, writeable by owner. | ||
pub async fn chmod_executable_async(filepath: &Path) -> Result<(), std::io::Error> { | ||
chmod_async(0o755, filepath).await | ||
} |
This file contains 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,3 @@ | ||
mod chmod; | ||
|
||
pub use chmod::*; |
This file contains 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 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 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.