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 pub fn quiet_failure(code: i32) -> Self {
91 Self {
92 exit_code: code,
93 message: None,
94 quiet: true,
95 }
96 }
97}
98
99pub struct CommandRunner {
101 ctx: Arc<RwLock<CliContext>>,
102 hooks: Vec<Box<dyn CommandHook>>,
103}
104
105impl CommandRunner {
106 pub fn new(ctx: CliContext) -> Self {
108 Self {
109 ctx: Arc::new(RwLock::new(ctx)),
110 hooks: Vec::new(),
111 }
112 }
113
114 pub fn add_hook(&mut self, hook: impl CommandHook + 'static) {
116 self.hooks.push(Box::new(hook));
117 }
118
119 pub async fn run(&self, cmd: &dyn Command) -> CliResult<CommandOutput> {
121 cmd.validate()?;
123
124 for hook in &self.hooks {
126 hook.before_execute(cmd.name()).await?;
127 }
128
129 let mut ctx = self.ctx.write().await;
131 let result = cmd.execute(&mut ctx).await;
132
133 let is_success = result.is_ok();
135 for hook in &self.hooks {
136 hook.after_execute(cmd.name(), is_success).await?;
137 }
138
139 result
140 }
141
142 pub async fn run_with_shutdown<C: Command>(&self, cmd: &C) -> CliResult<CommandOutput> {
149 if !cmd.supports_shutdown() {
150 return self.run(cmd).await;
151 }
152
153 let shutdown_signal = {
154 let ctx = self.ctx.read().await;
155 ctx.shutdown_signal()
156 };
157
158 let signal = shutdown_signal.clone();
160 ctrlc::set_handler(move || {
161 signal.notify_waiters();
162 })
163 .map_err(|e| CliError::ExecutionFailed {
164 message: format!("Failed to set Ctrl+C handler: {}", e),
165 })?;
166
167 #[cfg(unix)]
172 {
173 let sigtstp_shutdown = shutdown_signal.clone();
174 let mut sigtstp = tokio::signal::unix::signal(
175 tokio::signal::unix::SignalKind::from_raw(libc::SIGTSTP),
176 )
177 .map_err(|e| CliError::ExecutionFailed {
178 message: format!("Failed to set SIGTSTP handler: {}", e),
179 })?;
180
181 tokio::spawn(async move {
182 if sigtstp.recv().await.is_some() {
183 eprintln!(
184 "\n⚠ Received Ctrl+Z (SIGTSTP). Performing graceful shutdown instead of \
185 suspending to release the port.\n \
186 Use 'kill -STOP <pid>' if you really need to suspend."
187 );
188 sigtstp_shutdown.notify_waiters();
189 }
190 });
191 }
192
193 self.run(cmd).await
196 }
197
198 pub fn context(&self) -> Arc<RwLock<CliContext>> {
200 self.ctx.clone()
201 }
202}
203
204#[async_trait]
206pub trait CommandHook: Send + Sync {
207 async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
209 Ok(())
210 }
211
212 async fn after_execute(&self, _cmd_name: &str, _success: bool) -> CliResult<()> {
214 Ok(())
215 }
216}
217
218pub struct LoggingHook;
220
221#[async_trait]
222impl CommandHook for LoggingHook {
223 async fn before_execute(&self, cmd_name: &str) -> CliResult<()> {
224 tracing::info!(command = cmd_name, "Executing command");
225 Ok(())
226 }
227
228 async fn after_execute(&self, cmd_name: &str, success: bool) -> CliResult<()> {
229 if success {
230 tracing::info!(command = cmd_name, "Command completed successfully");
231 } else {
232 tracing::warn!(command = cmd_name, "Command failed");
233 }
234 Ok(())
235 }
236}
237
238pub struct MetricsHook {
240 start_time: std::sync::Mutex<Option<std::time::Instant>>,
241}
242
243impl MetricsHook {
244 pub fn new() -> Self {
245 Self {
246 start_time: std::sync::Mutex::new(None),
247 }
248 }
249}
250
251impl Default for MetricsHook {
252 fn default() -> Self {
253 Self::new()
254 }
255}
256
257#[async_trait]
258impl CommandHook for MetricsHook {
259 async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
260 *self.start_time.lock().unwrap() = Some(std::time::Instant::now());
261 Ok(())
262 }
263
264 async fn after_execute(&self, cmd_name: &str, success: bool) -> CliResult<()> {
265 if let Some(start) = self.start_time.lock().unwrap().take() {
266 let duration = start.elapsed();
267 tracing::debug!(
268 command = cmd_name,
269 success = success,
270 duration_ms = duration.as_millis() as u64,
271 "Command execution metrics"
272 );
273 }
274 Ok(())
275 }
276}
277
278pub trait CommandFactory: Send + Sync {
280 fn protocol(&self) -> &str;
282
283 fn create_run_command(&self, args: &RunCommandArgs) -> Box<dyn Command>;
285
286 fn create_list_command(&self) -> Box<dyn Command>;
288
289 fn create_validate_command(&self, path: std::path::PathBuf) -> Box<dyn Command>;
291}
292
293#[derive(Debug, Clone)]
295pub struct RunCommandArgs {
296 pub port: Option<u16>,
297 pub devices: usize,
298 pub points_per_device: usize,
299 pub tick_interval_ms: u64,
300}
301
302impl Default for RunCommandArgs {
303 fn default() -> Self {
304 Self {
305 port: None,
306 devices: 1,
307 points_per_device: 100,
308 tick_interval_ms: 100,
309 }
310 }
311}