dx_forge/core/
lifecycle.rs

1//! Tool lifecycle management
2//!
3//! Manages the lifecycle of DX tools including starting, stopping, and monitoring their status.
4
5use anyhow::{anyhow, Result};
6use chrono::{DateTime, Utc};
7use std::collections::HashMap;
8use tokio::sync::broadcast;
9use tokio::task::JoinHandle;
10use uuid::Uuid;
11
12use crate::orchestrator::DxTool;
13
14/// Unique identifier for a tool instance
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct ToolId(Uuid);
17
18impl ToolId {
19    /// Create a new random tool ID
20    pub fn new() -> Self {
21        Self(Uuid::new_v4())
22    }
23}
24
25impl Default for ToolId {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31/// Status of a tool
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum ToolStatus {
34    /// Tool is registered but not running
35    Stopped,
36    
37    /// Tool is in the process of starting
38    Starting,
39    
40    /// Tool is running
41    Running,
42    
43    /// Tool is in the process of stopping
44    Stopping,
45    
46    /// Tool failed with an error
47    Failed(String),
48    
49    /// Tool completed successfully
50    Completed,
51}
52
53/// Lifecycle event emitted by the manager
54#[derive(Debug, Clone)]
55pub enum LifecycleEvent {
56    /// Tool is starting
57    ToolStarting {
58        id: ToolId,
59        name: String,
60    },
61    
62    /// Tool has started successfully
63    ToolStarted {
64        id: ToolId,
65        name: String,
66    },
67    
68    /// Tool is stopping
69    ToolStopping {
70        id: ToolId,
71        name: String,
72    },
73    
74    /// Tool has stopped
75    ToolStopped {
76        id: ToolId,
77        name: String,
78    },
79    
80    /// Tool failed with an error
81    ToolFailed {
82        id: ToolId,
83        name: String,
84        error: String,
85    },
86    
87    /// Tool completed successfully
88    ToolCompleted {
89        id: ToolId,
90        name: String,
91    },
92}
93
94/// State of a managed tool
95pub struct ToolState {
96    pub id: ToolId,
97    pub status: ToolStatus,
98    pub tool: Box<dyn DxTool>,
99    pub started_at: Option<DateTime<Utc>>,
100    pub stopped_at: Option<DateTime<Utc>>,
101    pub handle: Option<JoinHandle<()>>,
102}
103
104impl ToolState {
105    fn new(id: ToolId, tool: Box<dyn DxTool>) -> Self {
106        Self {
107            id,
108            status: ToolStatus::Stopped,
109            tool,
110            started_at: None,
111            stopped_at: None,
112            handle: None,
113        }
114    }
115}
116
117/// Manages the lifecycle of DX tools
118pub struct LifecycleManager {
119    tools: HashMap<ToolId, ToolState>,
120    event_bus: broadcast::Sender<LifecycleEvent>,
121}
122
123impl LifecycleManager {
124    /// Create a new lifecycle manager
125    pub fn new() -> Self {
126        let (event_bus, _) = broadcast::channel(1000);
127        
128        Self {
129            tools: HashMap::new(),
130            event_bus,
131        }
132    }
133    
134    /// Register a new tool
135    pub fn register_tool(&mut self, tool: Box<dyn DxTool>) -> Result<ToolId> {
136        let id = ToolId::new();
137        let state = ToolState::new(id, tool);
138        
139        self.tools.insert(id, state);
140        
141        tracing::debug!("Registered tool with id: {:?}", id);
142        Ok(id)
143    }
144    
145    /// Start a tool
146    pub async fn start_tool(&mut self, id: ToolId) -> Result<()> {
147        let state = self.tools.get_mut(&id)
148            .ok_or_else(|| anyhow!("Tool not found: {:?}", id))?;
149        
150        if state.status == ToolStatus::Running {
151            return Err(anyhow!("Tool is already running: {:?}", id));
152        }
153        
154        let tool_name = state.tool.name().to_string();
155        
156        // Emit starting event
157        state.status = ToolStatus::Starting;
158        let _ = self.event_bus.send(LifecycleEvent::ToolStarting {
159            id,
160            name: tool_name.clone(),
161        });
162        
163        // Update state
164        state.status = ToolStatus::Running;
165        state.started_at = Some(Utc::now());
166        
167        // Emit started event
168        let _ = self.event_bus.send(LifecycleEvent::ToolStarted {
169            id,
170            name: tool_name,
171        });
172        
173        tracing::info!("Started tool: {:?}", id);
174        Ok(())
175    }
176    
177    /// Stop a tool
178    pub async fn stop_tool(&mut self, id: ToolId) -> Result<()> {
179        let state = self.tools.get_mut(&id)
180            .ok_or_else(|| anyhow!("Tool not found: {:?}", id))?;
181        
182        if state.status == ToolStatus::Stopped {
183            return Ok(());
184        }
185        
186        let tool_name = state.tool.name().to_string();
187        
188        // Emit stopping event
189        state.status = ToolStatus::Stopping;
190        let _ = self.event_bus.send(LifecycleEvent::ToolStopping {
191            id,
192            name: tool_name.clone(),
193        });
194        
195        // Cancel background task if running
196        if let Some(handle) = state.handle.take() {
197            handle.abort();
198        }
199        
200        // Update state
201        state.status = ToolStatus::Stopped;
202        state.stopped_at = Some(Utc::now());
203        
204        // Emit stopped event
205        let _ = self.event_bus.send(LifecycleEvent::ToolStopped {
206            id,
207            name: tool_name,
208        });
209        
210        tracing::info!("Stopped tool: {:?}", id);
211        Ok(())
212    }
213    
214    /// Get tool status
215    pub fn get_status(&self, id: ToolId) -> Option<ToolStatus> {
216        self.tools.get(&id).map(|state| state.status.clone())
217    }
218    
219    /// Get all tool IDs
220    pub fn list_tool_ids(&self) -> Vec<ToolId> {
221        self.tools.keys().copied().collect()
222    }
223    
224    /// Stop all running tools
225    pub fn stop_all(&mut self) -> Result<()> {
226        let tool_ids: Vec<ToolId> = self.tools.keys().copied().collect();
227        
228        for id in tool_ids {
229            if let Some(state) = self.tools.get(&id) {
230                if state.status == ToolStatus::Running {
231                    // Use blocking since we can't be async in Drop
232                    let rt = tokio::runtime::Handle::try_current();
233                    if let Ok(handle) = rt {
234                        handle.block_on(async {
235                            let _ = self.stop_tool(id).await;
236                        });
237                    }
238                }
239            }
240        }
241        
242        Ok(())
243    }
244    
245    /// Subscribe to lifecycle events
246    pub fn subscribe(&self) -> broadcast::Receiver<LifecycleEvent> {
247        self.event_bus.subscribe()
248    }
249    
250    /// Get count of running tools
251    pub fn running_count(&self) -> usize {
252        self.tools
253            .values()
254            .filter(|state| state.status == ToolStatus::Running)
255            .count()
256    }
257    
258    /// Get count of total registered tools
259    pub fn total_count(&self) -> usize {
260        self.tools.len()
261    }
262}
263
264impl Default for LifecycleManager {
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use crate::orchestrator::{ExecutionContext, ToolOutput};
274    
275    struct TestTool {
276        name: String,
277    }
278    
279    impl DxTool for TestTool {
280        fn name(&self) -> &str {
281            &self.name
282        }
283        
284        fn version(&self) -> &str {
285            "1.0.0"
286        }
287        
288        fn priority(&self) -> u32 {
289            50
290        }
291        
292        fn execute(&mut self, _ctx: &ExecutionContext) -> Result<ToolOutput> {
293            Ok(ToolOutput::success())
294        }
295    }
296    
297    #[tokio::test]
298    async fn test_register_tool() {
299        let mut manager = LifecycleManager::new();
300        let tool = Box::new(TestTool {
301            name: "test-tool".to_string(),
302        });
303        
304        let id = manager.register_tool(tool).unwrap();
305        assert_eq!(manager.total_count(), 1);
306        assert_eq!(manager.get_status(id), Some(ToolStatus::Stopped));
307    }
308    
309    #[tokio::test]
310    async fn test_start_stop_tool() {
311        let mut manager = LifecycleManager::new();
312        let tool = Box::new(TestTool {
313            name: "test-tool".to_string(),
314        });
315        
316        let id = manager.register_tool(tool).unwrap();
317        
318        manager.start_tool(id).await.unwrap();
319        assert_eq!(manager.get_status(id), Some(ToolStatus::Running));
320        assert_eq!(manager.running_count(), 1);
321        
322        manager.stop_tool(id).await.unwrap();
323        assert_eq!(manager.get_status(id), Some(ToolStatus::Stopped));
324        assert_eq!(manager.running_count(), 0);
325    }
326    
327    #[tokio::test]
328    async fn test_lifecycle_events() {
329        let mut manager = LifecycleManager::new();
330        let mut rx = manager.subscribe();
331        
332        let tool = Box::new(TestTool {
333            name: "test-tool".to_string(),
334        });
335        
336        let id = manager.register_tool(tool).unwrap();
337        
338        // Start tool and check events
339        manager.start_tool(id).await.unwrap();
340        
341        if let Ok(event) = rx.try_recv() {
342            match event {
343                LifecycleEvent::ToolStarting { id: _, name } => {
344                    assert_eq!(name, "test-tool");
345                }
346                _ => panic!("Expected ToolStarting event"),
347            }
348        }
349    }
350}