1use async_trait::async_trait;
4use futures::Stream;
5use std::pin::Pin;
6use std::sync::Arc;
7use tracing::{debug, info};
8use uuid::Uuid;
9
10use crate::command::{CommandEvent, CommandExecutor, CommandRequest, CommandResult};
11use crate::error::UbiquityError;
12
13#[cfg(not(target_arch = "wasm32"))]
14use crate::command_local::LocalCommandExecutor;
15
16#[cfg(target_arch = "wasm32")]
17use crate::command_wasm::WasmCommandExecutor;
18
19use crate::command_cloud::CloudCommandExecutor;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ExecutionMode {
24 Local,
26 Wasm,
28 Cloud,
30 Auto,
32}
33
34#[derive(Debug, Clone)]
36pub struct UnifiedExecutorConfig {
37 pub mode: ExecutionMode,
38 pub cloud_worker_url: Option<String>,
39 pub cloud_api_token: Option<String>,
40 pub cloud_namespace_id: Option<String>,
41 pub event_buffer_size: usize,
42 pub max_wasm_workers: usize,
43}
44
45impl Default for UnifiedExecutorConfig {
46 fn default() -> Self {
47 Self {
48 mode: ExecutionMode::Auto,
49 cloud_worker_url: None,
50 cloud_api_token: None,
51 cloud_namespace_id: None,
52 event_buffer_size: 1024,
53 max_wasm_workers: 4,
54 }
55 }
56}
57
58pub struct UnifiedCommandExecutor {
60 config: UnifiedExecutorConfig,
61 executor: Arc<dyn CommandExecutor>,
62}
63
64impl UnifiedCommandExecutor {
65 pub fn new(config: UnifiedExecutorConfig) -> Result<Self, UbiquityError> {
67 let executor = Self::create_executor(&config)?;
68
69 Ok(Self {
70 config,
71 executor: Arc::from(executor),
72 })
73 }
74
75 pub fn auto() -> Result<Self, UbiquityError> {
77 Self::new(UnifiedExecutorConfig::default())
78 }
79
80 pub fn mode(&self) -> ExecutionMode {
82 if self.config.mode == ExecutionMode::Auto {
83 Self::detect_mode()
84 } else {
85 self.config.mode
86 }
87 }
88
89 fn detect_mode() -> ExecutionMode {
91 #[cfg(target_arch = "wasm32")]
92 {
93 ExecutionMode::Wasm
94 }
95
96 #[cfg(not(target_arch = "wasm32"))]
97 {
98 ExecutionMode::Local
99 }
100 }
101
102 fn create_executor(config: &UnifiedExecutorConfig) -> Result<Box<dyn CommandExecutor>, UbiquityError> {
104 let mode = if config.mode == ExecutionMode::Auto {
105 Self::detect_mode()
106 } else {
107 config.mode
108 };
109
110 info!("Creating command executor in {:?} mode", mode);
111
112 match mode {
113 #[cfg(not(target_arch = "wasm32"))]
114 ExecutionMode::Local => {
115 let executor = LocalCommandExecutor::new()
116 .with_event_buffer_size(config.event_buffer_size);
117 Ok(Box::new(executor))
118 }
119
120 #[cfg(target_arch = "wasm32")]
121 ExecutionMode::Wasm => {
122 let executor = WasmCommandExecutor::new()?
123 .with_max_workers(config.max_wasm_workers)?;
124 Ok(Box::new(executor))
125 }
126
127 ExecutionMode::Cloud => {
128 let worker_url = config.cloud_worker_url.as_ref()
129 .ok_or_else(|| UbiquityError::Configuration(
130 "Cloud worker URL not configured".to_string()
131 ))?;
132
133 let api_token = config.cloud_api_token.as_ref()
134 .ok_or_else(|| UbiquityError::Configuration(
135 "Cloud API token not configured".to_string()
136 ))?;
137
138 let namespace_id = config.cloud_namespace_id.as_ref()
139 .ok_or_else(|| UbiquityError::Configuration(
140 "Cloud namespace ID not configured".to_string()
141 ))?;
142
143 let executor = CloudCommandExecutor::new(
144 worker_url.clone(),
145 api_token.clone(),
146 namespace_id.clone(),
147 );
148
149 Ok(Box::new(executor))
150 }
151
152 _ => Err(UbiquityError::Configuration(format!(
153 "Execution mode {:?} not available on this platform",
154 mode
155 ))),
156 }
157 }
158
159 pub fn switch_mode(&mut self, mode: ExecutionMode) -> Result<(), UbiquityError> {
161 self.config.mode = mode;
162 self.executor = Arc::from(Self::create_executor(&self.config)?);
163 Ok(())
164 }
165
166 pub fn command(&self, command: impl Into<String>) -> CommandBuilder {
168 CommandBuilder::new(self, command)
169 }
170}
171
172#[async_trait]
173impl CommandExecutor for UnifiedCommandExecutor {
174 async fn execute(
175 &self,
176 request: CommandRequest,
177 ) -> Result<Pin<Box<dyn Stream<Item = CommandEvent> + Send>>, UbiquityError> {
178 debug!("Executing command {} in {:?} mode", request.command, self.mode());
179 self.executor.execute(request).await
180 }
181
182 async fn cancel(&self, command_id: Uuid) -> Result<(), UbiquityError> {
183 debug!("Cancelling command {} in {:?} mode", command_id, self.mode());
184 self.executor.cancel(command_id).await
185 }
186
187 async fn status(&self, command_id: Uuid) -> Result<Option<CommandResult>, UbiquityError> {
188 self.executor.status(command_id).await
189 }
190}
191
192pub struct CommandBuilder<'a> {
194 executor: &'a UnifiedCommandExecutor,
195 request: CommandRequest,
196}
197
198impl<'a> CommandBuilder<'a> {
199 fn new(executor: &'a UnifiedCommandExecutor, command: impl Into<String>) -> Self {
200 Self {
201 executor,
202 request: CommandRequest::new(command),
203 }
204 }
205
206 pub fn arg(mut self, arg: impl Into<String>) -> Self {
207 self.request.args.push(arg.into());
208 self
209 }
210
211 pub fn args<I, S>(mut self, args: I) -> Self
212 where
213 I: IntoIterator<Item = S>,
214 S: Into<String>,
215 {
216 self.request.args.extend(args.into_iter().map(Into::into));
217 self
218 }
219
220 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
221 self.request.env.insert(key.into(), value.into());
222 self
223 }
224
225 pub fn envs<I, K, V>(mut self, vars: I) -> Self
226 where
227 I: IntoIterator<Item = (K, V)>,
228 K: Into<String>,
229 V: Into<String>,
230 {
231 for (k, v) in vars {
232 self.request.env.insert(k.into(), v.into());
233 }
234 self
235 }
236
237 pub fn current_dir(mut self, dir: impl Into<String>) -> Self {
238 self.request.working_dir = Some(dir.into());
239 self
240 }
241
242 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
243 self.request.timeout = Some(timeout);
244 self
245 }
246
247 pub fn stdin(mut self, stdin: impl Into<String>) -> Self {
248 self.request.stdin = Some(stdin.into());
249 self
250 }
251
252 pub async fn execute(self) -> Result<Pin<Box<dyn Stream<Item = CommandEvent> + Send>>, UbiquityError> {
253 self.executor.execute(self.request).await
254 }
255
256 pub async fn output(self) -> Result<CommandOutput, UbiquityError> {
257 use futures::StreamExt;
258
259 let request_id = self.request.id;
260 let mut stream = self.execute().await?;
261 let mut output = CommandOutput::new(request_id);
262
263 while let Some(event) = stream.next().await {
264 output.process_event(event);
265 }
266
267 Ok(output)
268 }
269}
270
271#[derive(Debug, Clone)]
273pub struct CommandOutput {
274 pub id: Uuid,
275 pub stdout: String,
276 pub stderr: String,
277 pub exit_code: Option<i32>,
278 pub success: bool,
279 pub duration_ms: Option<u64>,
280 pub cancelled: bool,
281}
282
283impl CommandOutput {
284 fn new(id: Uuid) -> Self {
285 Self {
286 id,
287 stdout: String::new(),
288 stderr: String::new(),
289 exit_code: None,
290 success: false,
291 duration_ms: None,
292 cancelled: false,
293 }
294 }
295
296 fn process_event(&mut self, event: CommandEvent) {
297 match event {
298 CommandEvent::Stdout { data, .. } => {
299 if !self.stdout.is_empty() {
300 self.stdout.push('\n');
301 }
302 self.stdout.push_str(&data);
303 }
304 CommandEvent::Stderr { data, .. } => {
305 if !self.stderr.is_empty() {
306 self.stderr.push('\n');
307 }
308 self.stderr.push_str(&data);
309 }
310 CommandEvent::Completed { exit_code, duration_ms, .. } => {
311 self.exit_code = Some(exit_code);
312 self.success = exit_code == 0;
313 self.duration_ms = Some(duration_ms);
314 }
315 CommandEvent::Failed { duration_ms, .. } => {
316 self.success = false;
317 self.duration_ms = Some(duration_ms);
318 }
319 CommandEvent::Cancelled { duration_ms, .. } => {
320 self.cancelled = true;
321 self.duration_ms = Some(duration_ms);
322 }
323 _ => {}
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use futures::StreamExt;
332
333 #[tokio::test]
334 async fn test_unified_executor_auto_mode() {
335 let executor = UnifiedCommandExecutor::auto().unwrap();
336
337 #[cfg(not(target_arch = "wasm32"))]
338 assert_eq!(executor.mode(), ExecutionMode::Local);
339
340 #[cfg(target_arch = "wasm32")]
341 assert_eq!(executor.mode(), ExecutionMode::Wasm);
342 }
343
344 #[tokio::test]
345 async fn test_command_builder() {
346 let executor = UnifiedCommandExecutor::auto().unwrap();
347
348 let output = executor
349 .command("echo")
350 .arg("hello")
351 .arg("world")
352 .env("TEST", "value")
353 .timeout(std::time::Duration::from_secs(5))
354 .output()
355 .await
356 .unwrap();
357
358 assert!(output.success);
359 assert_eq!(output.stdout.trim(), "hello world");
360 }
361
362 #[tokio::test]
363 async fn test_command_streaming() {
364 let executor = UnifiedCommandExecutor::auto().unwrap();
365
366 let mut stream = executor
367 .command("echo")
368 .args(["line1", "line2"])
369 .execute()
370 .await
371 .unwrap();
372
373 let mut events = Vec::new();
374 while let Some(event) = stream.next().await {
375 events.push(event);
376 }
377
378 assert!(!events.is_empty());
379 assert!(events.iter().any(|e| matches!(e, CommandEvent::Started { .. })));
380 }
381}