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> {
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 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 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 pub fn context(&self) -> Arc<RwLock<CliContext>> {
168 self.ctx.clone()
169 }
170}
171
172#[async_trait]
174pub trait CommandHook: Send + Sync {
175 async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
177 Ok(())
178 }
179
180 async fn after_execute(&self, _cmd_name: &str, _success: bool) -> CliResult<()> {
182 Ok(())
183 }
184}
185
186pub 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
206pub 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
246pub trait CommandFactory: Send + Sync {
248 fn protocol(&self) -> &str;
250
251 fn create_run_command(&self, args: &RunCommandArgs) -> Box<dyn Command>;
253
254 fn create_list_command(&self) -> Box<dyn Command>;
256
257 fn create_validate_command(&self, path: std::path::PathBuf) -> Box<dyn Command>;
259}
260
261#[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}