use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::io::Write; use std::time::Duration; use tokio::io::AsyncReadExt; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ExecutionResult { pub command: String, pub exit_code: Option, pub stdout: String, pub stderr: String, pub duration_ms: u64, pub success: bool, } #[allow(dead_code)] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ScriptExecution { pub language: String, pub code: String, pub result: ExecutionResult, } pub async fn execute_command( cmd: &str, args: &[&str], timeout_secs: u64, ) -> Result { let start = std::time::Instant::now(); let mut child = tokio::process::Command::new(cmd) .args(args) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .with_context(|| format!("Failed to spawn: {} {}", cmd, args.join(" ")))?; let timeout = Duration::from_secs(timeout_secs); match tokio::time::timeout(timeout, child.wait()).await { Ok(Ok(status)) => { let elapsed = start.elapsed(); let mut stdout_buf = String::new(); let mut stderr_buf = String::new(); if let Some(mut out) = child.stdout.take() { let _ = out.read_to_string(&mut stdout_buf).await; } if let Some(mut err) = child.stderr.take() { let _ = err.read_to_string(&mut stderr_buf).await; } Ok(ExecutionResult { command: format!("{} {}", cmd, args.join(" ")), exit_code: status.code(), stdout: stdout_buf.trim().to_string(), stderr: stderr_buf.trim().to_string(), duration_ms: elapsed.as_millis() as u64, success: status.success(), }) } Ok(Err(e)) => Err(anyhow::anyhow!("Process failed: {}", e)), Err(_) => { let _ = child.kill().await; let elapsed = start.elapsed(); Ok(ExecutionResult { command: format!("{} {}", cmd, args.join(" ")), exit_code: None, stdout: String::new(), stderr: format!("Killed after {}s timeout", timeout_secs), duration_ms: elapsed.as_millis() as u64, success: false, }) } } } pub fn detect_language(code: &str) -> &'static str { let trimmed = code.trim(); let first_line = trimmed.lines().next().unwrap_or("").trim(); if first_line.starts_with("#!/") { if first_line.contains("python") || first_line.contains("python3") { return "python"; } if first_line.contains("bash") { return "bash"; } if first_line.contains("sh") && !first_line.contains("bash") { return "bash"; } if first_line.contains("ruby") { return "ruby"; } if first_line.contains("perl") { return "perl"; } if first_line.contains("node") { return "node"; } } if trimmed.contains("fn main") || (trimmed.contains("println!") && trimmed.contains("fn ")) || trimmed.contains("use std::") { return "rust"; } if (trimmed.contains("package main") && trimmed.contains("func main")) || trimmed.contains("import (") || trimmed.starts_with("package ") { return "go"; } if trimmed.contains("#include <") { if trimmed.contains("iostream") || trimmed.contains("std::") || trimmed.contains("using namespace") { return "cpp"; } return "c"; } let ps_score = [ "Write-Host", "Get-Process", "Get-Service", "Get-ChildItem", "Param(", "Write-Output", "Write-Error", "Get-Item", "Set-Item", "New-Item", "Remove-Item", "ForEach-Object", "Where-Object", "Select-Object", "| ForEach-Object", "foreach ($", "function ", "Export-Csv", ] .iter() .filter(|&&kw| trimmed.contains(kw)) .count(); if ps_score >= 2 || (trimmed.starts_with("#requires") || trimmed.starts_with("<#")) { return "powershell"; } if trimmed.contains("$") && (trimmed.contains("Write-Host") || trimmed.contains("Write-Output")) { return "powershell"; } let py_score = [ "import ", "from ", "def ", "class ", "print(", "if __name__", "elif ", "except ", "with ", "as ", "lambda ", "yield ", "async def", "await ", ] .iter() .filter(|&&kw| trimmed.contains(kw)) .count(); if py_score >= 3 { return "python"; } let js_score = [ "require(", "console.log", "module.exports", "process.env", "=>", "const ", "let ", "var ", "function(", "async (", "document.", "window.", "export default", "import ", ] .iter() .filter(|&&kw| trimmed.contains(kw)) .count(); if js_score >= 2 { return "node"; } let rb_score = [ "def ", "\nend", "puts ", "require '", "attr_accessor", "attr_reader", "attr_writer", "do |", "=>", "::", ] .iter() .filter(|&&kw| trimmed.contains(kw)) .count(); if rb_score >= 2 { return "ruby"; } let pl_score = [ "use strict", "use warnings", "my $", "sub ", "shift", "print \"", "print '", "chomp", "foreach my", "while (<", ] .iter() .filter(|&&kw| trimmed.contains(kw)) .count(); if pl_score >= 2 { return "perl"; } let sh_score = [ "if [", "if [[", "$#", "\nfi", "\ndone", "\nthen", "case ", "esac", ">&2", "2>&1", "#!/", "echo \"", "while read", "for i in", "do\n", "$((", "${", ] .iter() .filter(|&&kw| trimmed.contains(kw)) .count(); if sh_score >= 2 { return "bash"; } let cmd_score = [ "@echo", "echo off", "setlocal", "endlocal", "%", "REM ", "rem ", "color ", "title ", "exit /b", "if not", "if defined", "set \"", ">nul", ] .iter() .filter(|&&kw| trimmed.contains(kw)) .count(); if cmd_score >= 2 { return "cmd"; } "python" } pub async fn execute_script( language: &str, code: &str, timeout_secs: u64, ) -> Result { let (interpreter, ext) = match language.to_lowercase().as_str() { "python" | "py" => ("python", ".py"), "bash" | "sh" => ("bash", ".sh"), "powershell" | "ps1" => ("powershell", ".ps1"), "cmd" | "bat" => ("cmd", ".bat"), "ruby" => ("ruby", ".rb"), "perl" => ("perl", ".pl"), "node" | "javascript" | "js" => ("node", ".js"), "rust" => return compile_and_run_rust(code, timeout_secs).await, "go" | "golang" => return compile_and_run_go(code, timeout_secs).await, "c" => return compile_and_run_c_cpp(code, timeout_secs, "c").await, "cpp" | "c++" | "cxx" => return compile_and_run_c_cpp(code, timeout_secs, "cpp").await, _ => return Err(anyhow::anyhow!("Unsupported language: {}. Supported: python, bash, powershell, cmd, ruby, perl, node, rust, go, c, cpp", language)), }; let temp_dir = std::env::temp_dir().join("airust_scripts"); std::fs::create_dir_all(&temp_dir)?; let script_path = temp_dir.join(format!("script_{}_{}", std::process::id(), ext)); let mut file = std::fs::File::create(&script_path)?; file.write_all(code.as_bytes())?; drop(file); let result = if interpreter == "cmd" { execute_command("cmd", &["/C", script_path.to_str().unwrap()], timeout_secs).await } else { execute_command(interpreter, &[script_path.to_str().unwrap()], timeout_secs).await }; let _ = std::fs::remove_file(&script_path); result } async fn compile_and_run_rust(code: &str, timeout_secs: u64) -> Result { let temp_dir = std::env::temp_dir().join("airust_scripts"); std::fs::create_dir_all(&temp_dir)?; let source_path = temp_dir.join(format!("script_{}.rs", std::process::id())); let bin_path = temp_dir.join(format!("script_{}", std::process::id())); let mut file = std::fs::File::create(&source_path)?; file.write_all(code.as_bytes())?; drop(file); let compile = execute_command( "rustc", &[ source_path.to_str().unwrap(), "-o", bin_path.to_str().unwrap(), "--quiet", ], timeout_secs, ) .await?; if !compile.success { return Ok(ExecutionResult { command: "rustc (compile)".to_string(), exit_code: compile.exit_code, stdout: String::new(), stderr: compile.stderr, duration_ms: compile.duration_ms, success: false, }); } let run_result = execute_command(bin_path.to_str().unwrap(), &[], timeout_secs).await; let _ = std::fs::remove_file(&source_path); let _ = std::fs::remove_file(&bin_path); run_result } async fn compile_and_run_go(code: &str, timeout_secs: u64) -> Result { let temp_dir = std::env::temp_dir().join("airust_scripts"); std::fs::create_dir_all(&temp_dir)?; let source_path = temp_dir.join(format!("script_{}.go", std::process::id())); let bin_path = temp_dir.join(format!("script_{}.exe", std::process::id())); let mut file = std::fs::File::create(&source_path)?; file.write_all(code.as_bytes())?; drop(file); let compile = execute_command( "go", &[ "build", "-o", bin_path.to_str().unwrap(), source_path.to_str().unwrap(), ], timeout_secs, ) .await?; if !compile.success { return Ok(ExecutionResult { command: "go build".to_string(), exit_code: compile.exit_code, stdout: String::new(), stderr: compile.stderr, duration_ms: compile.duration_ms, success: false, }); } let run_result = execute_command(bin_path.to_str().unwrap(), &[], timeout_secs).await; let _ = std::fs::remove_file(&source_path); let _ = std::fs::remove_file(&bin_path); run_result } async fn compile_and_run_c_cpp( code: &str, timeout_secs: u64, lang: &str, ) -> Result { let temp_dir = std::env::temp_dir().join("airust_scripts"); std::fs::create_dir_all(&temp_dir)?; let ext = if lang == "cpp" { ".cpp" } else { ".c" }; let source_path = temp_dir.join(format!("script_{}{}", std::process::id(), ext)); let bin_path = temp_dir.join(format!("script_{}.exe", std::process::id())); let mut file = std::fs::File::create(&source_path)?; file.write_all(code.as_bytes())?; drop(file); let compiler = if lang == "cpp" { "g++" } else { "gcc" }; let compile = execute_command( compiler, &[ source_path.to_str().unwrap(), "-o", bin_path.to_str().unwrap(), ], timeout_secs, ) .await?; if !compile.success { return Ok(ExecutionResult { command: format!("{} (compile)", compiler), exit_code: compile.exit_code, stdout: String::new(), stderr: compile.stderr, duration_ms: compile.duration_ms, success: false, }); } let run_result = execute_command(bin_path.to_str().unwrap(), &[], timeout_secs).await; let _ = std::fs::remove_file(&source_path); let _ = std::fs::remove_file(&bin_path); run_result } #[allow(dead_code)] pub fn list_supported_languages() -> Vec { vec![ "python".to_string(), "bash".to_string(), "powershell".to_string(), "cmd".to_string(), "node".to_string(), "ruby".to_string(), "perl".to_string(), "rust".to_string(), "go".to_string(), "c".to_string(), "cpp".to_string(), ] }