1use async_trait::async_trait;
4use futures::Stream;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::pin::Pin;
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::sync::{mpsc, oneshot, RwLock};
11use uuid::Uuid;
12
13use crate::error::UbiquityError;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(tag = "type")]
18pub enum CommandEvent {
19 Started {
21 command_id: Uuid,
22 command: String,
23 args: Vec<String>,
24 timestamp: chrono::DateTime<chrono::Utc>,
25 },
26 Stdout {
28 command_id: Uuid,
29 data: String,
30 timestamp: chrono::DateTime<chrono::Utc>,
31 },
32 Stderr {
34 command_id: Uuid,
35 data: String,
36 timestamp: chrono::DateTime<chrono::Utc>,
37 },
38 Progress {
40 command_id: Uuid,
41 percentage: f32,
42 message: String,
43 timestamp: chrono::DateTime<chrono::Utc>,
44 },
45 Completed {
47 command_id: Uuid,
48 exit_code: i32,
49 duration_ms: u64,
50 timestamp: chrono::DateTime<chrono::Utc>,
51 },
52 Failed {
54 command_id: Uuid,
55 error: String,
56 duration_ms: u64,
57 timestamp: chrono::DateTime<chrono::Utc>,
58 },
59 Cancelled {
61 command_id: Uuid,
62 duration_ms: u64,
63 timestamp: chrono::DateTime<chrono::Utc>,
64 },
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CommandRequest {
70 pub id: Uuid,
71 pub command: String,
72 pub args: Vec<String>,
73 pub env: HashMap<String, String>,
74 pub working_dir: Option<String>,
75 pub timeout: Option<Duration>,
76 pub stdin: Option<String>,
77}
78
79impl CommandRequest {
80 pub fn new(command: impl Into<String>) -> Self {
81 Self {
82 id: Uuid::new_v4(),
83 command: command.into(),
84 args: Vec::new(),
85 env: HashMap::new(),
86 working_dir: None,
87 timeout: None,
88 stdin: None,
89 }
90 }
91
92 pub fn with_args(mut self, args: Vec<String>) -> Self {
93 self.args = args;
94 self
95 }
96
97 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
98 self.env.insert(key.into(), value.into());
99 self
100 }
101
102 pub fn with_working_dir(mut self, dir: impl Into<String>) -> Self {
103 self.working_dir = Some(dir.into());
104 self
105 }
106
107 pub fn with_timeout(mut self, timeout: Duration) -> Self {
108 self.timeout = Some(timeout);
109 self
110 }
111
112 pub fn with_stdin(mut self, stdin: impl Into<String>) -> Self {
113 self.stdin = Some(stdin.into());
114 self
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct CommandResult {
121 pub id: Uuid,
122 pub exit_code: Option<i32>,
123 pub stdout: String,
124 pub stderr: String,
125 pub duration_ms: u64,
126 pub cancelled: bool,
127}
128
129#[async_trait]
131pub trait CommandExecutor: Send + Sync {
132 async fn execute(
134 &self,
135 request: CommandRequest,
136 ) -> Result<Pin<Box<dyn Stream<Item = CommandEvent> + Send>>, UbiquityError>;
137
138 async fn cancel(&self, command_id: Uuid) -> Result<(), UbiquityError>;
140
141 async fn status(&self, command_id: Uuid) -> Result<Option<CommandResult>, UbiquityError>;
143}
144
145pub struct CommandContext {
147 active_commands: Arc<RwLock<HashMap<Uuid, CommandHandle>>>,
148}
149
150impl CommandContext {
151 pub fn new() -> Self {
152 Self {
153 active_commands: Arc::new(RwLock::new(HashMap::new())),
154 }
155 }
156
157 pub async fn register(&self, id: Uuid, handle: CommandHandle) {
158 self.active_commands.write().await.insert(id, handle);
159 }
160
161 pub async fn unregister(&self, id: &Uuid) {
162 self.active_commands.write().await.remove(id);
163 }
164
165 pub async fn get(&self, id: &Uuid) -> Option<CommandHandle> {
166 self.active_commands.read().await.get(id).cloned()
167 }
168
169 pub async fn cancel(&self, id: &Uuid) -> Result<(), UbiquityError> {
170 if let Some(handle) = self.get(id).await {
171 handle.cancel().await
172 } else {
173 Err(UbiquityError::NotFound(format!("Command {} not found", id)))
174 }
175 }
176
177 pub async fn cancel_all(&self) -> Vec<(Uuid, Result<(), UbiquityError>)> {
178 let commands: Vec<(Uuid, CommandHandle)> = {
179 let guard = self.active_commands.read().await;
180 guard.iter().map(|(id, h)| (*id, h.clone())).collect()
181 };
182
183 let mut results = Vec::new();
184 for (id, handle) in commands {
185 let result = handle.cancel().await;
186 results.push((id, result));
187 }
188 results
189 }
190}
191
192#[derive(Clone)]
194pub struct CommandHandle {
195 pub id: Uuid,
196 cancel_tx: mpsc::Sender<()>,
197 status_tx: mpsc::Sender<oneshot::Sender<CommandResult>>,
198}
199
200impl CommandHandle {
201 pub fn new(
202 id: Uuid,
203 cancel_tx: mpsc::Sender<()>,
204 status_tx: mpsc::Sender<oneshot::Sender<CommandResult>>,
205 ) -> Self {
206 Self {
207 id,
208 cancel_tx,
209 status_tx,
210 }
211 }
212
213 pub async fn cancel(&self) -> Result<(), UbiquityError> {
214 self.cancel_tx
215 .send(())
216 .await
217 .map_err(|_| UbiquityError::Internal("Failed to send cancel signal".to_string()))
218 }
219
220 pub async fn status(&self) -> Result<CommandResult, UbiquityError> {
221 let (tx, rx) = oneshot::channel();
222 self.status_tx
223 .send(tx)
224 .await
225 .map_err(|_| UbiquityError::Internal("Failed to request status".to_string()))?;
226
227 rx.await
228 .map_err(|_| UbiquityError::Internal("Failed to receive status".to_string()))
229 }
230}
231
232pub struct ProgressTracker {
234 command_id: Uuid,
235 tx: mpsc::Sender<CommandEvent>,
236}
237
238impl ProgressTracker {
239 pub fn new(command_id: Uuid, tx: mpsc::Sender<CommandEvent>) -> Self {
240 Self { command_id, tx }
241 }
242
243 pub async fn update(&self, percentage: f32, message: impl Into<String>) -> Result<(), UbiquityError> {
244 self.tx
245 .send(CommandEvent::Progress {
246 command_id: self.command_id,
247 percentage: percentage.clamp(0.0, 100.0),
248 message: message.into(),
249 timestamp: chrono::Utc::now(),
250 })
251 .await
252 .map_err(|_| UbiquityError::Internal("Failed to send progress update".to_string()))
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_command_request_builder() {
262 let request = CommandRequest::new("echo")
263 .with_args(vec!["hello".to_string(), "world".to_string()])
264 .with_env("PATH", "/usr/bin")
265 .with_working_dir("/tmp")
266 .with_timeout(Duration::from_secs(30))
267 .with_stdin("test input");
268
269 assert_eq!(request.command, "echo");
270 assert_eq!(request.args, vec!["hello", "world"]);
271 assert_eq!(request.env.get("PATH"), Some(&"/usr/bin".to_string()));
272 assert_eq!(request.working_dir, Some("/tmp".to_string()));
273 assert_eq!(request.timeout, Some(Duration::from_secs(30)));
274 assert_eq!(request.stdin, Some("test input".to_string()));
275 }
276
277 #[tokio::test]
278 async fn test_command_context() {
279 let context = CommandContext::new();
280 let id = Uuid::new_v4();
281
282 let (cancel_tx, _cancel_rx) = mpsc::channel(1);
283 let (status_tx, _status_rx) = mpsc::channel(1);
284 let handle = CommandHandle::new(id, cancel_tx, status_tx);
285
286 context.register(id, handle.clone()).await;
287
288 let retrieved = context.get(&id).await;
289 assert!(retrieved.is_some());
290 assert_eq!(retrieved.unwrap().id, id);
291
292 context.unregister(&id).await;
293 assert!(context.get(&id).await.is_none());
294 }
295
296 #[tokio::test]
297 async fn test_progress_tracker() {
298 let id = Uuid::new_v4();
299 let (tx, mut rx) = mpsc::channel(10);
300 let tracker = ProgressTracker::new(id, tx);
301
302 tracker.update(50.0, "Half way there").await.unwrap();
303
304 let event = rx.recv().await.unwrap();
305 match event {
306 CommandEvent::Progress { command_id, percentage, message, .. } => {
307 assert_eq!(command_id, id);
308 assert_eq!(percentage, 50.0);
309 assert_eq!(message, "Half way there");
310 }
311 _ => panic!("Expected Progress event"),
312 }
313 }
314}