1use crate::context::CliContext;
6use crate::error::{CliError, CliResult};
7use async_trait::async_trait;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11#[async_trait]
15pub trait Command: Send + Sync {
16 fn name(&self) -> &str;
18
19 fn description(&self) -> &str;
21
22 async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput>;
24
25 fn validate(&self) -> CliResult<()> {
27 Ok(())
28 }
29
30 fn requires_engine(&self) -> bool {
32 false
33 }
34
35 fn supports_shutdown(&self) -> bool {
37 false
38 }
39}
40
41#[derive(Debug, Default)]
43pub struct CommandOutput {
44 pub exit_code: i32,
46 pub message: Option<String>,
48 pub quiet: bool,
50}
51
52impl CommandOutput {
53 pub fn success() -> Self {
55 Self {
56 exit_code: 0,
57 message: None,
58 quiet: false,
59 }
60 }
61
62 pub fn success_with_message(msg: impl Into<String>) -> Self {
64 Self {
65 exit_code: 0,
66 message: Some(msg.into()),
67 quiet: false,
68 }
69 }
70
71 pub fn quiet_success() -> Self {
73 Self {
74 exit_code: 0,
75 message: None,
76 quiet: true,
77 }
78 }
79
80 pub fn failure(code: i32, msg: impl Into<String>) -> Self {
82 Self {
83 exit_code: code,
84 message: Some(msg.into()),
85 quiet: false,
86 }
87 }
88}
89
90pub struct CommandRunner {
92 ctx: Arc<RwLock<CliContext>>,
93 hooks: Vec<Box<dyn CommandHook>>,
94}
95
96impl CommandRunner {
97 pub fn new(ctx: CliContext) -> Self {
99 Self {
100 ctx: Arc::new(RwLock::new(ctx)),
101 hooks: Vec::new(),
102 }
103 }
104
105 pub fn add_hook(&mut self, hook: impl CommandHook + 'static) {
107 self.hooks.push(Box::new(hook));
108 }
109
110 pub async fn run(&self, cmd: &dyn Command) -> CliResult<CommandOutput> {
112 cmd.validate()?;
114
115 for hook in &self.hooks {
117 hook.before_execute(cmd.name()).await?;
118 }
119
120 let mut ctx = self.ctx.write().await;
122 let result = cmd.execute(&mut ctx).await;
123
124 let is_success = result.is_ok();
126 for hook in &self.hooks {
127 hook.after_execute(cmd.name(), is_success).await?;
128 }
129
130 result
131 }
132
133 pub async fn run_with_shutdown<C: Command>(&self, cmd: &C) -> CliResult<CommandOutput> {
140 if !cmd.supports_shutdown() {
141 return self.run(cmd).await;
142 }
143
144 let shutdown_signal = {
145 let ctx = self.ctx.read().await;
146 ctx.shutdown_signal()
147 };
148
149 let signal = shutdown_signal.clone();
151 ctrlc::set_handler(move || {
152 signal.notify_waiters();
153 })
154 .map_err(|e| CliError::ExecutionFailed {
155 message: format!("Failed to set Ctrl+C handler: {}", e),
156 })?;
157
158 #[cfg(unix)]
163 {
164 let sigtstp_shutdown = shutdown_signal.clone();
165 let mut sigtstp = tokio::signal::unix::signal(
166 tokio::signal::unix::SignalKind::from_raw(libc::SIGTSTP),
167 )
168 .map_err(|e| CliError::ExecutionFailed {
169 message: format!("Failed to set SIGTSTP handler: {}", e),
170 })?;
171
172 tokio::spawn(async move {
173 if sigtstp.recv().await.is_some() {
174 eprintln!(
175 "\n⚠ Received Ctrl+Z (SIGTSTP). Performing graceful shutdown instead of \
176 suspending to release the port.\n \
177 Use 'kill -STOP <pid>' if you really need to suspend."
178 );
179 sigtstp_shutdown.notify_waiters();
180 }
181 });
182 }
183
184 self.run(cmd).await
187 }
188
189 pub fn context(&self) -> Arc<RwLock<CliContext>> {
191 self.ctx.clone()
192 }
193}
194
195#[async_trait]
197pub trait CommandHook: Send + Sync {
198 async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
200 Ok(())
201 }
202
203 async fn after_execute(&self, _cmd_name: &str, _success: bool) -> CliResult<()> {
205 Ok(())
206 }
207}
208
209pub struct LoggingHook;
211
212#[async_trait]
213impl CommandHook for LoggingHook {
214 async fn before_execute(&self, cmd_name: &str) -> CliResult<()> {
215 tracing::info!(command = cmd_name, "Executing command");
216 Ok(())
217 }
218
219 async fn after_execute(&self, cmd_name: &str, success: bool) -> CliResult<()> {
220 if success {
221 tracing::info!(command = cmd_name, "Command completed successfully");
222 } else {
223 tracing::warn!(command = cmd_name, "Command failed");
224 }
225 Ok(())
226 }
227}
228
229pub struct MetricsHook {
231 start_time: std::sync::Mutex<Option<std::time::Instant>>,
232}
233
234impl MetricsHook {
235 pub fn new() -> Self {
236 Self {
237 start_time: std::sync::Mutex::new(None),
238 }
239 }
240}
241
242impl Default for MetricsHook {
243 fn default() -> Self {
244 Self::new()
245 }
246}
247
248#[async_trait]
249impl CommandHook for MetricsHook {
250 async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
251 *self.start_time.lock().unwrap() = Some(std::time::Instant::now());
252 Ok(())
253 }
254
255 async fn after_execute(&self, cmd_name: &str, success: bool) -> CliResult<()> {
256 if let Some(start) = self.start_time.lock().unwrap().take() {
257 let duration = start.elapsed();
258 tracing::debug!(
259 command = cmd_name,
260 success = success,
261 duration_ms = duration.as_millis() as u64,
262 "Command execution metrics"
263 );
264 }
265 Ok(())
266 }
267}
268
269pub trait CommandFactory: Send + Sync {
271 fn protocol(&self) -> &str;
273
274 fn create_run_command(&self, args: &RunCommandArgs) -> Box<dyn Command>;
276
277 fn create_list_command(&self) -> Box<dyn Command>;
279
280 fn create_validate_command(&self, path: std::path::PathBuf) -> Box<dyn Command>;
282}
283
284#[derive(Debug, Clone)]
286pub struct RunCommandArgs {
287 pub port: Option<u16>,
288 pub devices: usize,
289 pub points_per_device: usize,
290 pub tick_interval_ms: u64,
291}
292
293impl Default for RunCommandArgs {
294 fn default() -> Self {
295 Self {
296 port: None,
297 devices: 1,
298 points_per_device: 100,
299 tick_interval_ms: 100,
300 }
301 }
302}