Skip to main content

soul_core/vexec/
native.rs

1//! Native OS command executor using `tokio::process`.
2
3use std::future::Future;
4use std::pin::Pin;
5
6use crate::error::{SoulError, SoulResult};
7
8use super::{ExecOutput, VirtualExecutor};
9
10/// Executes commands as real OS subprocesses via `sh -c`.
11pub 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}