use command_group::{AsyncCommandGroup, AsyncGroupChild};
use std::ffi::OsString;
use std::io;
use std::process::{ExitStatus, Stdio};
use std::time::{Duration, Instant};
use tokio::process::{ChildStderr, ChildStdin, ChildStdout, Command};
use crate::error::AdapterError;
pub const DEFAULT_INTERRUPT_GRACE: Duration = Duration::from_secs(2);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IoPlan {
pub stdin: StdioKind,
pub stdout: StdioKind,
pub stderr: StdioKind,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StdioKind {
Piped,
Null,
Inherit,
Preserve,
}
impl StdioKind {
fn apply(self, command: &mut Command, slot: StdioSlot) {
if let Some(stdio) = self.stdio() {
match slot {
StdioSlot::Stdin => {
command.stdin(stdio);
}
StdioSlot::Stdout => {
command.stdout(stdio);
}
StdioSlot::Stderr => {
command.stderr(stdio);
}
}
}
}
fn stdio(self) -> Option<Stdio> {
match self {
StdioKind::Piped => Some(Stdio::piped()),
StdioKind::Null => Some(Stdio::null()),
StdioKind::Inherit => Some(Stdio::inherit()),
StdioKind::Preserve => None,
}
}
}
enum StdioSlot {
Stdin,
Stdout,
Stderr,
}
impl Default for IoPlan {
fn default() -> Self {
Self {
stdin: StdioKind::Piped,
stdout: StdioKind::Piped,
stderr: StdioKind::Piped,
}
}
}
pub struct SupervisedChild {
child: Option<AsyncGroupChild>,
pid: Option<u32>,
reaped: bool,
}
impl SupervisedChild {
pub fn spawn(command: &mut Command, io: IoPlan) -> Result<Self, AdapterError> {
io.stdin.apply(command, StdioSlot::Stdin);
io.stdout.apply(command, StdioSlot::Stdout);
io.stderr.apply(command, StdioSlot::Stderr);
command.kill_on_drop(false);
let child = command.group_spawn().map_err(|error| {
AdapterError::io("agent_process_start_failed", "process/start", error)
})?;
let pid = child.id();
Ok(Self {
child: Some(child),
pid,
reaped: false,
})
}
fn child(&mut self) -> &mut AsyncGroupChild {
self.child.as_mut().expect("supervised child taken")
}
pub fn command(
spec: &crate::model::CommandSpec,
env: &std::collections::BTreeMap<String, OsString>,
cwd: Option<&std::path::Path>,
) -> Command {
let mut command = Command::new(&spec.program);
command.args(&spec.args).env_clear().envs(env);
match cwd.or(spec.cwd.as_deref()) {
Some(dir) => {
command.current_dir(dir);
}
None => {
command.current_dir(std::env::temp_dir());
}
}
command
}
pub fn pid(&self) -> Option<u32> {
self.pid
}
pub fn stdin(&mut self) -> Option<ChildStdin> {
self.child().inner().stdin.take()
}
pub fn stdout(&mut self) -> Option<ChildStdout> {
self.child().inner().stdout.take()
}
pub fn stderr(&mut self) -> Option<ChildStderr> {
self.child().inner().stderr.take()
}
pub async fn wait(&mut self) -> io::Result<ExitStatus> {
let status = self.child().wait().await;
if status.is_ok() {
self.reaped = true;
}
status
}
pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
match self.child().try_wait() {
Ok(Some(status)) => {
self.reaped = true;
Ok(Some(status))
}
other => other,
}
}
pub async fn wait_natural(
&mut self,
deadline: Option<Instant>,
) -> io::Result<Option<ExitStatus>> {
if self.reaped {
return Ok(None);
}
match deadline {
Some(end) => {
let remaining = end.saturating_duration_since(Instant::now());
match tokio::time::timeout(remaining, self.child().wait()).await {
Ok(status) => {
self.reaped = true;
status.map(Some)
}
Err(_) => Ok(None),
}
}
None => self.wait().await.map(Some),
}
}
#[cfg(unix)]
pub async fn interrupt(&mut self, grace: Duration) -> Result<Option<ExitStatus>, AdapterError> {
if let Some(status) = self.try_wait().map_err(|error| {
AdapterError::io("agent_process_wait_failed", "process/interrupt", error)
})? {
return Ok(Some(status));
}
if let Some(pid) = self.pid {
let result = unsafe { libc::killpg(pid as libc::pid_t, libc::SIGINT) };
if result != 0 {
let error = io::Error::last_os_error();
if !matches!(
error.raw_os_error(),
Some(code) if code == libc::ESRCH || code == libc::EPERM
) {
return Err(AdapterError::io(
"agent_process_interrupt_failed",
"process/interrupt",
error,
));
}
}
}
match self
.wait_natural(Some(Instant::now() + grace))
.await
.map_err(|error| {
AdapterError::io("agent_process_wait_failed", "process/interrupt", error)
})? {
Some(status) => Ok(Some(status)),
None => self.terminate().await,
}
}
#[cfg(not(unix))]
pub async fn interrupt(
&mut self,
_grace: Duration,
) -> Result<Option<ExitStatus>, AdapterError> {
self.terminate().await
}
pub async fn terminate(&mut self) -> Result<Option<ExitStatus>, AdapterError> {
if let Some(status) = self.try_wait().map_err(|error| {
AdapterError::io("agent_process_wait_failed", "process/terminate", error)
})? {
return Ok(Some(status));
}
if let Err(error) = self.child().start_kill() {
if !process_gone(&error) {
return Err(AdapterError::io(
"agent_process_terminate_failed",
"process/terminate",
error,
));
}
}
let status = self.child().wait().await.map_err(|error| {
AdapterError::io("agent_process_wait_failed", "process/terminate", error)
})?;
self.reaped = true;
Ok(Some(status))
}
}
#[cfg(unix)]
fn process_gone(error: &io::Error) -> bool {
error
.raw_os_error()
.is_some_and(|code| code == libc::ESRCH || code == libc::EPERM)
}
#[cfg(not(unix))]
fn process_gone(error: &io::Error) -> bool {
matches!(
error.kind(),
io::ErrorKind::InvalidInput | io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
)
}
impl Drop for SupervisedChild {
fn drop(&mut self) {
if self.reaped {
return;
}
if let Some(mut child) = self.child.take() {
let _ = child.start_kill();
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn(async move {
let _ = child.wait().await;
});
}
}
}
}