use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
use std::process::Stdio;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::{Mutex as AsyncMutex, mpsc, watch};
use crate::agent::client::AgentProcess;
pub struct TokioProcess {
stdin: mpsc::UnboundedSender<Vec<u8>>,
stdout: AsyncMutex<tokio::process::ChildStdout>,
stderr: AsyncMutex<tokio::process::ChildStderr>,
exited: watch::Receiver<Option<i32>>,
killed: Arc<AtomicBool>,
}
impl AgentProcess for TokioProcess {
fn write(&self, bytes: &[u8]) -> std::io::Result<()> {
if self.killed.load(Ordering::Relaxed) {
return Err(std::io::Error::other(
"the sandbox launcher has been terminated",
));
}
self.stdin
.send(bytes.to_vec())
.map_err(|_| std::io::Error::other("the sandbox launcher has been terminated"))
}
fn read_stdout<'a>(
&'a self,
buf: &'a mut [u8],
) -> Pin<Box<dyn Future<Output = std::io::Result<usize>> + Send + 'a>> {
Box::pin(async move {
let mut pipe = self.stdout.lock().await;
pipe.read(buf).await
})
}
fn read_stderr<'a>(
&'a self,
buf: &'a mut [u8],
) -> Pin<Box<dyn Future<Output = std::io::Result<usize>> + Send + 'a>> {
Box::pin(async move {
let mut pipe = self.stderr.lock().await;
pipe.read(buf).await
})
}
fn exited(&self) -> Pin<Box<dyn Future<Output = Option<i32>> + Send>> {
let mut receiver = self.exited.clone();
Box::pin(async move {
let _ = receiver.changed().await;
*receiver.borrow()
})
}
}
pub fn spawn_agent(
command: &str,
args: &[String],
env: Option<&BTreeMap<String, String>>,
cwd: Option<&str>,
) -> std::io::Result<SpawnedAgent> {
let mut child = tokio::process::Command::new(command);
child
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env_clear()
.process_group(0);
if let Some(env) = env {
child.envs(env);
}
if let Some(cwd) = cwd {
child.current_dir(cwd);
}
let mut child = child.spawn()?;
let pid = match child.id().and_then(|id| i32::try_from(id).ok()) {
Some(pid) if pid > 0 => pid,
_ => {
let _ = child.start_kill();
return Err(std::io::Error::other(
"the sandbox launcher started without a process id",
));
}
};
let (stdin_sender, mut stdin_receiver) = mpsc::unbounded_channel::<Vec<u8>>();
let mut stdin = child.stdin.take().expect("stdin is piped");
tokio::spawn(async move {
while let Some(bytes) = stdin_receiver.recv().await {
if stdin.write_all(&bytes).await.is_err() {
break;
}
}
});
let stdout = child.stdout.take().expect("stdout is piped");
let stderr = child.stderr.take().expect("stderr is piped");
let (exit_sender, exit_receiver) = watch::channel(None);
let mut waiting = child;
tokio::spawn(async move {
let status = waiting.wait().await.ok().and_then(|status| status.code());
let _ = exit_sender.send(status);
});
let killed = Arc::new(AtomicBool::new(false));
let process = TokioProcess {
stdin: stdin_sender,
stdout: AsyncMutex::new(stdout),
stderr: AsyncMutex::new(stderr),
exited: exit_receiver,
killed: Arc::clone(&killed),
};
Ok(SpawnedAgent {
pid,
process: Arc::new(process),
killed,
})
}
pub struct SpawnedAgent {
pub pid: i32,
pub process: Arc<dyn AgentProcess>,
killed: Arc<AtomicBool>,
}
impl SpawnedAgent {
pub fn kill(&self, signal: Signal) {
self.killed.store(true, Ordering::Relaxed);
if kill(Pid::from_raw(-self.pid), signal).is_err() {
let _ = kill(Pid::from_raw(self.pid), signal);
}
}
}