use crate::app_config::AppConfig;
use crate::config::Config;
use crate::services::ota::OtaCoordinator;
use crate::services::{GcsService, UpdateService, VersionService};
use anyhow::Result;
use clap::{Args, Subcommand};
use serde_json;
use tracing::{info, warn};
#[derive(Subcommand)]
pub enum Commands {
Update(UpdateArgs),
Status(StatusArgs),
List,
Version,
Uninstall,
Rollback(RollbackArgs),
#[command(name = "check-update")]
CheckUpdate(CheckUpdateArgs),
Watch(WatchArgs),
}
#[derive(Args)]
pub struct UpdateArgs {
pub version: Option<String>,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Args)]
pub struct RollbackArgs {
pub backup_name: Option<String>,
}
#[derive(Args)]
pub struct StatusArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Args)]
pub struct CheckUpdateArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Args)]
pub struct WatchArgs {
#[arg(long, default_value = "3600")]
pub interval: u64,
}
pub fn handle_command(command: Commands, app: &AppConfig) -> Result<()> {
match command {
Commands::Update(args) => handle_update(args.version, args.dry_run, app),
Commands::Status(args) => handle_status(args.json, app),
Commands::List => handle_list(app),
Commands::Version => handle_version(),
Commands::Uninstall => handle_uninstall(app),
Commands::Rollback(args) => handle_rollback(args.backup_name, app),
Commands::CheckUpdate(args) => handle_check_update(args.json, app),
Commands::Watch(args) => handle_watch(args.interval, app),
}
}
fn handle_update(version: Option<String>, dry_run: bool, app: &AppConfig) -> Result<()> {
if dry_run {
info!("Starting dry-run update process...");
let update_service = UpdateService::with_config(app);
update_service.dry_run_update(version)?;
} else {
info!("Starting update process...");
let update_service = UpdateService::with_config(app);
update_service.update(version)?;
println!("Update completed successfully!");
}
Ok(())
}
fn handle_status(json_output: bool, app: &AppConfig) -> Result<()> {
info!("Checking status...");
let app_binary_path = app.app_binary_path();
let registry_url = app.registry_url();
let ota_coordinator = OtaCoordinator::new(app_binary_path.clone(), app.service_name.clone());
let version_service = VersionService::with_service_name(app.service_name.clone());
let system_info = version_service.get_system_info(&app_binary_path);
let service_status = ota_coordinator.get_service_status_readonly();
let gcs = GcsService::with_bundle_names(
String::new(),
registry_url,
app.release_bundle_name.clone(),
app.checksum_file_name.clone(),
);
let (latest_version, update_available) = match gcs.get_latest_version() {
Ok(latest) => {
let formatted_current = Config::format_version(&system_info.binary_version);
let formatted_latest = Config::format_version(&latest);
let available = formatted_current != formatted_latest;
(formatted_latest, available)
}
Err(e) => {
warn!("Failed to check for updates: {}", e);
(Config::format_version(&system_info.binary_version), false)
}
};
if json_output {
let json_response = serde_json::json!({
"binary_version": system_info.binary_version,
"git_hash": system_info.git_hash,
"build_date": system_info.build_date,
"target_platform": system_info.target_platform,
"deployment_mode": system_info.deployment_mode,
"deployment_size_mb": system_info.deployment_size_mb,
"binary_location": system_info.binary_location,
"service_status": service_status,
"supervisor_version": system_info.supervisor_version,
"required_dependencies": system_info.required_dependencies,
"update_available": update_available,
"latest_version": latest_version,
"ota_metadata": system_info.ota_metadata
});
println!("{}", serde_json::to_string_pretty(&json_response)?);
} else {
println!("Service status: {}", service_status);
println!("Current Version: {}", system_info.binary_version);
println!("Latest Version: {}", latest_version);
println!(
"Update Available: {}",
if update_available { "Yes" } else { "No" }
);
println!("Deployment Mode: {}", system_info.deployment_mode);
println!("Supervisor Version: {}", system_info.supervisor_version);
println!("Git Hash: {}", system_info.git_hash);
println!("Build Date: {}", system_info.build_date);
}
Ok(())
}
fn handle_rollback(backup_name: Option<String>, app: &AppConfig) -> Result<()> {
info!("Processing rollback request...");
let app_binary_path = app.app_binary_path();
let ota_coordinator = OtaCoordinator::new(app_binary_path, app.service_name.clone());
if let Some(backup_name) = backup_name {
info!("Rolling back to backup: {}", backup_name);
ota_coordinator.rollback_to_backup(&backup_name)?;
println!("Successfully rolled back to backup: {}", backup_name);
} else {
info!("Listing available backups...");
let backups = ota_coordinator.list_backups()?;
if backups.is_empty() {
println!("No backups available");
} else {
println!("Available backups:");
for backup in backups {
println!(" {}", backup);
}
}
}
Ok(())
}
fn handle_check_update(json_output: bool, app: &AppConfig) -> Result<()> {
info!("Checking for updates...");
let current_version = Config::format_version(&Config::get_current_version());
let gcs = GcsService::with_bundle_names(
String::new(),
app.registry_url(),
app.release_bundle_name.clone(),
app.checksum_file_name.clone(),
);
let (latest_version, update_available, error) = match gcs.get_latest_version() {
Ok(latest) => {
let formatted_current = Config::format_version(¤t_version);
let formatted_latest = Config::format_version(&latest);
let available = formatted_current != formatted_latest;
(Some(formatted_latest), available, None)
}
Err(e) => {
warn!("Failed to check for updates: {}", e);
(None, false, Some(e.to_string()))
}
};
if json_output {
let json_response = if let Some(error) = error {
format!(
r#"{{"update_available":false,"current_version":"{}","error":"{}"}}"#,
current_version, error
)
} else if let Some(latest) = latest_version {
format!(
r#"{{"update_available":{},"current_version":"{}","latest_version":"{}"}}"#,
update_available, current_version, latest
)
} else {
format!(
r#"{{"update_available":false,"current_version":"{}","error":"Unknown error"}}"#,
current_version
)
};
println!("{}", json_response);
} else {
println!("Current Version: {}", current_version);
if let Some(latest) = latest_version {
println!("Latest Version: {}", latest);
println!(
"Update Available: {}",
if update_available { "Yes" } else { "No" }
);
} else if let Some(error) = error {
println!("Update Check Failed: {}", error);
}
}
Ok(())
}
fn handle_list(app: &AppConfig) -> Result<()> {
info!("Listing available versions...");
let current_version = Config::format_version(&Config::get_current_version());
println!("Installed:");
println!(" {} (current)", current_version);
let ota_coordinator = OtaCoordinator::new(app.app_binary_path(), app.service_name.clone());
match ota_coordinator.list_backups() {
Ok(backups) if !backups.is_empty() => {
println!("\nBackups (rollback targets):");
for backup in &backups {
println!(" {}", backup);
}
}
_ => {
println!("\nBackups: none");
}
}
let gcs = GcsService::with_bundle_names(
String::new(),
app.registry_url(),
app.release_bundle_name.clone(),
app.checksum_file_name.clone(),
);
match gcs.get_latest_version() {
Ok(latest_version) => {
let update_marker = if latest_version != current_version {
" (update available)"
} else {
" (up to date)"
};
println!("\nRemote:");
println!(" {}{}", latest_version, update_marker);
}
Err(e) => {
println!("\nRemote: unavailable ({})", e);
}
}
Ok(())
}
fn handle_version() -> Result<()> {
let current_version = Config::format_version(&Config::get_current_version());
println!("Current version: {}", current_version);
Ok(())
}
fn handle_uninstall(app: &AppConfig) -> Result<()> {
info!("Starting uninstallation process...");
let app_binary_path = app.app_binary_path();
let ota_coordinator = OtaCoordinator::new(app_binary_path, app.service_name.clone());
ota_coordinator.uninstall()?;
info!("Uninstallation completed successfully!");
Ok(())
}
fn handle_watch(interval_secs: u64, app: &AppConfig) -> Result<()> {
info!(
"Starting watchdog daemon (poll interval: {}s)",
interval_secs
);
println!(
"Watching for updates every {}s. Press Ctrl+C to stop.",
interval_secs
);
let interval = std::time::Duration::from_secs(interval_secs);
loop {
let current_version = Config::format_version(&Config::get_current_version());
let gcs = GcsService::with_bundle_names(
String::new(),
app.registry_url(),
app.release_bundle_name.clone(),
app.checksum_file_name.clone(),
);
match gcs.get_latest_version() {
Ok(latest_version) => {
if latest_version != current_version {
info!(
"New version available: {} -> {}",
current_version, latest_version
);
println!(
"[watch] Update available: {} -> {}. Applying...",
current_version, latest_version
);
let update_service = UpdateService::with_config(app);
match update_service.update(Some("latest".to_string())) {
Ok(()) => {
info!("Auto-update to {} succeeded", latest_version);
println!("[watch] Updated to {}", latest_version);
}
Err(e) => {
warn!("Auto-update failed: {}", e);
println!("[watch] Update failed: {}. Will retry next cycle.", e);
}
}
} else {
info!("No update available (current: {})", current_version);
}
}
Err(e) => {
warn!("Failed to check for updates: {}", e);
}
}
if let Some(ref url) = app.health_check_url {
let total_timeout =
std::time::Duration::from_secs(app.health_check_timeout_secs as u64);
let per_retry_delay = total_timeout / app.health_check_retries.max(1);
let health = crate::services::HealthCheckService::new(
url.clone(),
app.health_check_retries,
per_retry_delay,
);
if let Err(e) = health.check_health() {
warn!("Health check failed: {}", e);
println!("[watch] Health check FAILED: {}", e);
}
}
std::thread::sleep(interval);
}
}