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    ///
137    /// Handles both Ctrl+C (SIGINT) and Ctrl+Z (SIGTSTP) signals.
138    /// Ctrl+Z triggers a graceful shutdown instead of suspending the process,
139    /// which prevents the zombie-port scenario where a suspended process holds
140    /// a port but never processes incoming data.
141    pub async fn run_with_shutdown<C: Command>(&self, cmd: &C) -> CliResult<CommandOutput> {
142        if !cmd.supports_shutdown() {
143            return self.run(cmd).await;
144        }
145
146        let shutdown_signal = {
147            let ctx = self.ctx.read().await;
148            ctx.shutdown_signal()
149        };
150
151        // Setup Ctrl+C handler
152        let signal = shutdown_signal.clone();
153        ctrlc::set_handler(move || {
154            signal.notify_waiters();
155        })
156        .map_err(|e| CliError::ExecutionFailed {
157            message: format!("Failed to set Ctrl+C handler: {}", e),
158        })?;
159
160        // Setup SIGTSTP handler (Ctrl+Z) — treat as graceful shutdown instead of suspend.
161        // A suspended process keeps holding the port, creating a zombie-port scenario
162        // that is very difficult to diagnose: TCP connects succeed (kernel handles SYN/ACK)
163        // but all application-layer reads time out indefinitely.
164        #[cfg(unix)]
165        {
166            let sigtstp_shutdown = shutdown_signal.clone();
167            let mut sigtstp = tokio::signal::unix::signal(
168                tokio::signal::unix::SignalKind::from_raw(libc::SIGTSTP),
169            )
170            .map_err(|e| CliError::ExecutionFailed {
171                message: format!("Failed to set SIGTSTP handler: {}", e),
172            })?;
173
174            tokio::spawn(async move {
175                if sigtstp.recv().await.is_some() {
176                    eprintln!(
177                        "\n⚠ Received Ctrl+Z (SIGTSTP). Performing graceful shutdown instead of \
178                         suspending to release the port.\n  \
179                         Use 'kill -STOP <pid>' if you really need to suspend."
180                    );
181                    sigtstp_shutdown.notify_waiters();
182                }
183            });
184        }
185
186        // Run command with shutdown support
187        tokio::select! {
188            result = self.run(cmd) => result,
189            _ = shutdown_signal.notified() => {
190                let ctx = self.ctx.read().await;
191                ctx.output().info("Shutting down...");
192                Err(CliError::Interrupted)
193            }
194        }
195    }
196
197    /// Get the context.
198    pub fn context(&self) -> Arc<RwLock<CliContext>> {
199        self.ctx.clone()
200    }
201}
202
203/// Hook trait for command lifecycle events.
204#[async_trait]
205pub trait CommandHook: Send + Sync {
206    /// Called before command execution.
207    async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
208        Ok(())
209    }
210
211    /// Called after command execution.
212    async fn after_execute(&self, _cmd_name: &str, _success: bool) -> CliResult<()> {
213        Ok(())
214    }
215}
216
217/// Logging hook for command execution.
218pub struct LoggingHook;
219
220#[async_trait]
221impl CommandHook for LoggingHook {
222    async fn before_execute(&self, cmd_name: &str) -> CliResult<()> {
223        tracing::info!(command = cmd_name, "Executing command");
224        Ok(())
225    }
226
227    async fn after_execute(&self, cmd_name: &str, success: bool) -> CliResult<()> {
228        if success {
229            tracing::info!(command = cmd_name, "Command completed successfully");
230        } else {
231            tracing::warn!(command = cmd_name, "Command failed");
232        }
233        Ok(())
234    }
235}
236
237/// Metrics hook for command execution.
238pub struct MetricsHook {
239    start_time: std::sync::Mutex<Option<std::time::Instant>>,
240}
241
242impl MetricsHook {
243    pub fn new() -> Self {
244        Self {
245            start_time: std::sync::Mutex::new(None),
246        }
247    }
248}
249
250impl Default for MetricsHook {
251    fn default() -> Self {
252        Self::new()
253    }
254}
255
256#[async_trait]
257impl CommandHook for MetricsHook {
258    async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
259        *self.start_time.lock().unwrap() = Some(std::time::Instant::now());
260        Ok(())
261    }
262
263    async fn after_execute(&self, cmd_name: &str, success: bool) -> CliResult<()> {
264        if let Some(start) = self.start_time.lock().unwrap().take() {
265            let duration = start.elapsed();
266            tracing::debug!(
267                command = cmd_name,
268                success = success,
269                duration_ms = duration.as_millis() as u64,
270                "Command execution metrics"
271            );
272        }
273        Ok(())
274    }
275}
276
277/// Command factory for dynamic command creation.
278pub trait CommandFactory: Send + Sync {
279    /// Get the protocol this factory supports.
280    fn protocol(&self) -> &str;
281
282    /// Create a run command for this protocol.
283    fn create_run_command(&self, args: &RunCommandArgs) -> Box<dyn Command>;
284
285    /// Create a list command for this protocol.
286    fn create_list_command(&self) -> Box<dyn Command>;
287
288    /// Create a validate command for this protocol.
289    fn create_validate_command(&self, path: std::path::PathBuf) -> Box<dyn Command>;
290}
291
292/// Arguments for run commands.
293#[derive(Debug, Clone)]
294pub struct RunCommandArgs {
295    pub port: Option<u16>,
296    pub devices: usize,
297    pub points_per_device: usize,
298    pub tick_interval_ms: u64,
299}
300
301impl Default for RunCommandArgs {
302    fn default() -> Self {
303        Self {
304            port: None,
305            devices: 1,
306            points_per_device: 100,
307            tick_interval_ms: 100,
308        }
309    }
310}