1use async_trait::async_trait;
4use futures::{Stream, StreamExt};
5use reqwest::Client;
6use serde::{Deserialize, Serialize};
7use std::pin::Pin;
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::sync::mpsc;
11use tracing::error;
12use uuid::Uuid;
13
14use crate::command::{
15 CommandContext, CommandEvent, CommandExecutor, CommandHandle, CommandRequest, CommandResult,
16};
17use crate::error::UbiquityError;
18
19pub struct CloudCommandExecutor {
21 context: Arc<CommandContext>,
22 event_buffer_size: usize,
23 client: Client,
24 worker_url: String,
25 api_token: String,
26 namespace_id: String,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31struct CloudExecuteRequest {
32 pub request: CommandRequest,
33 pub namespace_id: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38struct CloudExecuteResponse {
39 pub durable_object_id: String,
40 pub websocket_url: String,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(tag = "type")]
46enum CloudWebSocketMessage {
47 Subscribe {
48 command_id: Uuid,
49 },
50 Event {
51 event: CommandEvent,
52 },
53 Cancel {
54 command_id: Uuid,
55 },
56 Status {
57 command_id: Uuid,
58 },
59 StatusResponse {
60 result: Option<CommandResult>,
61 },
62 Error {
63 message: String,
64 },
65}
66
67impl CloudCommandExecutor {
68 pub fn new(worker_url: String, api_token: String, namespace_id: String) -> Self {
69 Self {
70 context: Arc::new(CommandContext::new()),
71 event_buffer_size: 1024,
72 client: Client::builder()
73 .timeout(Duration::from_secs(30))
74 .build()
75 .unwrap(),
76 worker_url,
77 api_token,
78 namespace_id,
79 }
80 }
81
82 async fn create_durable_object(
83 &self,
84 request: CommandRequest,
85 ) -> Result<CloudExecuteResponse, UbiquityError> {
86 let url = format!("{}/api/commands/execute", self.worker_url);
87
88 let cloud_request = CloudExecuteRequest {
89 request,
90 namespace_id: self.namespace_id.clone(),
91 };
92
93 let response = self
94 .client
95 .post(&url)
96 .header("Authorization", format!("Bearer {}", self.api_token))
97 .json(&cloud_request)
98 .send()
99 .await
100 .map_err(|e| UbiquityError::Network(format!("Failed to create durable object: {}", e)))?;
101
102 if !response.status().is_success() {
103 let status = response.status();
104 let body = response.text().await.unwrap_or_default();
105 return Err(UbiquityError::CloudExecution(format!(
106 "Failed to create durable object: {} - {}",
107 status, body
108 )));
109 }
110
111 response
112 .json::<CloudExecuteResponse>()
113 .await
114 .map_err(|e| UbiquityError::Serialization(format!("Failed to parse response: {}", e)))
115 }
116
117 async fn connect_websocket(
118 &self,
119 websocket_url: &str,
120 command_id: Uuid,
121 event_tx: mpsc::Sender<CommandEvent>,
122 ) -> Result<(), UbiquityError> {
123 use tokio_tungstenite::{connect_async, tungstenite::Message};
124
125 let (ws_stream, _) = connect_async(websocket_url)
126 .await
127 .map_err(|e| UbiquityError::Network(format!("Failed to connect WebSocket: {}", e)))?;
128
129 let (write, read) = ws_stream.split();
130 let (internal_tx, mut internal_rx) = mpsc::channel::<Message>(100);
131
132 let subscribe_msg = CloudWebSocketMessage::Subscribe { command_id };
134 let msg_text = serde_json::to_string(&subscribe_msg)
135 .map_err(|e| UbiquityError::Serialization(e.to_string()))?;
136
137 internal_tx
138 .send(Message::Text(msg_text))
139 .await
140 .map_err(|_| UbiquityError::Internal("Failed to send subscribe message".to_string()))?;
141
142 let write_task = tokio::spawn(async move {
144 use futures::SinkExt;
145 let mut write = write;
146 while let Some(msg) = internal_rx.recv().await {
147 if let Err(e) = write.send(msg).await {
148 error!("WebSocket write error: {}", e);
149 break;
150 }
151 }
152 });
153
154 let read_task = tokio::spawn(async move {
156 use futures::StreamExt;
157 let mut read = read;
158 while let Some(result) = read.next().await {
159 match result {
160 Ok(Message::Text(text)) => {
161 match serde_json::from_str::<CloudWebSocketMessage>(&text) {
162 Ok(CloudWebSocketMessage::Event { event }) => {
163 if event_tx.send(event).await.is_err() {
164 break;
165 }
166 }
167 Ok(CloudWebSocketMessage::Error { message }) => {
168 error!("Cloud execution error: {}", message);
169 let _ = event_tx
170 .send(CommandEvent::Failed {
171 command_id,
172 error: message,
173 duration_ms: 0,
174 timestamp: chrono::Utc::now(),
175 })
176 .await;
177 break;
178 }
179 _ => {}
180 }
181 }
182 Ok(Message::Close(_)) => break,
183 Err(e) => {
184 error!("WebSocket read error: {}", e);
185 break;
186 }
187 _ => {}
188 }
189 }
190 });
191
192 tokio::select! {
194 _ = write_task => {}
195 _ = read_task => {}
196 }
197
198 Ok(())
199 }
200
201 async fn execute_cloud(
202 request: CommandRequest,
203 event_tx: mpsc::Sender<CommandEvent>,
204 executor: CloudCommandExecutor,
205 ) -> Result<(), UbiquityError> {
206 let command_id = request.id;
207
208 let response = executor.create_durable_object(request).await?;
210
211 executor
213 .connect_websocket(&response.websocket_url, command_id, event_tx)
214 .await
215 }
216}
217
218#[async_trait]
219impl CommandExecutor for CloudCommandExecutor {
220 async fn execute(
221 &self,
222 request: CommandRequest,
223 ) -> Result<Pin<Box<dyn Stream<Item = CommandEvent> + Send>>, UbiquityError> {
224 let (event_tx, event_rx) = mpsc::channel(self.event_buffer_size);
225 let (cancel_tx, _cancel_rx) = mpsc::channel(1);
226 let (status_tx, _status_rx) = mpsc::channel(1);
227
228 let command_id = request.id;
229 let handle = CommandHandle::new(command_id, cancel_tx, status_tx);
230
231 self.context.register(command_id, handle).await;
233
234 let executor = self.clone();
236 let context = self.context.clone();
237
238 tokio::spawn(async move {
240 let result = Self::execute_cloud(request, event_tx, executor).await;
241
242 context.unregister(&command_id).await;
244
245 if let Err(e) = result {
246 error!("Cloud command execution error: {}", e);
247 }
248 });
249
250 Ok(Box::pin(tokio_stream::wrappers::ReceiverStream::new(event_rx)))
251 }
252
253 async fn cancel(&self, command_id: Uuid) -> Result<(), UbiquityError> {
254 let url = format!("{}/api/commands/{}/cancel", self.worker_url, command_id);
256
257 let response = self
258 .client
259 .post(&url)
260 .header("Authorization", format!("Bearer {}", self.api_token))
261 .send()
262 .await
263 .map_err(|e| UbiquityError::Network(format!("Failed to cancel command: {}", e)))?;
264
265 if !response.status().is_success() {
266 let status = response.status();
267 let body = response.text().await.unwrap_or_default();
268 return Err(UbiquityError::CloudExecution(format!(
269 "Failed to cancel command: {} - {}",
270 status, body
271 )));
272 }
273
274 Ok(())
275 }
276
277 async fn status(&self, command_id: Uuid) -> Result<Option<CommandResult>, UbiquityError> {
278 let url = format!("{}/api/commands/{}/status", self.worker_url, command_id);
280
281 let response = self
282 .client
283 .get(&url)
284 .header("Authorization", format!("Bearer {}", self.api_token))
285 .send()
286 .await
287 .map_err(|e| UbiquityError::Network(format!("Failed to get command status: {}", e)))?;
288
289 if response.status() == 404 {
290 return Ok(None);
291 }
292
293 if !response.status().is_success() {
294 let status = response.status();
295 let body = response.text().await.unwrap_or_default();
296 return Err(UbiquityError::CloudExecution(format!(
297 "Failed to get command status: {} - {}",
298 status, body
299 )));
300 }
301
302 let result = response
303 .json::<CommandResult>()
304 .await
305 .map_err(|e| UbiquityError::Serialization(format!("Failed to parse status: {}", e)))?;
306
307 Ok(Some(result))
308 }
309}
310
311impl Clone for CloudCommandExecutor {
312 fn clone(&self) -> Self {
313 Self {
314 context: self.context.clone(),
315 event_buffer_size: self.event_buffer_size,
316 client: self.client.clone(),
317 worker_url: self.worker_url.clone(),
318 api_token: self.api_token.clone(),
319 namespace_id: self.namespace_id.clone(),
320 }
321 }
322}
323
324#[cfg(feature = "cloudflare-worker")]
326pub mod worker {
327 use super::*;
328
329 #[durable_object]
330 pub struct CommandDurableObject {
331 state: State,
332 env: Env,
333 websockets: Vec<WebSocket>,
334 command_result: Option<CommandResult>,
335 event_history: Vec<CommandEvent>,
336 }
337
338 #[durable_object]
339 impl DurableObject for CommandDurableObject {
340 fn new(state: State, env: Env) -> Self {
341 Self {
342 state,
343 env,
344 websockets: Vec::new(),
345 command_result: None,
346 event_history: Vec::new(),
347 }
348 }
349
350 async fn fetch(&mut self, req: Request) -> Result<Response> {
351 let path = req.path();
352
353 match path.as_str() {
354 "/execute" => self.handle_execute(req).await,
355 "/websocket" => self.handle_websocket(req).await,
356 "/cancel" => self.handle_cancel(req).await,
357 "/status" => self.handle_status(req).await,
358 _ => Response::error("Not Found", 404),
359 }
360 }
361 }
362
363 impl CommandDurableObject {
364 async fn handle_execute(&mut self, mut req: Request) -> Result<Response> {
365 let request: CommandRequest = req.json().await?;
366
367 let event_tx = self.create_event_broadcaster();
369
370 self.simulate_command_execution(request, event_tx).await;
373
374 Response::ok("Command execution started")
375 }
376
377 async fn handle_websocket(&mut self, req: Request) -> Result<Response> {
378 let pair = WebSocketPair::new()?;
379 let server = pair.server;
380
381 server.accept()?;
382 self.websockets.push(server);
383
384 Response::from_websocket(pair.client)
385 }
386
387 async fn handle_cancel(&mut self, _req: Request) -> Result<Response> {
388 let event = CommandEvent::Cancelled {
390 command_id: self.get_command_id()?,
391 duration_ms: 0,
392 timestamp: chrono::Utc::now(),
393 };
394
395 self.broadcast_event(event).await;
396 Response::ok("Command cancelled")
397 }
398
399 async fn handle_status(&mut self, _req: Request) -> Result<Response> {
400 match &self.command_result {
401 Some(result) => Response::ok(serde_json::to_string(result)?),
402 None => Response::error("Command not found", 404),
403 }
404 }
405
406 async fn simulate_command_execution(
407 &mut self,
408 request: CommandRequest,
409 event_tx: mpsc::Sender<CommandEvent>,
410 ) {
411 let start = std::time::Instant::now();
415 let command_id = request.id;
416
417 let _ = event_tx.send(CommandEvent::Started {
419 command_id,
420 command: request.command.clone(),
421 args: request.args.clone(),
422 timestamp: chrono::Utc::now(),
423 }).await;
424
425 let _ = event_tx.send(CommandEvent::Stdout {
427 command_id,
428 data: format!("Executing: {} {}", request.command, request.args.join(" ")),
429 timestamp: chrono::Utc::now(),
430 }).await;
431
432 let duration_ms = start.elapsed().as_millis() as u64;
434 let _ = event_tx.send(CommandEvent::Completed {
435 command_id,
436 exit_code: 0,
437 duration_ms,
438 timestamp: chrono::Utc::now(),
439 }).await;
440
441 self.command_result = Some(CommandResult {
443 id: command_id,
444 exit_code: Some(0),
445 stdout: format!("Executed: {} {}", request.command, request.args.join(" ")),
446 stderr: String::new(),
447 duration_ms,
448 cancelled: false,
449 });
450 }
451
452 fn create_event_broadcaster(&self) -> mpsc::Sender<CommandEvent> {
453 let (tx, mut rx) = mpsc::channel(100);
454
455 let websockets = self.websockets.clone();
456 wasm_bindgen_futures::spawn_local(async move {
457 while let Some(event) = rx.recv().await {
458 let msg = CloudWebSocketMessage::Event { event };
459 let text = serde_json::to_string(&msg).unwrap();
460
461 for ws in &websockets {
462 let _ = ws.send_with_str(&text);
463 }
464 }
465 });
466
467 tx
468 }
469
470 async fn broadcast_event(&mut self, event: CommandEvent) {
471 self.event_history.push(event.clone());
472
473 let msg = CloudWebSocketMessage::Event { event };
474 let text = serde_json::to_string(&msg).unwrap();
475
476 self.websockets.retain(|ws| {
477 ws.send_with_str(&text).is_ok()
478 });
479 }
480
481 fn get_command_id(&self) -> Result<Uuid> {
482 self.command_result
483 .as_ref()
484 .map(|r| r.id)
485 .ok_or_else(|| Error::RustError("No command ID found".to_string()))
486 }
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[tokio::test]
495 async fn test_cloud_executor_creation() {
496 let executor = CloudCommandExecutor::new(
497 "https://example.workers.dev".to_string(),
498 "test-token".to_string(),
499 "test-namespace".to_string(),
500 );
501
502 assert_eq!(executor.worker_url, "https://example.workers.dev");
503 assert_eq!(executor.api_token, "test-token");
504 assert_eq!(executor.namespace_id, "test-namespace");
505 }
506}