xbp 0.9.3

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use anyhow::Result;
use colored::Colorize;
use serde::{Deserialize, Serialize};
use sysinfo::{System, Disks, Networks};
use tokio::process::Command;
use crate::logging::log_info;

#[derive(Debug, Serialize, Deserialize)]
pub struct SystemMetrics {
    pub cpu_usage: f32,
    pub memory_total: u64,
    pub memory_used: u64,
    pub memory_percent: f32,
    pub disk_total: u64,
    pub disk_used: u64,
    pub disk_percent: f32,
    pub network_rx: u64,
    pub network_tx: u64,
    pub uptime: u64,
    pub process_count: usize,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PortCheck {
    pub port: u16,
    pub is_open: bool,
    pub is_blocked: bool,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct InternetSpeed {
    pub download_mbps: f64,
    pub upload_mbps: f64,
    pub ping_ms: f64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct DiagnosticReport {
    pub system_metrics: SystemMetrics,
    pub nginx_status: Option<NginxStatus>,
    pub port_checks: Vec<PortCheck>,
    pub internet_speed: Option<InternetSpeed>,
    pub connectivity: bool,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct NginxStatus {
    pub is_running: bool,
    pub is_enabled: bool,
    pub config_valid: bool,
    pub error: Option<String>,
}

pub async fn get_system_metrics() -> Result<SystemMetrics> {
    let mut sys = System::new_all();
    sys.refresh_all();

    let cpu_usage = sys.cpus().iter().map(|cpu| cpu.cpu_usage()).sum::<f32>() / sys.cpus().len() as f32;
    let memory_total = sys.total_memory();
    let memory_used = sys.used_memory();
    let memory_percent = (memory_used as f32 / memory_total as f32) * 100.0;

    let mut disk_total = 0;
    let mut disk_used = 0;
    for disk in Disks::new_with_refreshed_list().iter() {
        disk_total += disk.total_space();
        disk_used += disk.total_space() - disk.available_space();
    }
    let disk_percent = if disk_total > 0 {
        (disk_used as f32 / disk_total as f32) * 100.0
    } else {
        0.0
    };

    let networks = Networks::new_with_refreshed_list();
    let mut network_rx = 0;
    let mut network_tx = 0;
    for (_interface_name, network) in &networks {
        network_rx += network.total_received();
        network_tx += network.total_transmitted();
    }

    let uptime = System::uptime();
    let process_count = sys.processes().len();

    Ok(SystemMetrics {
        cpu_usage,
        memory_total,
        memory_used,
        memory_percent,
        disk_total,
        disk_used,
        disk_percent,
        network_rx,
        network_tx,
        uptime,
        process_count,
    })
}

pub async fn check_nginx_status() -> Result<NginxStatus> {
    let is_running = check_systemctl_status("nginx").await?;
    let is_enabled = check_systemctl_enabled("nginx").await?;
    
    let config_valid = if is_running {
        match Command::new("nginx").arg("-t").output().await {
            Ok(output) => output.status.success(),
            Err(_) => false,
        }
    } else {
        false
    };

    Ok(NginxStatus {
        is_running,
        is_enabled,
        config_valid,
        error: None,
    })
}

async fn check_systemctl_status(service: &str) -> Result<bool> {
    let output = Command::new("systemctl")
        .arg("is-active")
        .arg(service)
        .output()
        .await?;
    
    Ok(output.status.success())
}

async fn check_systemctl_enabled(service: &str) -> Result<bool> {
    let output = Command::new("systemctl")
        .arg("is-enabled")
        .arg(service)
        .output()
        .await?;
    
    Ok(output.status.success())
}

pub async fn check_port_availability(port: u16) -> Result<PortCheck> {
    use std::net::TcpListener;
    
    let is_open = TcpListener::bind(format!("127.0.0.1:{}", port)).is_ok();
    
    let is_blocked = if cfg!(target_os = "linux") {
        check_firewall_blocked(port).await.unwrap_or(false)
    } else {
        false
    };

    Ok(PortCheck {
        port,
        is_open,
        is_blocked,
    })
}

async fn check_firewall_blocked(port: u16) -> Result<bool> {
    let output = Command::new("iptables")
        .arg("-L")
        .arg("-n")
        .output()
        .await?;
    
    if !output.status.success() {
        return Ok(false);
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let port_str = port.to_string();
    Ok(stdout.contains(&format!("dpt:{}", port_str)) && stdout.contains("DROP"))
}

pub async fn check_internet_connectivity() -> Result<bool> {
    let result = Command::new("ping")
        .arg("-c")
        .arg("1")
        .arg("-W")
        .arg("2")
        .arg("8.8.8.8")
        .output()
        .await?;
    
    Ok(result.status.success())
}

pub async fn measure_internet_speed() -> Result<InternetSpeed> {
    let ping = measure_ping().await?;
    
    let download_mbps = measure_download_speed().await.unwrap_or(0.0);
    let upload_mbps = 0.0;

    Ok(InternetSpeed {
        download_mbps,
        upload_mbps,
        ping_ms: ping,
    })
}

async fn measure_ping() -> Result<f64> {
    let output = Command::new("ping")
        .arg("-c")
        .arg("4")
        .arg("8.8.8.8")
        .output()
        .await?;
    
    if !output.status.success() {
        return Ok(0.0);
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    
    for line in stdout.lines() {
        if line.contains("avg") || line.contains("rtt") {
            if let Some(avg_str) = line.split('/').nth(4) {
                if let Ok(avg) = avg_str.trim().parse::<f64>() {
                    return Ok(avg);
                }
            }
        }
    }
    
    Ok(0.0)
}

async fn measure_download_speed() -> Result<f64> {
    use std::time::Instant;
    
    let start = Instant::now();
    let client = reqwest::Client::new();
    
    let response = client
        .get("http://speedtest.ftp.otenet.gr/files/test1Mb.db")
        .send()
        .await?;
    
    let bytes = response.bytes().await?;
    let duration = start.elapsed().as_secs_f64();
    
    let megabits = (bytes.len() as f64 * 8.0) / 1_000_000.0;
    let mbps = megabits / duration;
    
    Ok(mbps)
}

pub async fn print_diagnostic_report(report: &DiagnosticReport) {
    println!("\n{}", "═══════════════════════════════════════════════════".bright_cyan());
    println!("{}", "           XBP SYSTEM DIAGNOSTICS REPORT".bright_cyan().bold());
    println!("{}", "═══════════════════════════════════════════════════".bright_cyan());
    
    println!("\n{}", "📊 SYSTEM METRICS".bright_yellow().bold());
    println!("{}", "─────────────────────────────────────────────────".bright_black());
    
    let metrics = &report.system_metrics;
    
    let cpu_color = if metrics.cpu_usage > 80.0 { "red" } else if metrics.cpu_usage > 50.0 { "yellow" } else { "green" };
    println!("  {} {:.1}%", "CPU Usage:".bright_white(), format!("{}", metrics.cpu_usage).color(cpu_color));
    
    let mem_color = if metrics.memory_percent > 80.0 { "red" } else if metrics.memory_percent > 50.0 { "yellow" } else { "green" };
    println!("  {} {:.1}% ({} MB / {} MB)", 
        "Memory:".bright_white(),
        format!("{}", metrics.memory_percent).color(mem_color),
        metrics.memory_used / 1024 / 1024,
        metrics.memory_total / 1024 / 1024
    );
    
    let disk_color = if metrics.disk_percent > 80.0 { "red" } else if metrics.disk_percent > 50.0 { "yellow" } else { "green" };
    println!("  {} {:.1}% ({} GB / {} GB)", 
        "Disk:".bright_white(),
        format!("{}", metrics.disk_percent).color(disk_color),
        metrics.disk_used / 1024 / 1024 / 1024,
        metrics.disk_total / 1024 / 1024 / 1024
    );
    
    println!("  {} {} MB ↓ / {} MB ↑", 
        "Network:".bright_white(),
        metrics.network_rx / 1024 / 1024,
        metrics.network_tx / 1024 / 1024
    );
    
    let uptime_hours = metrics.uptime / 3600;
    let uptime_minutes = (metrics.uptime % 3600) / 60;
    println!("  {} {}h {}m", "Uptime:".bright_white(), uptime_hours, uptime_minutes);
    println!("  {} {}", "Processes:".bright_white(), metrics.process_count);
    
    if let Some(nginx) = &report.nginx_status {
        println!("\n{}", "🔧 NGINX STATUS".bright_yellow().bold());
        println!("{}", "─────────────────────────────────────────────────".bright_black());
        
        let status_icon = if nginx.is_running { "".green() } else { "".red() };
        println!("  {} {}", status_icon, if nginx.is_running { "Running".green() } else { "Stopped".red() });
        
        let enabled_icon = if nginx.is_enabled { "".green() } else { "".red() };
        println!("  {} {}", enabled_icon, if nginx.is_enabled { "Enabled".green() } else { "Disabled".red() });
        
        let config_icon = if nginx.config_valid { "".green() } else { "".red() };
        println!("  {} {}", config_icon, if nginx.config_valid { "Config Valid".green() } else { "Config Invalid".red() });
    }
    
    if !report.port_checks.is_empty() {
        println!("\n{}", "🔌 PORT STATUS".bright_yellow().bold());
        println!("{}", "─────────────────────────────────────────────────".bright_black());
        
        for port_check in &report.port_checks {
            let status_icon = if port_check.is_open { "".green() } else { "".red() };
            let blocked_text = if port_check.is_blocked { " (BLOCKED)".red() } else { "".normal() };
            println!("  {} Port {}: {}{}", 
                status_icon, 
                port_check.port, 
                if port_check.is_open { "Available".green() } else { "In Use".red() },
                blocked_text
            );
        }
    }
    
    println!("\n{}", "🌐 CONNECTIVITY".bright_yellow().bold());
    println!("{}", "─────────────────────────────────────────────────".bright_black());
    
    let conn_icon = if report.connectivity { "".green() } else { "".red() };
    println!("  {} {}", conn_icon, if report.connectivity { "Internet Connected".green() } else { "No Internet".red() });
    
    if let Some(speed) = &report.internet_speed {
        println!("  {} {:.2} ms", "Ping:".bright_white(), speed.ping_ms);
        println!("  {} {:.2} Mbps", "Download:".bright_white(), speed.download_mbps);
    }
    
    println!("\n{}", "═══════════════════════════════════════════════════".bright_cyan());
}

pub async fn run_full_diagnostics(ports: Vec<u16>) -> Result<DiagnosticReport> {
    let _ = log_info("diag", "Running system diagnostics...", None).await;
    
    let system_metrics = get_system_metrics().await?;
    let nginx_status = check_nginx_status().await.ok();
    let connectivity = check_internet_connectivity().await.unwrap_or(false);
    
    let mut port_checks = Vec::new();
    for port in ports {
        if let Ok(check) = check_port_availability(port).await {
            port_checks.push(check);
        }
    }
    
    let internet_speed = if connectivity {
        measure_internet_speed().await.ok()
    } else {
        None
    };
    
    Ok(DiagnosticReport {
        system_metrics,
        nginx_status,
        port_checks,
        internet_speed,
        connectivity,
    })
}