soul_core/vexec/
native.rs1use std::future::Future;
4use std::pin::Pin;
5
6use crate::error::{SoulError, SoulResult};
7
8use super::{ExecOutput, VirtualExecutor};
9
10pub struct NativeExecutor;
12
13impl NativeExecutor {
14 pub fn new() -> Self {
15 Self
16 }
17}
18
19impl Default for NativeExecutor {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl VirtualExecutor for NativeExecutor {
26 fn exec_shell<'a>(
27 &'a self,
28 command: &'a str,
29 timeout_secs: u64,
30 cwd: Option<&'a str>,
31 ) -> Pin<Box<dyn Future<Output = SoulResult<ExecOutput>> + Send + 'a>> {
32 Box::pin(async move {
33 let mut cmd = tokio::process::Command::new("sh");
34 cmd.arg("-c").arg(command);
35
36 if let Some(dir) = cwd {
37 cmd.current_dir(dir);
38 }
39
40 let output =
41 tokio::time::timeout(std::time::Duration::from_secs(timeout_secs), cmd.output())
42 .await
43 .map_err(|_| SoulError::ToolExecution {
44 tool_name: "virtual_executor".into(),
45 message: format!("Command timed out after {timeout_secs}s"),
46 })?
47 .map_err(|e| SoulError::ToolExecution {
48 tool_name: "virtual_executor".into(),
49 message: format!("Failed to execute: {e}"),
50 })?;
51
52 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
53 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
54
55 #[cfg(unix)]
56 let exit_code = {
57 use std::os::unix::process::ExitStatusExt;
58 output
59 .status
60 .code()
61 .unwrap_or_else(|| output.status.signal().map(|s| 128 + s).unwrap_or(1))
62 };
63 #[cfg(not(unix))]
64 let exit_code = output.status.code().unwrap_or(1);
65
66 Ok(ExecOutput {
67 stdout,
68 stderr,
69 exit_code,
70 })
71 })
72 }
73}