use std::collections::HashMap;
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
use tokio::process::{Child, Command};
use tokio::sync::{mpsc, watch};
use tokio::task::JoinHandle;
use crate::signal::{send_signal_to_process, signal_exit_code, Delivery, DEFAULT_KILL_SIGNAL};
use crate::stream::OutputChunk;
use crate::stream_stdin::{self, StdinReceiver};
use crate::trace::trace_lazy;
use crate::Result;
#[derive(Clone)]
pub(crate) enum StreamingCommand {
Shell(String),
Argv {
program: OsString,
args: Vec<OsString>,
},
}
impl StreamingCommand {
pub(crate) fn command(&self, env: Option<&HashMap<String, String>>) -> Command {
match self {
StreamingCommand::Shell(command) => crate::utils::shell_command(command, env),
StreamingCommand::Argv { program, args } => {
let mut cmd = Command::new(program);
cmd.args(args);
cmd
}
}
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ChildEnv {
pub(crate) vars: Option<HashMap<String, String>>,
pub(crate) clear: bool,
pub(crate) remove: Vec<String>,
}
impl ChildEnv {
pub(crate) fn apply(&self, cmd: &mut std::process::Command) {
if self.clear {
cmd.env_clear();
}
if let Some(vars) = &self.vars {
cmd.envs(vars);
}
for name in &self.remove {
cmd.env_remove(name);
}
}
}
pub(crate) struct ProcessSetup {
pub(crate) command: StreamingCommand,
pub(crate) cwd: Option<PathBuf>,
pub(crate) env: ChildEnv,
pub(crate) stdin_content: Option<Vec<u8>>,
pub(crate) stdin_requests: Option<StdinReceiver>,
pub(crate) timeout: Option<Duration>,
pub(crate) kill_signal: String,
}
pub(crate) struct StreamChannels {
pub(crate) output_tx: mpsc::Sender<OutputChunk>,
pub(crate) kill_rx: mpsc::UnboundedReceiver<String>,
pub(crate) pid_tx: watch::Sender<Option<u32>>,
pub(crate) exit_signal_tx: watch::Sender<Option<String>>,
pub(crate) timed_out_tx: watch::Sender<bool>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct GraceWindows {
pub(crate) exit_pump_ms: u64,
pub(crate) kill_ms: u64,
}
pub(crate) async fn run_streaming_process(
setup: ProcessSetup,
grace: GraceWindows,
channels: StreamChannels,
) -> Result<()> {
let ProcessSetup {
command,
cwd,
env,
stdin_content,
stdin_requests,
timeout,
kill_signal,
} = setup;
let StreamChannels {
output_tx: tx,
mut kill_rx,
pid_tx,
exit_signal_tx,
timed_out_tx,
} = channels;
trace_lazy("StreamingRunner", || match &command {
StreamingCommand::Shell(command) => format!("Starting: {command}"),
StreamingCommand::Argv { program, args } => {
format!("Starting argv command: {program:?} {args:?}")
}
});
let mut cmd = command.command(env.vars.as_ref());
cmd.kill_on_drop(true);
if stdin_content.is_some() || stdin_requests.is_some() {
cmd.stdin(Stdio::piped());
} else {
cmd.stdin(Stdio::null());
}
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
#[cfg(unix)]
cmd.process_group(0);
if let Some(ref cwd) = cwd {
cmd.current_dir(cwd);
}
env.apply(cmd.as_std_mut());
let mut child = cmd.spawn()?;
let _ = pid_tx.send(child.id());
let stdin_handle = child.stdin.take().map(|stdin| {
let requests = stdin_requests.unwrap_or_else(|| stream_stdin::channel().1);
stream_stdin::serve(stdin, stdin_content, requests)
});
let stdout_handle = child
.stdout
.take()
.map(|stdout| spawn_reader(stdout, tx.clone(), OutputChunk::Stdout));
let stderr_handle = child
.stderr
.take()
.map(|stderr| spawn_reader(stderr, tx.clone(), OutputChunk::Stderr));
let deadline = async {
match timeout {
Some(limit) => tokio::time::sleep(limit).await,
None => std::future::pending().await,
}
};
tokio::pin!(deadline);
let code;
tokio::select! {
status = child.wait() => {
let status = status?;
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
let signal = status.signal().and_then(|number| {
nix::sys::signal::Signal::try_from(number).ok().map(|signal| signal.to_string())
});
let _ = exit_signal_tx.send(signal);
}
code = status_to_code(status);
}
maybe_signal = kill_rx.recv() => {
let signal = maybe_signal.unwrap_or_else(|| DEFAULT_KILL_SIGNAL.to_string());
trace_lazy("StreamingRunner", || format!("Kill requested | signal={}", signal));
code = stop_child(&mut child, &signal, grace.kill_ms, &exit_signal_tx).await;
}
() = &mut deadline => {
trace_lazy("StreamingRunner", || {
format!("Timeout elapsed | timeout={timeout:?} signal={kill_signal}")
});
let _ = timed_out_tx.send(true);
code = stop_child(&mut child, &kill_signal, grace.kill_ms, &exit_signal_tx).await;
}
}
if let Some(handle) = stdin_handle {
handle.abort();
let _ = handle.await;
}
let stdout_abort = stdout_handle.as_ref().map(|h| h.abort_handle());
let stderr_abort = stderr_handle.as_ref().map(|h| h.abort_handle());
let drain = async {
if let Some(handle) = stdout_handle {
let _ = handle.await;
}
if let Some(handle) = stderr_handle {
let _ = handle.await;
}
};
if tokio::time::timeout(Duration::from_millis(grace.exit_pump_ms), drain)
.await
.is_err()
{
if let Some(abort) = stdout_abort {
abort.abort();
}
if let Some(abort) = stderr_abort {
abort.abort();
}
}
let _ = tx.send(OutputChunk::Exit(code)).await;
trace_lazy("StreamingRunner", || format!("Exited with code: {}", code));
Ok(())
}
fn spawn_reader<R>(
pipe: R,
tx: mpsc::Sender<OutputChunk>,
wrap: fn(Vec<u8>) -> OutputChunk,
) -> JoinHandle<()>
where
R: AsyncRead + Unpin + Send + 'static,
{
tokio::spawn(async move {
let mut reader = BufReader::new(pipe);
let mut buf = vec![0u8; 8192];
loop {
match reader.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(n) => {
if tx.send(wrap(buf[..n].to_vec())).await.is_err() {
break;
}
}
}
}
})
}
async fn stop_child(
child: &mut Child,
signal: &str,
grace_ms: u64,
exit_signal_tx: &watch::Sender<Option<String>>,
) -> i32 {
let pid = child.id();
if cfg!(unix) && grace_ms > 0 && signal != "SIGKILL" {
if let Some(pid) = pid {
send_signal_to_process(pid, signal, Delivery::ProcessAndGroup);
}
tokio::time::sleep(Duration::from_millis(grace_ms)).await;
}
if let Some(pid) = pid {
send_signal_to_process(pid, "SIGKILL", Delivery::ProcessAndGroup);
}
let _ = child.start_kill();
let _ = child.wait().await;
let _ = exit_signal_tx.send(Some(signal.to_string()));
signal_exit_code(signal)
}
fn status_to_code(status: std::process::ExitStatus) -> i32 {
if let Some(code) = status.code() {
return code;
}
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(sig) = status.signal() {
return 128 + sig;
}
}
-1
}