use anyhow::Result;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::io::AsyncReadExt;
use tokio::time::Duration;
#[derive(Debug)]
pub struct ExecResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
pub timed_out: bool,
}
pub struct NoSandbox {
pub working_dir: PathBuf,
}
impl NoSandbox {
pub fn new(working_dir: PathBuf) -> Self {
NoSandbox { working_dir }
}
pub async fn exec(&self, command: &str, timeout_secs: u64) -> Result<ExecResult> {
let mut child = tokio::process::Command::new("sh")
.arg("-c")
.arg(command)
.current_dir(&self.working_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut stdout_handle = child.stdout.take().expect("stdout piped");
let mut stderr_handle = child.stderr.take().expect("stderr piped");
let timeout_dur = Duration::from_secs(timeout_secs);
let read_stdout = async {
let mut buf = Vec::new();
let _ = stdout_handle.read_to_end(&mut buf).await;
buf
};
let read_stderr = async {
let mut buf = Vec::new();
let _ = stderr_handle.read_to_end(&mut buf).await;
buf
};
match tokio::time::timeout(timeout_dur, async {
let (stdout_bytes, stderr_bytes) = tokio::join!(read_stdout, read_stderr);
let status = child.wait().await?;
Ok::<_, anyhow::Error>((stdout_bytes, stderr_bytes, status))
})
.await
{
Ok(Ok((stdout_bytes, stderr_bytes, status))) => {
let stdout = String::from_utf8_lossy(&stdout_bytes).to_string();
let stderr = String::from_utf8_lossy(&stderr_bytes).to_string();
let exit_code = status.code().unwrap_or(-1);
Ok(ExecResult {
stdout,
stderr,
exit_code,
timed_out: false,
})
}
Ok(Err(e)) => Err(e),
Err(_elapsed) => {
let _ = child.kill().await;
Ok(ExecResult {
stdout: String::new(),
stderr: String::new(),
exit_code: -1,
timed_out: true,
})
}
}
}
}
#[allow(dead_code)] pub struct SboxSession {
pub session_name: String,
pub project_dir: PathBuf,
pub sbox_path: String,
pub is_initialized: bool,
}
#[allow(dead_code)] impl SboxSession {
pub fn new(session_name: String, project_dir: PathBuf, sbox_path: String) -> Self {
SboxSession {
session_name,
project_dir,
sbox_path,
is_initialized: false,
}
}
pub fn init(&mut self) -> Result<()> {
let status = std::process::Command::new(&self.sbox_path)
.args(["create", &self.session_name])
.status()
.map_err(|e| anyhow::anyhow!("failed to run sbox create: {}", e))?;
if !status.success() {
return Err(anyhow::anyhow!(
"sbox create failed with exit code: {:?}",
status.code()
));
}
let project_str = self.project_dir.to_string_lossy().to_string();
let status = std::process::Command::new(&self.sbox_path)
.args(["mount", &self.session_name, &project_str, "/project"])
.status()
.map_err(|e| anyhow::anyhow!("failed to run sbox mount: {}", e))?;
if !status.success() {
return Err(anyhow::anyhow!(
"sbox mount failed with exit code: {:?}",
status.code()
));
}
self.is_initialized = true;
Ok(())
}
pub async fn exec(&self, command: &str, timeout_secs: u64) -> Result<ExecResult> {
let mut child = tokio::process::Command::new(&self.sbox_path)
.args(["exec", &self.session_name, "--", "sh", "-c", command])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut stdout_handle = child.stdout.take().expect("stdout piped");
let mut stderr_handle = child.stderr.take().expect("stderr piped");
let timeout_dur = Duration::from_secs(timeout_secs);
let read_stdout = async {
let mut buf = Vec::new();
let _ = stdout_handle.read_to_end(&mut buf).await;
buf
};
let read_stderr = async {
let mut buf = Vec::new();
let _ = stderr_handle.read_to_end(&mut buf).await;
buf
};
match tokio::time::timeout(timeout_dur, async {
let (stdout_bytes, stderr_bytes) = tokio::join!(read_stdout, read_stderr);
let status = child.wait().await?;
Ok::<_, anyhow::Error>((stdout_bytes, stderr_bytes, status))
})
.await
{
Ok(Ok((stdout_bytes, stderr_bytes, status))) => {
let stdout = String::from_utf8_lossy(&stdout_bytes).to_string();
let stderr = String::from_utf8_lossy(&stderr_bytes).to_string();
let exit_code = status.code().unwrap_or(-1);
Ok(ExecResult {
stdout,
stderr,
exit_code,
timed_out: false,
})
}
Ok(Err(e)) => Err(e),
Err(_elapsed) => {
let _ = child.kill().await;
Ok(ExecResult {
stdout: String::new(),
stderr: String::new(),
exit_code: -1,
timed_out: true,
})
}
}
}
pub fn destroy(&mut self) -> Result<()> {
let status = std::process::Command::new(&self.sbox_path)
.args(["destroy", &self.session_name])
.status();
match status {
Ok(s) if s.success() => {}
Ok(s) => {
tracing::warn!(
"sbox destroy returned non-zero exit code {:?} for session {}",
s.code(),
self.session_name
);
}
Err(e) => {
tracing::warn!(
"sbox destroy failed for session {}: {}",
self.session_name,
e
);
}
}
self.is_initialized = false;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_nosandbox_exec_simple() {
let sandbox = NoSandbox::new(std::path::PathBuf::from("/tmp"));
let result = sandbox.exec("echo hello", 10).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("hello"));
assert!(!result.timed_out);
}
#[tokio::test]
async fn test_nosandbox_exec_exit_code() {
let sandbox = NoSandbox::new(std::path::PathBuf::from("/tmp"));
let result = sandbox.exec("exit 42", 10).await.unwrap();
assert_eq!(result.exit_code, 42);
assert!(!result.timed_out);
}
#[tokio::test]
async fn test_nosandbox_timeout() {
let sandbox = NoSandbox::new(std::path::PathBuf::from("/tmp"));
let result = sandbox.exec("sleep 10", 1).await.unwrap();
assert!(result.timed_out);
assert_eq!(result.exit_code, -1);
}
}