use crate::context::CliContext;
use crate::error::{CliError, CliResult};
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::RwLock;
#[async_trait]
pub trait Command: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
async fn execute(&self, ctx: &mut CliContext) -> CliResult<CommandOutput>;
fn validate(&self) -> CliResult<()> {
Ok(())
}
fn requires_engine(&self) -> bool {
false
}
fn supports_shutdown(&self) -> bool {
false
}
}
#[derive(Debug, Default)]
pub struct CommandOutput {
pub exit_code: i32,
pub message: Option<String>,
pub quiet: bool,
}
impl CommandOutput {
pub fn success() -> Self {
Self {
exit_code: 0,
message: None,
quiet: false,
}
}
pub fn success_with_message(msg: impl Into<String>) -> Self {
Self {
exit_code: 0,
message: Some(msg.into()),
quiet: false,
}
}
pub fn quiet_success() -> Self {
Self {
exit_code: 0,
message: None,
quiet: true,
}
}
pub fn failure(code: i32, msg: impl Into<String>) -> Self {
Self {
exit_code: code,
message: Some(msg.into()),
quiet: false,
}
}
}
pub struct CommandRunner {
ctx: Arc<RwLock<CliContext>>,
hooks: Vec<Box<dyn CommandHook>>,
}
impl CommandRunner {
pub fn new(ctx: CliContext) -> Self {
Self {
ctx: Arc::new(RwLock::new(ctx)),
hooks: Vec::new(),
}
}
pub fn add_hook(&mut self, hook: impl CommandHook + 'static) {
self.hooks.push(Box::new(hook));
}
pub async fn run(&self, cmd: &dyn Command) -> CliResult<CommandOutput> {
cmd.validate()?;
for hook in &self.hooks {
hook.before_execute(cmd.name()).await?;
}
let mut ctx = self.ctx.write().await;
let result = cmd.execute(&mut ctx).await;
let is_success = result.is_ok();
for hook in &self.hooks {
hook.after_execute(cmd.name(), is_success).await?;
}
result
}
pub async fn run_with_shutdown<C: Command>(&self, cmd: &C) -> CliResult<CommandOutput> {
if !cmd.supports_shutdown() {
return self.run(cmd).await;
}
let shutdown_signal = {
let ctx = self.ctx.read().await;
ctx.shutdown_signal()
};
let signal = shutdown_signal.clone();
ctrlc::set_handler(move || {
signal.notify_waiters();
})
.map_err(|e| CliError::ExecutionFailed {
message: format!("Failed to set Ctrl+C handler: {}", e),
})?;
#[cfg(unix)]
{
let sigtstp_shutdown = shutdown_signal.clone();
let mut sigtstp = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::from_raw(libc::SIGTSTP),
)
.map_err(|e| CliError::ExecutionFailed {
message: format!("Failed to set SIGTSTP handler: {}", e),
})?;
tokio::spawn(async move {
if sigtstp.recv().await.is_some() {
eprintln!(
"\n⚠ Received Ctrl+Z (SIGTSTP). Performing graceful shutdown instead of \
suspending to release the port.\n \
Use 'kill -STOP <pid>' if you really need to suspend."
);
sigtstp_shutdown.notify_waiters();
}
});
}
self.run(cmd).await
}
pub fn context(&self) -> Arc<RwLock<CliContext>> {
self.ctx.clone()
}
}
#[async_trait]
pub trait CommandHook: Send + Sync {
async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
Ok(())
}
async fn after_execute(&self, _cmd_name: &str, _success: bool) -> CliResult<()> {
Ok(())
}
}
pub struct LoggingHook;
#[async_trait]
impl CommandHook for LoggingHook {
async fn before_execute(&self, cmd_name: &str) -> CliResult<()> {
tracing::info!(command = cmd_name, "Executing command");
Ok(())
}
async fn after_execute(&self, cmd_name: &str, success: bool) -> CliResult<()> {
if success {
tracing::info!(command = cmd_name, "Command completed successfully");
} else {
tracing::warn!(command = cmd_name, "Command failed");
}
Ok(())
}
}
pub struct MetricsHook {
start_time: std::sync::Mutex<Option<std::time::Instant>>,
}
impl MetricsHook {
pub fn new() -> Self {
Self {
start_time: std::sync::Mutex::new(None),
}
}
}
impl Default for MetricsHook {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl CommandHook for MetricsHook {
async fn before_execute(&self, _cmd_name: &str) -> CliResult<()> {
*self.start_time.lock().unwrap() = Some(std::time::Instant::now());
Ok(())
}
async fn after_execute(&self, cmd_name: &str, success: bool) -> CliResult<()> {
if let Some(start) = self.start_time.lock().unwrap().take() {
let duration = start.elapsed();
tracing::debug!(
command = cmd_name,
success = success,
duration_ms = duration.as_millis() as u64,
"Command execution metrics"
);
}
Ok(())
}
}
pub trait CommandFactory: Send + Sync {
fn protocol(&self) -> &str;
fn create_run_command(&self, args: &RunCommandArgs) -> Box<dyn Command>;
fn create_list_command(&self) -> Box<dyn Command>;
fn create_validate_command(&self, path: std::path::PathBuf) -> Box<dyn Command>;
}
#[derive(Debug, Clone)]
pub struct RunCommandArgs {
pub port: Option<u16>,
pub devices: usize,
pub points_per_device: usize,
pub tick_interval_ms: u64,
}
impl Default for RunCommandArgs {
fn default() -> Self {
Self {
port: None,
devices: 1,
points_per_device: 100,
tick_interval_ms: 100,
}
}
}