a3s_box_sdk/sandbox/
commands.rs1use 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#[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#[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#[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#[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}