Skip to main content

a3s_box_sdk/sandbox/
commands.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use a3s_box_core::ExecRequest;
6
7use super::SandboxInner;
8use crate::{ClientError, Result};
9
10/// A shell command or an explicit argument vector.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum SandboxCommand {
13    Shell(String),
14    Argv(Vec<String>),
15}
16
17impl SandboxCommand {
18    pub fn shell(command: impl Into<String>) -> Self {
19        Self::Shell(command.into())
20    }
21
22    pub fn argv(command: impl IntoIterator<Item = impl Into<String>>) -> Self {
23        Self::Argv(command.into_iter().map(Into::into).collect())
24    }
25
26    fn into_argv(self) -> Result<Vec<String>> {
27        let argv = match self {
28            Self::Shell(command) => {
29                if command.trim().is_empty() {
30                    return Err(ClientError::Validation(
31                        "sandbox command cannot be empty".to_string(),
32                    ));
33                }
34                vec!["/bin/sh".to_string(), "-lc".to_string(), command]
35            }
36            Self::Argv(argv) => argv,
37        };
38        if argv.is_empty() {
39            return Err(ClientError::Validation(
40                "sandbox command cannot be empty".to_string(),
41            ));
42        }
43        Ok(argv)
44    }
45}
46
47impl From<String> for SandboxCommand {
48    fn from(command: String) -> Self {
49        Self::Shell(command)
50    }
51}
52
53impl From<&str> for SandboxCommand {
54    fn from(command: &str) -> Self {
55        Self::Shell(command.to_string())
56    }
57}
58
59impl From<Vec<String>> for SandboxCommand {
60    fn from(command: Vec<String>) -> Self {
61        Self::Argv(command)
62    }
63}
64
65/// Optional controls for one Sandbox command.
66#[derive(Debug, Clone, Default, PartialEq, Eq)]
67pub struct CommandRunOptions {
68    pub timeout: Option<Duration>,
69    pub envs: BTreeMap<String, String>,
70    pub cwd: Option<String>,
71    pub user: Option<String>,
72    pub stdin: Option<Vec<u8>>,
73}
74
75impl CommandRunOptions {
76    pub const fn timeout(mut self, timeout: Duration) -> Self {
77        self.timeout = Some(timeout);
78        self
79    }
80
81    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
82        self.envs.insert(key.into(), value.into());
83        self
84    }
85
86    pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
87        self.cwd = Some(cwd.into());
88        self
89    }
90
91    pub fn user(mut self, user: impl Into<String>) -> Self {
92        self.user = Some(user.into());
93        self
94    }
95
96    pub fn stdin(mut self, stdin: impl Into<Vec<u8>>) -> Self {
97        self.stdin = Some(stdin.into());
98        self
99    }
100}
101
102/// Captured result of one E2B-style command execution.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct CommandResult {
105    pub stdout: String,
106    pub stderr: String,
107    pub exit_code: i32,
108    pub truncated: bool,
109    pub(crate) stdout_bytes: Vec<u8>,
110    pub(crate) stderr_bytes: Vec<u8>,
111}
112
113/// E2B-style command namespace attached to a local [`super::Sandbox`].
114#[derive(Clone)]
115pub struct Commands {
116    pub(crate) inner: Arc<SandboxInner>,
117}
118
119impl std::fmt::Debug for Commands {
120    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        formatter
122            .debug_struct("Commands")
123            .field("sandbox_id", &self.inner.execution_id)
124            .finish()
125    }
126}
127
128impl Commands {
129    pub async fn run(&self, command: impl Into<SandboxCommand>) -> Result<CommandResult> {
130        self.run_with_options(command, CommandRunOptions::default())
131            .await
132    }
133
134    pub async fn run_with_options(
135        &self,
136        command: impl Into<SandboxCommand>,
137        options: CommandRunOptions,
138    ) -> Result<CommandResult> {
139        let timeout_ns = match options.timeout {
140            Some(timeout) if timeout.is_zero() => {
141                return Err(ClientError::Validation(
142                    "sandbox command timeout must be greater than zero".to_string(),
143                ))
144            }
145            Some(timeout) => u64::try_from(timeout.as_nanos()).unwrap_or(u64::MAX),
146            None => 0,
147        };
148        let (_, generation) = self.inner.active_execution()?;
149        let request = ExecRequest {
150            request_id: Some(format!("sdk-command-{}", uuid::Uuid::new_v4())),
151            cmd: command.into().into_argv()?,
152            timeout_ns,
153            env: options
154                .envs
155                .into_iter()
156                .map(|(key, value)| format!("{key}={value}"))
157                .collect(),
158            working_dir: options.cwd,
159            rootfs: None,
160            stdin: options.stdin,
161            stdin_streaming: false,
162            user: options.user,
163            streaming: false,
164        };
165
166        #[cfg(not(unix))]
167        {
168            let _ = (generation, request);
169            return Err(ClientError::Execution(
170                a3s_box_core::ExecutionManagerError::Unavailable(
171                    "local command sessions are not available on this host".to_string(),
172                ),
173            ));
174        }
175
176        #[cfg(unix)]
177        {
178            let output = self
179                .inner
180                .client
181                .execute_execution(&self.inner.execution_id, generation, request)
182                .await?;
183
184            Ok(CommandResult {
185                stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
186                stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
187                exit_code: output.exit_code,
188                truncated: output.truncated,
189                stdout_bytes: output.stdout,
190                stderr_bytes: output.stderr,
191            })
192        }
193    }
194}