1use 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#[async_trait]
17pub trait Command: Send + Sync {
18 fn name(&self) -> &str;
20
21 fn description(&self) -> &str;
23
24 async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput>;
26
27 fn validate(&self) -> CliResult<()> {
29 Ok(())
30 }
31
32 fn requires_engine(&self) -> bool {
34 false
35 }
36
37 fn supports_shutdown(&self) -> bool {
39 false
40 }
41}
42
43#[derive(Debug, Default)]
45pub struct CommandOutput {
46 pub exit_code: i32,
48 pub message: Option<String>,
50 pub quiet: bool,
52}
53
54impl CommandOutput {
55 pub fn success() -> Self {
57 Self {
58 exit_code: 0,
59 message: None,
60 quiet: false,
61 }
62 }
63
64 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 pub fn quiet_success() -> Self {
75 Self {
76 exit_code: 0,
77 message: None,
78 quiet: true,
79 }
80 }
81
82 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
92pub struct CommandRunner {
94 ctx: Arc<RwLock<CliContext>>,
95 hooks: Vec<Box<dyn CommandHook>>,
96}
97
98impl CommandRunner {
99 pub fn new(ctx: CliContext) -> Self {
101 Self {
102 ctx: Arc::new(RwLock::new(ctx)),
103 hooks: Vec::new(),
104 }
105 }
106
107 pub fn add_hook(&mut self, hook: impl CommandHook + 'static) {
109 self.hooks.push(Box::new(hook));
110 }
111
112 pub async fn run(&self, cmd: &dyn Command) -> CliResult<CommandOutput> {
114 cmd.validate()?;
116
117 for hook in &self.hooks {
119 hook.before_execute(cmd.name()).await?;
120 }
121
122 let mut ctx = self.ctx.write().await;
124 let result = cmd.execute(&mut ctx).await;
125
126 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 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 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 #[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 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 pub fn context(&self) -> Arc<RwLock<CliContext>> {
199 self.ctx.clone()
200 }
201}
202
203#[async_trait]
205pub trait CommandHook: Send + Sync {
206 async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
208 Ok(())
209 }
210
211 async fn after_execute(&self, _cmd_name: &str, _success: bool) -> CliResult<()> {
213 Ok(())
214 }
215}
216
217pub 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
237pub 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
277pub trait CommandFactory: Send + Sync {
279 fn protocol(&self) -> &str;
281
282 fn create_run_command(&self, args: &RunCommandArgs) -> Box<dyn Command>;
284
285 fn create_list_command(&self) -> Box<dyn Command>;
287
288 fn create_validate_command(&self, path: std::path::PathBuf) -> Box<dyn Command>;
290}
291
292#[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}