use std::io::Write;
use std::path::Path;
use std::process::ExitStatus;
use std::time::Duration;
use tokio::process::{Child, Command};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use crate::io::{CommandIo, OutTarget};
const GRACE_PERIOD: Duration = Duration::from_secs(5);
const DRAIN_PERIOD: Duration = Duration::from_secs(2);
pub(crate) async fn run_external(
path: &Path,
args: &[String],
env: &[(String, String)],
cwd: &Path,
io: CommandIo,
cancel: &CancellationToken,
) -> i32 {
let CommandIo {
stdin,
stdout,
stderr,
} = io;
let error_sink = stderr.try_clone();
let (stdout_stdio, stdout_sink) = stdout.into_stdio();
let (stderr_stdio, stderr_sink) = stderr.into_stdio();
let mut command = build_command(path, args, env, cwd);
command
.stdin(stdin.into_stdio())
.stdout(stdout_stdio)
.stderr(stderr_stdio);
let spawned = command.spawn();
drop(command);
let mut child = match spawned {
Ok(child) => child,
Err(error) => return report_spawn_failure(error_sink, path, &error),
};
drop(error_sink);
let mut stdout_task = stdout_sink.map(|sink| {
let handle = child
.stdout
.take()
.expect("child spawned with piped stdout");
tokio::spawn(sink.forward(handle))
});
let mut stderr_task = stderr_sink.map(|sink| {
let handle = child
.stderr
.take()
.expect("child spawned with piped stderr");
tokio::spawn(sink.forward(handle))
});
let status: Option<ExitStatus> = tokio::select! {
result = child.wait() => result.ok(),
_ = cancel.cancelled() => terminate(&mut child).await,
};
let drain = async {
tokio::join!(join(stdout_task.as_mut()), join(stderr_task.as_mut()));
};
if tokio::time::timeout(DRAIN_PERIOD, drain).await.is_err() {
abort(stdout_task.as_ref());
abort(stderr_task.as_ref());
}
status.and_then(|s| s.code()).unwrap_or(-1)
}
async fn join(task: Option<&mut JoinHandle<()>>) {
if let Some(task) = task {
let _ = task.await;
}
}
fn abort(task: Option<&JoinHandle<()>>) {
if let Some(task) = task {
task.abort();
}
}
fn report_spawn_failure(
sink: std::io::Result<OutTarget>,
path: &Path,
error: &std::io::Error,
) -> i32 {
if let Ok(sink) = sink {
let mut writer = sink.writer();
let _ = writeln!(
writer,
"alba-shell: failed to spawn {}: {error}",
path.display()
);
}
126
}
fn build_command(path: &Path, args: &[String], env: &[(String, String)], cwd: &Path) -> Command {
let mut command = Command::new(path);
command.args(args);
#[cfg(unix)]
command.process_group(0);
command
.current_dir(cwd)
.env_clear()
.envs(env.iter().cloned());
command
}
async fn terminate(child: &mut Child) -> Option<ExitStatus> {
send_terminate_signal(child);
match tokio::time::timeout(GRACE_PERIOD, child.wait()).await {
Ok(result) => result.ok(),
Err(_elapsed) => {
force_kill(child).await;
child.wait().await.ok()
}
}
}
#[cfg(unix)]
fn send_terminate_signal(child: &Child) {
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
if let Some(pid) = child.id() {
let _ = kill(Pid::from_raw(-(pid as i32)), Signal::SIGTERM);
}
}
#[cfg(windows)]
fn send_terminate_signal(child: &mut Child) {
let _ = child.start_kill();
}
#[cfg(unix)]
async fn force_kill(child: &Child) {
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
if let Some(pid) = child.id() {
let _ = kill(Pid::from_raw(-(pid as i32)), Signal::SIGKILL);
}
}
#[cfg(windows)]
async fn force_kill(child: &mut Child) {
let _ = child.kill().await;
}