use std::collections::HashMap;
use std::ffi::OsString;
use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::{mpsc, watch};
use tokio::task::JoinHandle;
use crate::signal::{DEFAULT_KILL_GRACE_MS, DEFAULT_KILL_SIGNAL};
use crate::stream_process::{
run_streaming_process, ChildEnv, GraceWindows, ProcessSetup, StreamChannels, StreamingCommand,
};
use crate::stream_stdin::{StdinRequest, StdinSender};
use crate::trace::trace_lazy;
use crate::{CommandResult, Result};
pub(crate) const DEFAULT_EXIT_PUMP_GRACE_MS: u64 = 100;
#[derive(Debug, Clone)]
pub enum OutputChunk {
Stdout(Vec<u8>),
Stderr(Vec<u8>),
Exit(i32),
}
pub struct StreamingRunner {
command: StreamingCommand,
cwd: Option<PathBuf>,
env: Option<HashMap<String, String>>,
env_clear: bool,
env_remove: Vec<String>,
prefer_local: crate::PreferLocal,
stdin_content: Option<Vec<u8>>,
stdin_pipe: bool,
timeout: Option<Duration>,
kill_signal: String,
kill_grace_ms: u64,
exit_pump_grace_ms: u64,
}
impl StreamingRunner {
pub fn new(command: impl Into<String>) -> Self {
Self::with_command(StreamingCommand::Shell(command.into()))
}
pub fn from_argv<P, I, S>(program: P, args: I) -> Self
where
P: Into<OsString>,
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
Self::with_command(StreamingCommand::Argv {
program: program.into(),
args: args.into_iter().map(Into::into).collect(),
})
}
pub(crate) fn with_command(command: StreamingCommand) -> Self {
StreamingRunner {
command,
cwd: None,
env: None,
env_clear: false,
env_remove: Vec::new(),
prefer_local: crate::PreferLocal::Off,
stdin_content: None,
stdin_pipe: false,
timeout: None,
kill_signal: DEFAULT_KILL_SIGNAL.to_string(),
kill_grace_ms: DEFAULT_KILL_GRACE_MS,
exit_pump_grace_ms: DEFAULT_EXIT_PUMP_GRACE_MS,
}
}
pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
self.cwd = Some(path.into());
self
}
pub fn env(mut self, env: HashMap<String, String>) -> Self {
self.env = Some(env);
self
}
pub fn prefer_local(mut self, preference: crate::PreferLocal) -> Self {
self.prefer_local = preference;
self
}
pub fn stdin(mut self, content: impl Into<String>) -> Self {
self.stdin_content = Some(content.into().into_bytes());
self
}
pub fn stdin_bytes(mut self, content: impl Into<Vec<u8>>) -> Self {
self.stdin_content = Some(content.into());
self
}
pub fn clear_env(mut self, clear: bool) -> Self {
self.env_clear = clear;
self
}
pub fn env_remove(mut self, name: impl Into<String>) -> Self {
self.env_remove.push(name.into());
self
}
pub fn env_remove_all<I, S>(mut self, names: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.env_remove.extend(names.into_iter().map(Into::into));
self
}
pub fn stdin_pipe(mut self) -> Self {
self.stdin_pipe = true;
self
}
pub fn timeout(mut self, limit: Duration) -> Self {
self.timeout = Some(limit);
self
}
pub fn kill_signal(mut self, signal: impl Into<String>) -> Self {
self.kill_signal = signal.into();
self
}
pub fn kill_grace_ms(mut self, ms: u64) -> Self {
self.kill_grace_ms = ms;
self
}
pub fn exit_pump_grace_ms(mut self, ms: u64) -> Self {
self.exit_pump_grace_ms = ms;
self
}
fn take_child_env(&mut self) -> (Option<PathBuf>, ChildEnv) {
let cwd = self.cwd.take();
let mut vars = self.env.take();
let local_cwd = cwd
.clone()
.or_else(|| std::env::current_dir().ok())
.unwrap_or_else(|| PathBuf::from("."));
if let Some((key, path)) =
crate::local_bin::preferred_path(vars.as_ref(), &local_cwd, &self.prefer_local)
{
vars.get_or_insert_with(|| std::env::vars().collect())
.insert(key, path);
}
let env = ChildEnv {
vars,
clear: self.env_clear,
remove: std::mem::take(&mut self.env_remove),
};
(cwd, env)
}
pub fn detach(mut self) -> Result<Option<u32>> {
let (cwd, env) = self.take_child_env();
crate::detach::spawn_detached(&self.command, cwd.as_deref(), &env)
}
pub(crate) fn spawn(mut self) -> (OutputStream, JoinHandle<Result<()>>) {
let (tx, rx) = mpsc::channel(1024);
let (kill_tx, kill_rx) = mpsc::unbounded_channel::<String>();
let (pid_tx, pid_rx) = watch::channel(None);
let (exit_signal_tx, exit_signal_rx) = watch::channel(None);
let command = self.command.clone();
let (cwd, env) = self.take_child_env();
let (timed_out_tx, timed_out_rx) = watch::channel(false);
let (stdin_tx, stdin_requests) = if self.stdin_pipe {
let (sender, receiver) = crate::stream_stdin::channel();
(Some(sender), Some(receiver))
} else {
(None, None)
};
let setup = ProcessSetup {
command,
cwd,
env,
stdin_content: self.stdin_content.take(),
stdin_requests,
timeout: self.timeout,
kill_signal: self.kill_signal.clone(),
};
let grace = GraceWindows {
exit_pump_ms: self.exit_pump_grace_ms,
kill_ms: self.kill_grace_ms,
};
let kill_signal = self.kill_signal.clone();
let task = tokio::spawn(async move {
let channels = StreamChannels {
output_tx: tx,
kill_rx,
pid_tx,
exit_signal_tx,
timed_out_tx,
};
let result = run_streaming_process(setup, grace, channels).await;
if let Err(error) = &result {
trace_lazy("StreamingRunner", || format!("Error: {error}"));
}
result
});
(
OutputStream {
rx,
kill_tx,
kill_signal,
killed: false,
pid_rx,
exit_signal_rx,
timed_out_rx,
stdin_tx,
},
task,
)
}
pub fn stream(self) -> OutputStream {
self.spawn().0
}
pub async fn collect(self) -> Result<CommandResult> {
let stdin_content = self.stdin_content.clone();
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let mut exit_code = 0;
let (mut stream, task) = self.spawn();
while let Some(chunk) = stream.rx.recv().await {
match chunk {
OutputChunk::Stdout(data) => stdout.extend(data),
OutputChunk::Stderr(data) => stderr.extend(data),
OutputChunk::Exit(code) => exit_code = code,
}
}
task.await.map_err(|error| {
std::io::Error::other(format!("streaming process task failed: {error}"))
})??;
let mut result = CommandResult::new(
String::from_utf8_lossy(&stdout).to_string(),
String::from_utf8_lossy(&stderr).to_string(),
exit_code,
);
if let Some(content) = stdin_content {
result.stdin = crate::result_streams::CapturedInput::new(content);
}
Ok(result)
}
pub fn collect_blocking(self) -> Result<CommandResult> {
if tokio::runtime::Handle::try_current().is_ok() {
return Err(std::io::Error::other(
"collect_blocking cannot run inside a Tokio runtime; use collect().await",
)
.into());
}
let runtime = tokio::runtime::Runtime::new()?;
runtime.block_on(self.collect())
}
}
pub struct OutputStream {
rx: mpsc::Receiver<OutputChunk>,
kill_tx: mpsc::UnboundedSender<String>,
kill_signal: String,
killed: bool,
pid_rx: watch::Receiver<Option<u32>>,
exit_signal_rx: watch::Receiver<Option<String>>,
timed_out_rx: watch::Receiver<bool>,
stdin_tx: Option<StdinSender>,
}
impl OutputStream {
pub async fn next(&mut self) -> Option<OutputChunk> {
self.rx.recv().await
}
pub fn exit_signal(&self) -> Option<String> {
self.exit_signal_rx.borrow().clone()
}
pub fn timed_out(&self) -> bool {
*self.timed_out_rx.borrow()
}
pub async fn write_stdin(&self, data: impl AsRef<[u8]>) -> Result<()> {
let data = data.as_ref().to_vec();
crate::stream_stdin::request(self.stdin_tx.as_ref(), |ack| StdinRequest::Write(data, ack))
.await?;
Ok(())
}
pub async fn close_stdin(&mut self) -> Result<()> {
let Some(sender) = self.stdin_tx.take() else {
return Ok(());
};
match crate::stream_stdin::request(Some(&sender), StdinRequest::Close).await {
Err(error) if error.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
other => Ok(other?),
}
}
pub fn pid(&self) -> Option<u32> {
*self.pid_rx.borrow()
}
pub async fn wait_for_pid(&mut self) -> Option<u32> {
match self.pid_rx.wait_for(|pid| pid.is_some()).await {
Ok(pid) => *pid,
Err(_) => None,
}
}
pub fn kill(&mut self) {
let signal = self.kill_signal.clone();
self.kill_with(&signal);
}
pub fn kill_with(&mut self, signal: &str) {
if self.killed {
return;
}
self.killed = true;
trace_lazy("OutputStream", || format!("kill | signal={}", signal));
let _ = self.kill_tx.send(signal.to_string());
}
pub async fn collect(mut self) -> (Vec<u8>, Vec<u8>, i32) {
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let mut exit_code = 0;
while let Some(chunk) = self.rx.recv().await {
match chunk {
OutputChunk::Stdout(data) => stdout.extend(data),
OutputChunk::Stderr(data) => stderr.extend(data),
OutputChunk::Exit(code) => exit_code = code,
}
}
(stdout, stderr, exit_code)
}
pub async fn collect_stdout(mut self) -> Vec<u8> {
let mut stdout = Vec::new();
while let Some(chunk) = self.rx.recv().await {
if let OutputChunk::Stdout(data) = chunk {
stdout.extend(data);
}
}
stdout
}
}
impl Drop for OutputStream {
fn drop(&mut self) {
if !self.killed {
let _ = self.kill_tx.send(self.kill_signal.clone());
}
}
}
#[async_trait::async_trait]
pub trait AsyncIterator {
type Item;
async fn next(&mut self) -> Option<Self::Item>;
}
#[async_trait::async_trait]
impl AsyncIterator for OutputStream {
type Item = OutputChunk;
async fn next(&mut self) -> Option<Self::Item> {
self.rx.recv().await
}
}
pub trait IntoStream {
fn into_stream(self) -> OutputStream;
}
impl IntoStream for crate::ProcessRunner {
fn into_stream(self) -> OutputStream {
let streaming = StreamingRunner::new(self.command().to_string());
streaming.stream()
}
}