Skip to main content

mabi_cli/
runner.rs

1//! Command runner and execution framework.
2//!
3//! Provides the core abstractions for command execution.
4
5use crate::context::CliContext;
6use crate::error::{CliError, CliResult};
7use async_trait::async_trait;
8use std::future::Future;
9use std::pin::Pin;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13/// Trait for executable commands.
14///
15/// All CLI commands implement this trait for consistent execution.
16#[async_trait]
17pub trait Command: Send + Sync {
18    /// Get the command name.
19    fn name(&self) -> &str;
20
21    /// Get the command description.
22    fn description(&self) -> &str;
23
24    /// Execute the command.
25    async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput>;
26
27    /// Validate command arguments before execution.
28    fn validate(&self) -> CliResult<()> {
29        Ok(())
30    }
31
32    /// Check if this command requires an engine instance.
33    fn requires_engine(&self) -> bool {
34        false
35    }
36
37    /// Check if this command supports graceful shutdown.
38    fn supports_shutdown(&self) -> bool {
39        false
40    }
41}
42
43/// Command output type.
44#[derive(Debug, Default)]
45pub struct CommandOutput {
46    /// Exit code (0 = success).
47    pub exit_code: i32,
48    /// Optional message.
49    pub message: Option<String>,
50    /// Whether to suppress default success message.
51    pub quiet: bool,
52}
53
54impl CommandOutput {
55    /// Create a successful output.
56    pub fn success() -> Self {
57        Self {
58            exit_code: 0,
59            message: None,
60            quiet: false,
61        }
62    }
63
64    /// Create a successful output with message.
65    pub fn success_with_message(msg: impl Into<String>) -> Self {
66        Self {
67            exit_code: 0,
68            message: Some(msg.into()),
69            quiet: false,
70        }
71    }
72
73    /// Create a quiet successful output.
74    pub fn quiet_success() -> Self {
75        Self {
76            exit_code: 0,
77            message: None,
78            quiet: true,
79        }
80    }
81
82    /// Create a failed output.
83    pub fn failure(code: i32, msg: impl Into<String>) -> Self {
84        Self {
85            exit_code: code,
86            message: Some(msg.into()),
87            quiet: false,
88        }
89    }
90}
91
92/// Command runner for executing commands with lifecycle management.
93pub struct CommandRunner {
94    ctx: Arc<RwLock<CliContext>>,
95    hooks: Vec<Box<dyn CommandHook>>,
96}
97
98impl CommandRunner {
99    /// Create a new command runner.
100    pub fn new(ctx: CliContext) -> Self {
101        Self {
102            ctx: Arc::new(RwLock::new(ctx)),
103            hooks: Vec::new(),
104        }
105    }
106
107    /// Add a command hook.
108    pub fn add_hook(&mut self, hook: impl CommandHook + 'static) {
109        self.hooks.push(Box::new(hook));
110    }
111
112    /// Run a command.
113    pub async fn run(&self, cmd: &dyn Command) -> CliResult<CommandOutput> {
114        // Validate command
115        cmd.validate()?;
116
117        // Run pre-execution hooks
118        for hook in &self.hooks {
119            hook.before_execute(cmd.name()).await?;
120        }
121
122        // Execute command
123        let mut ctx = self.ctx.write().await;
124        let result = cmd.execute(&mut ctx).await;
125
126        // Run post-execution hooks
127        let is_success = result.is_ok();
128        for hook in &self.hooks {
129            hook.after_execute(cmd.name(), is_success).await?;
130        }
131
132        result
133    }
134
135    /// Run a command with graceful shutdown support.
136    pub async fn run_with_shutdown<C: Command>(&self, cmd: &C) -> CliResult<CommandOutput> {
137        if !cmd.supports_shutdown() {
138            return self.run(cmd).await;
139        }
140
141        let shutdown_signal = {
142            let ctx = self.ctx.read().await;
143            ctx.shutdown_signal()
144        };
145
146        // Setup Ctrl+C handler
147        let signal = shutdown_signal.clone();
148        ctrlc::set_handler(move || {
149            signal.notify_waiters();
150        })
151        .map_err(|e| CliError::ExecutionFailed {
152            message: format!("Failed to set Ctrl+C handler: {}", e),
153        })?;
154
155        // Run command with shutdown support
156        tokio::select! {
157            result = self.run(cmd) => result,
158            _ = shutdown_signal.notified() => {
159                let ctx = self.ctx.read().await;
160                ctx.output().info("Shutting down...");
161                Err(CliError::Interrupted)
162            }
163        }
164    }
165
166    /// Get the context.
167    pub fn context(&self) -> Arc<RwLock<CliContext>> {
168        self.ctx.clone()
169    }
170}
171
172/// Hook trait for command lifecycle events.
173#[async_trait]
174pub trait CommandHook: Send + Sync {
175    /// Called before command execution.
176    async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
177        Ok(())
178    }
179
180    /// Called after command execution.
181    async fn after_execute(&self, _cmd_name: &str, _success: bool) -> CliResult<()> {
182        Ok(())
183    }
184}
185
186/// Logging hook for command execution.
187pub struct LoggingHook;
188
189#[async_trait]
190impl CommandHook for LoggingHook {
191    async fn before_execute(&self, cmd_name: &str) -> CliResult<()> {
192        tracing::info!(command = cmd_name, "Executing command");
193        Ok(())
194    }
195
196    async fn after_execute(&self, cmd_name: &str, success: bool) -> CliResult<()> {
197        if success {
198            tracing::info!(command = cmd_name, "Command completed successfully");
199        } else {
200            tracing::warn!(command = cmd_name, "Command failed");
201        }
202        Ok(())
203    }
204}
205
206/// Metrics hook for command execution.
207pub struct MetricsHook {
208    start_time: std::sync::Mutex<Option<std::time::Instant>>,
209}
210
211impl MetricsHook {
212    pub fn new() -> Self {
213        Self {
214            start_time: std::sync::Mutex::new(None),
215        }
216    }
217}
218
219impl Default for MetricsHook {
220    fn default() -> Self {
221        Self::new()
222    }
223}
224
225#[async_trait]
226impl CommandHook for MetricsHook {
227    async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
228        *self.start_time.lock().unwrap() = Some(std::time::Instant::now());
229        Ok(())
230    }
231
232    async fn after_execute(&self, cmd_name: &str, success: bool) -> CliResult<()> {
233        if let Some(start) = self.start_time.lock().unwrap().take() {
234            let duration = start.elapsed();
235            tracing::debug!(
236                command = cmd_name,
237                success = success,
238                duration_ms = duration.as_millis() as u64,
239                "Command execution metrics"
240            );
241        }
242        Ok(())
243    }
244}
245
246/// Command factory for dynamic command creation.
247pub trait CommandFactory: Send + Sync {
248    /// Get the protocol this factory supports.
249    fn protocol(&self) -> &str;
250
251    /// Create a run command for this protocol.
252    fn create_run_command(&self, args: &RunCommandArgs) -> Box<dyn Command>;
253
254    /// Create a list command for this protocol.
255    fn create_list_command(&self) -> Box<dyn Command>;
256
257    /// Create a validate command for this protocol.
258    fn create_validate_command(&self, path: std::path::PathBuf) -> Box<dyn Command>;
259}
260
261/// Arguments for run commands.
262#[derive(Debug, Clone)]
263pub struct RunCommandArgs {
264    pub port: Option<u16>,
265    pub devices: usize,
266    pub points_per_device: usize,
267    pub tick_interval_ms: u64,
268}
269
270impl Default for RunCommandArgs {
271    fn default() -> Self {
272        Self {
273            port: None,
274            devices: 1,
275            points_per_device: 100,
276            tick_interval_ms: 100,
277        }
278    }
279}