use smallvec::SmallVec;
use std::borrow::Cow;
use std::future::Future;
use std::io::Write;
use std::path::PathBuf;
use std::pin::Pin;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct ExecutionResult {
pub stdout: String,
pub stderr: String,
pub success: bool,
pub exit_code: Option<i32>,
pub duration: Duration,
}
impl ExecutionResult {
pub fn output(&self) -> &str {
if !self.stdout.is_empty() {
&self.stdout
} else {
&self.stderr
}
}
pub fn combined(&self) -> Cow<'_, str> {
if self.stderr.is_empty() {
Cow::Borrowed(&self.stdout)
} else if self.stdout.is_empty() {
Cow::Borrowed(&self.stderr)
} else {
Cow::Owned(format!("{}\n{}", self.stdout, self.stderr))
}
}
pub fn error(&self) -> Option<&str> {
if self.success {
None
} else if !self.stderr.is_empty() {
Some(&self.stderr)
} else {
Some("Execution failed with no error output")
}
}
}
pub trait CodeExecutor: Send + Sync {
type ExecuteFut<'a>: Future<Output = ExecutionResult> + Send + 'a
where
Self: 'a;
fn language(&self) -> &str;
fn extension(&self) -> &str;
fn execute<'a>(&'a self, code: &'a str) -> Self::ExecuteFut<'a>;
}
pub trait DynCodeExecutor: Send + Sync {
fn language(&self) -> &str;
fn extension(&self) -> &str;
fn execute_dyn<'a>(
&'a self,
code: &'a str,
) -> Pin<Box<dyn Future<Output = ExecutionResult> + Send + 'a>>;
}
impl<T: CodeExecutor> DynCodeExecutor for T {
fn language(&self) -> &str {
CodeExecutor::language(self)
}
fn extension(&self) -> &str {
CodeExecutor::extension(self)
}
fn execute_dyn<'a>(
&'a self,
code: &'a str,
) -> Pin<Box<dyn Future<Output = ExecutionResult> + Send + 'a>> {
Box::pin(CodeExecutor::execute(self, code))
}
}
impl DynCodeExecutor for Box<dyn DynCodeExecutor> {
fn language(&self) -> &str {
(**self).language()
}
fn extension(&self) -> &str {
(**self).extension()
}
fn execute_dyn<'a>(
&'a self,
code: &'a str,
) -> Pin<Box<dyn Future<Output = ExecutionResult> + Send + 'a>> {
(**self).execute_dyn(code)
}
}
pub struct ProcessExecutor {
command: &'static str,
args: SmallVec<[&'static str; 4]>,
extension: &'static str,
language: &'static str,
timeout: Duration,
use_stdin: bool,
working_dir: Option<PathBuf>,
env_vars: SmallVec<[(String, String); 4]>,
}
impl ProcessExecutor {
pub fn new(command: &'static str, extension: &'static str, language: &'static str) -> Self {
Self {
command,
args: SmallVec::new(),
extension,
language,
timeout: Duration::from_secs(30),
use_stdin: false,
working_dir: None,
env_vars: SmallVec::new(),
}
}
pub fn args(mut self, args: &[&'static str]) -> Self {
for arg in args {
self.args.push(*arg);
}
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn stdin(mut self) -> Self {
self.use_stdin = true;
self
}
pub fn working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.working_dir = Some(dir.into());
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars.push((key.into(), value.into()));
self
}
fn execute_sync(&self, code: &str) -> ExecutionResult {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let start = Instant::now();
let temp_file = if !self.use_stdin {
let temp_dir = std::env::temp_dir();
let unique_id = COUNTER.fetch_add(1, Ordering::SeqCst);
let file_path = temp_dir.join(format!(
"kkachi_exec_{}_{}.{}",
std::process::id(),
unique_id,
self.extension
));
if let Err(e) = std::fs::write(&file_path, code) {
return ExecutionResult {
stdout: String::new(),
stderr: format!("Failed to write temp file: {}", e),
success: false,
exit_code: None,
duration: start.elapsed(),
};
}
Some(file_path)
} else {
None
};
let mut cmd = Command::new(self.command);
for arg in &self.args {
cmd.arg(arg);
}
if let Some(ref file) = temp_file {
cmd.arg(file);
} else {
cmd.stdin(Stdio::piped());
}
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
if let Some(ref dir) = self.working_dir {
cmd.current_dir(dir);
}
for (key, value) in &self.env_vars {
cmd.env(key, value);
}
let result = if self.use_stdin {
match cmd.spawn() {
Ok(mut child) => {
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(code.as_bytes());
}
match child.wait_with_output() {
Ok(output) => {
let duration = start.elapsed();
ExecutionResult {
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
success: output.status.success(),
exit_code: output.status.code(),
duration,
}
}
Err(e) => ExecutionResult {
stdout: String::new(),
stderr: format!("Failed to wait for process: {}", e),
success: false,
exit_code: None,
duration: start.elapsed(),
},
}
}
Err(e) => ExecutionResult {
stdout: String::new(),
stderr: format!("Failed to spawn '{}': {}", self.command, e),
success: false,
exit_code: None,
duration: start.elapsed(),
},
}
} else {
match cmd.output() {
Ok(output) => {
let duration = start.elapsed();
ExecutionResult {
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
success: output.status.success(),
exit_code: output.status.code(),
duration,
}
}
Err(e) => ExecutionResult {
stdout: String::new(),
stderr: format!("Failed to execute '{}': {}", self.command, e),
success: false,
exit_code: None,
duration: start.elapsed(),
},
}
};
if let Some(file) = temp_file {
let _ = std::fs::remove_file(file);
}
result
}
}
impl CodeExecutor for ProcessExecutor {
type ExecuteFut<'a>
= std::future::Ready<ExecutionResult>
where
Self: 'a;
fn language(&self) -> &str {
self.language
}
fn extension(&self) -> &str {
self.extension
}
fn execute<'a>(&'a self, code: &'a str) -> Self::ExecuteFut<'a> {
std::future::ready(self.execute_sync(code))
}
}
pub fn python_executor() -> ProcessExecutor {
ProcessExecutor::new("python3", "py", "python")
}
pub fn node_executor() -> ProcessExecutor {
ProcessExecutor::new("node", "js", "javascript")
}
pub fn ruby_executor() -> ProcessExecutor {
ProcessExecutor::new("ruby", "rb", "ruby")
}
pub fn bash_executor() -> ProcessExecutor {
ProcessExecutor::new("bash", "sh", "bash")
}
pub fn rust_executor() -> ProcessExecutor {
ProcessExecutor::new("rustc", "rs", "rust").args(&[
"-o",
"/tmp/kkachi_rust_exec",
"--edition=2021",
])
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_python_executor() {
let exec = python_executor();
let result = exec.execute("print(2 + 2)").await;
if result.success {
assert!(result.stdout.trim() == "4" || result.output().contains("4"));
}
assert!(result.duration.as_nanos() > 0);
}
#[tokio::test]
async fn test_node_executor() {
let exec = node_executor();
let result = exec.execute("console.log(2 + 2)").await;
if result.success {
assert!(result.stdout.trim() == "4");
}
assert!(result.duration.as_nanos() > 0);
}
#[tokio::test]
#[cfg(unix)]
async fn test_bash_executor() {
let exec = bash_executor();
let result = exec.execute("echo hello").await;
assert!(result.success);
assert!(result.stdout.contains("hello"));
}
#[tokio::test]
async fn test_execution_result_methods() {
let result = ExecutionResult {
stdout: "output".to_string(),
stderr: "error".to_string(),
success: false,
exit_code: Some(1),
duration: Duration::from_millis(100),
};
assert_eq!(result.output(), "output");
assert!(result.combined().contains("output"));
assert!(result.combined().contains("error"));
assert_eq!(result.error(), Some("error"));
}
#[tokio::test]
async fn test_executor_timeout() {
let exec = python_executor().timeout(Duration::from_secs(1));
assert_eq!(exec.timeout, Duration::from_secs(1));
}
#[tokio::test]
async fn test_executor_env() {
let exec = python_executor().env("TEST_VAR", "test_value");
assert_eq!(exec.env_vars.len(), 1);
}
#[test]
fn test_process_executor_creation() {
let exec = ProcessExecutor::new("python3", "py", "python")
.args(&["-u"])
.timeout(Duration::from_secs(5))
.stdin();
assert_eq!(exec.command, "python3");
assert_eq!(CodeExecutor::language(&exec), "python");
assert_eq!(CodeExecutor::extension(&exec), "py");
assert!(exec.use_stdin);
}
}