use crate::config::Config;
use crate::services::ota::BackupManager;
use serde::Serialize;
use std::path::Path;
use std::process::Command;
use tracing::{debug, warn};
#[derive(Debug, Clone, Serialize)]
pub struct SystemInfo {
pub binary_version: String,
pub git_hash: String,
pub build_date: String,
pub target_platform: String,
pub deployment_mode: String,
pub deployment_size_mb: u64,
pub binary_location: String,
pub supervisor_version: String,
pub required_dependencies: Vec<String>,
pub ota_metadata: OtaMetadata,
}
#[derive(Debug, Clone, Serialize)]
pub struct OtaMetadata {
pub min_supervisor_version: String,
pub service_name: String,
pub assets_included: bool,
pub health_check_endpoint: String,
pub health_check_timeout: u32,
pub rollback_compatible_versions: Vec<String>,
}
pub struct VersionService {
service_name: String,
}
impl VersionService {
pub fn new() -> Self {
Self {
service_name: "geist_supervisor".to_string(),
}
}
pub fn with_service_name(service_name: String) -> Self {
Self { service_name }
}
pub fn get_system_info(&self, binary_path: &Path) -> SystemInfo {
let current_version = Config::format_version(&Config::get_current_version());
let supervisor_version = format!("v{}", Config::PKG_VERSION);
let git_hash = self.get_git_hash().unwrap_or_else(|| "unknown".to_string());
let build_date = self
.get_build_date()
.unwrap_or_else(|| "unknown".to_string());
let deployment_mode = self.detect_deployment_mode();
let deployment_size_mb = self.get_binary_size_mb(binary_path);
let ota_metadata = self.get_ota_metadata(¤t_version);
SystemInfo {
binary_version: current_version,
git_hash,
build_date,
target_platform: std::env::consts::OS.to_string(),
deployment_mode,
deployment_size_mb,
binary_location: binary_path.to_string_lossy().to_string(),
supervisor_version,
required_dependencies: vec![],
ota_metadata,
}
}
pub fn get_git_hash(&self) -> Option<String> {
let hash = env!("GIT_HASH");
if hash == "unknown" {
None
} else {
Some(hash.to_string())
}
}
pub fn get_build_date(&self) -> Option<String> {
let date = env!("BUILD_DATE");
if date == "unknown" {
None
} else {
Some(date.to_string())
}
}
pub fn detect_deployment_mode(&self) -> String {
if std::env::var("GEIST_PRODUCTION").is_ok() {
debug!("Production mode detected from GEIST_PRODUCTION env var");
return "production".to_string();
}
if std::env::var("INVOCATION_ID").is_ok() || std::env::var("SERVICE_RESULT").is_ok() {
debug!("Production mode detected from systemd environment");
return "production".to_string();
}
let prod_path = format!("/opt/{}", self.service_name);
if Path::new(&prod_path).exists() {
debug!("Production mode detected from {} path", prod_path);
return "production".to_string();
}
debug!("Development mode detected");
"development".to_string()
}
pub fn get_binary_size_mb(&self, binary_path: &Path) -> u64 {
match std::fs::metadata(binary_path) {
Ok(metadata) => {
let size_mb = metadata.len() / (1024 * 1024);
debug!("Binary size: {} MB", size_mb);
size_mb
}
Err(e) => {
warn!(
"Failed to get binary metadata for {}: {}",
binary_path.display(),
e
);
0
}
}
}
pub fn get_ota_metadata(&self, current_version: &str) -> OtaMetadata {
OtaMetadata {
min_supervisor_version: "v0.1.0".to_string(),
service_name: self.service_name.clone(),
assets_included: true,
health_check_endpoint: "/health".to_string(),
health_check_timeout: 30,
rollback_compatible_versions: self.get_rollback_compatible_versions(current_version),
}
}
pub fn get_rollback_compatible_versions(&self, current_version: &str) -> Vec<String> {
let mut versions = vec![current_version.to_string()];
if let Ok(backup_manager) = BackupManager::new() {
if let Ok(backups) = backup_manager.list_backups() {
for backup_name in &backups {
versions.push(backup_name.clone());
}
}
}
versions
}
pub fn is_git_available(&self) -> bool {
Command::new("git")
.args(["--version"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
pub fn get_current_version(&self) -> String {
Config::format_version(&Config::get_current_version())
}
pub fn get_supervisor_version(&self) -> String {
format!("v{}", Config::PKG_VERSION)
}
}
impl Default for VersionService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn test_version_service_creation() {
let service = VersionService::new();
assert_eq!(
service.get_supervisor_version(),
format!("v{}", Config::PKG_VERSION)
);
}
#[test]
fn test_version_service_with_service_name() {
let service = VersionService::with_service_name("orb".to_string());
let metadata = service.get_ota_metadata("v1.0.0");
assert_eq!(metadata.service_name, "orb");
}
#[test]
fn test_get_current_version() {
let service = VersionService::new();
let version = service.get_current_version();
assert!(version.starts_with('v') || version == "unknown");
}
#[test]
fn test_detect_deployment_mode_development() {
let service = VersionService::new();
let mode = service.detect_deployment_mode();
assert!(mode == "development" || mode == "production");
}
#[test]
fn test_get_binary_size_mb_nonexistent() {
let service = VersionService::new();
let nonexistent_path = PathBuf::from("/nonexistent/path");
let size = service.get_binary_size_mb(&nonexistent_path);
assert_eq!(size, 0);
}
#[test]
fn test_get_binary_size_mb_existing() {
let service = VersionService::new();
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_binary");
std::fs::write(&test_file, "test content").unwrap();
let size = service.get_binary_size_mb(&test_file);
assert_eq!(size, 0);
}
#[test]
fn test_get_ota_metadata() {
let service = VersionService::with_service_name("my_app".to_string());
let metadata = service.get_ota_metadata("v1.0.0");
assert_eq!(metadata.service_name, "my_app");
assert_eq!(metadata.health_check_endpoint, "/health");
assert_eq!(metadata.health_check_timeout, 30);
assert!(metadata.assets_included);
}
#[test]
fn test_get_rollback_compatible_versions() {
let service = VersionService::new();
let versions = service.get_rollback_compatible_versions("v1.0.0");
assert!(!versions.is_empty());
assert_eq!(versions[0], "v1.0.0");
}
#[test]
fn test_get_system_info() {
let service = VersionService::new();
let temp_dir = TempDir::new().unwrap();
let test_binary = temp_dir.path().join("test_binary");
std::fs::write(&test_binary, "test").unwrap();
let info = service.get_system_info(&test_binary);
assert!(!info.binary_version.is_empty());
assert!(!info.supervisor_version.is_empty());
assert_eq!(info.target_platform, std::env::consts::OS);
assert!(info.deployment_mode == "development" || info.deployment_mode == "production");
assert_eq!(info.binary_location, test_binary.to_string_lossy());
assert!(info.required_dependencies.is_empty());
}
#[test]
fn test_git_hash_with_no_git() {
let service = VersionService::new();
let hash = service.get_git_hash();
if let Some(h) = hash {
assert!(!h.is_empty());
}
}
#[test]
fn test_build_date_with_no_env() {
let service = VersionService::new();
let date = service.get_build_date();
if let Some(d) = date {
assert!(!d.is_empty());
}
}
#[test]
fn test_is_git_available() {
let service = VersionService::new();
let _ = service.is_git_available();
}
}