use crate::logging::log_info;
use crate::strategies::ProjectDetector;
use crate::utils::{collect_listening_port_ownership, command_exists, ListeningPortOwnership};
use anyhow::Result;
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use netstat2::{get_sockets_info, AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;
use sysinfo::{Disks, Networks, System};
use tokio::process::Command;
#[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,
pub pids: Vec<u32>,
pub xbp_projects: Vec<String>,
}
#[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 OsInfo {
pub name: Option<String>,
pub version: Option<String>,
pub kernel_version: Option<String>,
pub arch: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CpuInfo {
pub brand: Option<String>,
pub cores: usize,
pub usage: f32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DiskInfo {
pub mount: String,
pub fs: Option<String>,
pub total: u64,
pub used: u64,
pub percent: f32,
pub bar: String,
pub is_current: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ShellInfo {
pub shell: Option<String>,
pub term: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ProxyDetection {
pub nginx_sites_available: Option<usize>,
pub nginx_active: Option<bool>,
pub traefik_active: Option<bool>,
pub apache_active: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ExposureInfo {
pub public_ipv4: Option<String>,
pub listening_on_all_interfaces: Vec<String>,
pub firewall_hint: Option<String>,
pub summary: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ToolVersion {
pub present: bool,
pub version: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DiagnosticReport {
pub system_metrics: SystemMetrics,
pub os: Option<OsInfo>,
pub cpu: Option<CpuInfo>,
pub gpu_candidates: Vec<String>,
pub disks: Vec<DiskInfo>,
pub shell: Option<ShellInfo>,
pub proxy_detection: Option<ProxyDetection>,
pub clipboard_tools: HashMap<String, bool>,
pub exposure: Option<ExposureInfo>,
pub pm2_process_count: Option<usize>,
pub tool_versions: HashMap<String, ToolVersion>,
pub service_statuses: HashMap<String, String>,
pub feature_flags: HashMap<String, bool>,
pub xbp_cli_version: String,
pub provider_manifests: Vec<String>,
pub nginx_status: Option<NginxStatus>,
pub port_checks: Vec<PortCheck>,
pub internet_speed: Option<InternetSpeed>,
pub connectivity: bool,
pub installed_programs: HashMap<String, 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: bool = check_systemctl_status("nginx").await?;
let is_enabled: bool = check_systemctl_enabled("nginx").await?;
let config_valid: bool = 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> {
if !cfg!(target_os = "linux") || !command_exists("systemctl") {
return Ok(false);
}
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> {
if !cfg!(target_os = "linux") || !command_exists("systemctl") {
return Ok(false);
}
let output = Command::new("systemctl")
.arg("is-enabled")
.arg(service)
.output()
.await?;
Ok(output.status.success())
}
pub async fn check_port_availability(
port: u16,
ownership: Option<&ListeningPortOwnership>,
) -> Result<PortCheck> {
use std::net::TcpListener;
let is_open: bool = TcpListener::bind(format!("127.0.0.1:{}", port)).is_ok();
let is_blocked: bool = if cfg!(target_os = "linux") {
check_firewall_blocked(port).await.unwrap_or(false)
} else {
false
};
Ok(PortCheck {
port,
is_open,
is_blocked,
pids: ownership
.map(|value| value.pids.clone())
.unwrap_or_default(),
xbp_projects: ownership
.map(|value| value.xbp_projects.clone())
.unwrap_or_default(),
})
}
async fn check_firewall_blocked(port: u16) -> Result<bool> {
let output: std::process::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: std::process::Output = if cfg!(target_os = "windows") {
Command::new("ping")
.arg("-n")
.arg("1")
.arg("-w")
.arg("2000")
.arg("8.8.8.8")
.output()
.await?
} else {
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> {
if let Ok(speed) = measure_speed_with_speedtest_cli().await {
return Ok(speed);
}
let ping: f64 = 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_speed_with_speedtest_cli() -> Result<InternetSpeed> {
let output: std::process::Output = Command::new("speedtest-cli")
.arg("--simple")
.output()
.await?;
if !output.status.success() {
return Err(anyhow::anyhow!("speedtest-cli failed"));
}
let stdout: std::borrow::Cow<'_, str> = String::from_utf8_lossy(&output.stdout);
let mut ping_ms: f64 = 0.0;
let mut download_mbps: f64 = 0.0;
let mut upload_mbps: f64 = 0.0;
for line in stdout.lines() {
if line.starts_with("Ping:") {
if let Some(val) = line.split_whitespace().nth(1) {
ping_ms = val.parse().unwrap_or(0.0);
}
} else if line.starts_with("Download:") {
if let Some(val) = line.split_whitespace().nth(1) {
download_mbps = val.parse().unwrap_or(0.0);
}
} else if line.starts_with("Upload:") {
if let Some(val) = line.split_whitespace().nth(1) {
upload_mbps = val.parse().unwrap_or(0.0);
}
}
}
Ok(InternetSpeed {
download_mbps,
upload_mbps,
ping_ms,
})
}
async fn measure_ping() -> Result<f64> {
let output: std::process::Output = if cfg!(target_os = "windows") {
Command::new("ping")
.arg("-n")
.arg("4")
.arg("8.8.8.8")
.output()
.await?
} else {
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);
if cfg!(target_os = "windows") {
for line in stdout.lines() {
if line.contains("Average") {
if let Some(avg_part) = line.split('=').last() {
let avg_str = avg_part.trim().trim_end_matches("ms");
if let Ok(avg) = avg_str.parse::<f64>() {
return Ok(avg);
}
}
}
}
} else {
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 = Instant::now();
let client: reqwest::Client = reqwest::Client::new();
let response: reqwest::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 check_installed_programs() -> HashMap<String, bool> {
let programs = vec![
"jq",
"curl",
"nginx",
"git",
"docker",
"node",
"npm",
"python",
"python3",
"pip",
"pip3",
"cargo",
"rustc",
"pm2",
"speedtest-cli",
"systemctl",
"launchctl",
];
let mut results = HashMap::new();
for program in programs {
let is_installed = check_program_installed(program).await;
results.insert(program.to_string(), is_installed);
}
results
}
async fn check_program_installed(program: &str) -> bool {
let cmd = if cfg!(target_os = "windows") {
Command::new("where").arg(program).output().await
} else {
Command::new("which").arg(program).output().await
};
match cmd {
Ok(output) => output.status.success(),
Err(_) => false,
}
}
fn render_percent_bar(percent: f32, width: usize) -> String {
let pct = percent.clamp(0.0, 100.0);
let filled = ((pct / 100.0) * width as f32).round() as usize;
let filled = filled.min(width);
let empty = width.saturating_sub(filled);
format!("{}{}", "█".repeat(filled), "░".repeat(empty))
}
async fn get_os_info() -> Option<OsInfo> {
Some(OsInfo {
name: System::name(),
version: System::os_version(),
kernel_version: System::kernel_version(),
arch: Some(std::env::consts::ARCH.to_string()),
})
}
async fn get_cpu_info() -> Option<CpuInfo> {
let mut sys = System::new_all();
sys.refresh_cpu_all();
let cores = sys.cpus().len();
let usage = if cores > 0 {
sys.cpus().iter().map(|c| c.cpu_usage()).sum::<f32>() / cores as f32
} else {
0.0
};
let brand = sys.cpus().first().map(|c| c.brand().to_string());
Some(CpuInfo {
brand,
cores,
usage,
})
}
async fn run_command_capture(program: &str, args: &[&str]) -> Option<String> {
let output: std::process::Output = Command::new(program).args(args).output().await.ok()?;
let stdout: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
let stderr: String = String::from_utf8_lossy(&output.stderr).trim().to_string();
let combined: String = if !stdout.is_empty() { stdout } else { stderr };
if combined.is_empty() {
None
} else {
Some(combined)
}
}
async fn get_gpu_candidates() -> Vec<String> {
let mut out: Vec<String> = Vec::new();
if cfg!(target_os = "windows") {
if let Some(s) = run_command_capture(
"powershell",
&[
"-Command",
"Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name",
],
)
.await
{
for line in s.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
out.push(line.to_string());
}
}
return out;
}
if let Some(s) = run_command_capture("nvidia-smi", &["-L"]).await {
for line in s.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
out.push(line.to_string());
}
}
if out.is_empty() {
if let Some(s) =
run_command_capture("sh", &["-c", "lspci | grep -Ei 'vga|3d|display'"]).await
{
for line in s.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
out.push(line.to_string());
}
}
}
if out.is_empty() {
if let Some(s) =
run_command_capture("sh", &["-c", "lshw -C display 2>/dev/null | head"]).await
{
for line in s.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
out.push(line.to_string());
}
}
}
out
}
async fn get_disk_infos() -> Vec<DiskInfo> {
let disks = Disks::new_with_refreshed_list();
let cwd = env::current_dir().ok();
let mut candidates: Vec<(usize, usize)> = Vec::new();
if let Some(cwd) = &cwd {
for (i, d) in disks.iter().enumerate() {
let mp = d.mount_point();
if cwd.starts_with(mp) {
candidates.push((mp.as_os_str().len(), i));
}
}
}
candidates.sort_by(|a, b| b.0.cmp(&a.0));
let current_idx = candidates.first().map(|(_, idx)| *idx);
disks
.iter()
.enumerate()
.map(|(i, d)| {
let total = d.total_space();
let used = total.saturating_sub(d.available_space());
let percent = if total > 0 {
(used as f32 / total as f32) * 100.0
} else {
0.0
};
let mount = d.mount_point().to_string_lossy().to_string();
let fs = if d.file_system().is_empty() {
None
} else {
Some(d.file_system().to_string_lossy().to_string())
};
DiskInfo {
mount,
fs,
total,
used,
percent,
bar: render_percent_bar(percent, 20),
is_current: current_idx == Some(i),
}
})
.collect()
}
fn get_shell_info() -> Option<ShellInfo> {
let shell = env::var("SHELL")
.ok()
.or_else(|| env::var("ComSpec").ok())
.or_else(|| env::var("0").ok());
let term = env::var("TERM").ok();
if shell.is_none() && term.is_none() {
None
} else {
Some(ShellInfo { shell, term })
}
}
async fn systemctl_active(service: &str) -> Option<bool> {
if !cfg!(target_os = "linux") || !command_exists("systemctl") {
return None;
}
let output = Command::new("systemctl")
.arg("is-active")
.arg(service)
.output()
.await
.ok()?;
Some(output.status.success())
}
async fn get_service_statuses() -> HashMap<String, String> {
let mut out = HashMap::new();
let services = [
"nginx",
"traefik",
"apache2",
"httpd",
"postgresql",
"postgrest",
"prometheus",
"grafana-server",
"kafka",
"xbp",
];
for s in services {
let v = systemctl_active(s).await;
let status = match v {
Some(true) => "active",
Some(false) => "inactive",
None => "unknown",
};
out.insert(s.to_string(), status.to_string());
}
out
}
async fn get_proxy_detection() -> Option<ProxyDetection> {
let nginx_sites_available = if cfg!(target_os = "linux") {
let p = PathBuf::from("/etc/nginx/sites-available");
if p.exists() {
fs::read_dir(&p)
.ok()
.map(|rd| rd.filter_map(|e| e.ok()).count())
} else {
None
}
} else {
None
};
let nginx_active = systemctl_active("nginx").await;
let traefik_active = systemctl_active("traefik").await;
let apache_active = match systemctl_active("apache2").await {
Some(v) => Some(v),
None => systemctl_active("httpd").await,
};
if nginx_sites_available.is_none()
&& nginx_active.is_none()
&& traefik_active.is_none()
&& apache_active.is_none()
{
None
} else {
Some(ProxyDetection {
nginx_sites_available,
nginx_active,
traefik_active,
apache_active,
})
}
}
async fn get_clipboard_tools() -> HashMap<String, bool> {
let mut tools = vec!["xclip", "xsel", "wl-copy", "pbcopy"];
if cfg!(target_os = "windows") {
tools.push("clip");
}
let mut out = HashMap::new();
for t in tools {
out.insert(t.to_string(), check_program_installed(t).await);
}
out
}
fn get_listeners_on_all_interfaces() -> Vec<String> {
let af_flags = AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6;
let proto_flags = ProtocolFlags::TCP;
let sockets = get_sockets_info(af_flags, proto_flags).unwrap_or_default();
let mut out = Vec::new();
for socket in sockets {
if let ProtocolSocketInfo::Tcp(tcp) = socket.protocol_socket_info {
let state = format!("{:?}", tcp.state);
if state != "Listen" && state != "LISTEN" {
continue;
}
let addr = tcp.local_addr.to_string();
if addr == "0.0.0.0" || addr == "::" {
out.push(format!("{}:{}", addr, tcp.local_port));
}
}
}
out.sort();
out.dedup();
out
}
async fn get_firewall_hint() -> Option<String> {
if !cfg!(target_os = "linux") {
return None;
}
if let Some(s) = run_command_capture("sh", &["-c", "ufw status 2>/dev/null | head -n 1"]).await
{
if !s.trim().is_empty() {
return Some(s.trim().to_string());
}
}
if let Some(s) =
run_command_capture("sh", &["-c", "nft list ruleset 2>/dev/null | head -n 1"]).await
{
if !s.trim().is_empty() {
return Some("nftables detected".to_string());
}
}
if let Some(s) = run_command_capture("sh", &["-c", "iptables -S 2>/dev/null | head -n 1"]).await
{
if !s.trim().is_empty() {
return Some("iptables detected".to_string());
}
}
None
}
async fn get_public_ipv4(connectivity: bool) -> Option<String> {
if !connectivity {
return None;
}
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(3))
.build()
.ok()?;
let text = client
.get("https://api.ipify.org")
.send()
.await
.ok()?
.text()
.await
.ok()?;
let ip = text.trim().to_string();
if ip.is_empty() {
None
} else {
Some(ip)
}
}
async fn get_exposure_info(connectivity: bool) -> Option<ExposureInfo> {
let public_ipv4 = get_public_ipv4(connectivity).await;
let listening_on_all_interfaces = get_listeners_on_all_interfaces();
let firewall_hint = get_firewall_hint().await;
let summary = Some(if listening_on_all_interfaces.is_empty() {
"No listeners on 0.0.0.0/:: detected".to_string()
} else {
"Listeners on 0.0.0.0/:: detected (may be publicly reachable)".to_string()
});
Some(ExposureInfo {
public_ipv4,
listening_on_all_interfaces,
firewall_hint,
summary,
})
}
async fn get_pm2_process_count() -> Option<usize> {
let output = if cfg!(target_os = "windows") {
Command::new("powershell")
.arg("-Command")
.arg("pm2 jlist")
.output()
.await
.ok()?
} else {
Command::new("pm2").arg("jlist").output().await.ok()?
};
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let value: serde_json::Value = serde_json::from_str(&stdout).ok()?;
value.as_array().map(|arr| arr.len())
}
async fn get_version(cmd: &str, args: &[&str]) -> ToolVersion {
let output = Command::new(cmd).args(args).output().await;
match output {
Ok(o) => {
let stdout = String::from_utf8_lossy(&o.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&o.stderr).trim().to_string();
let line = stdout
.lines()
.next()
.filter(|l| !l.trim().is_empty())
.map(|l| l.trim().to_string())
.or_else(|| {
stderr
.lines()
.next()
.filter(|l| !l.trim().is_empty())
.map(|l| l.trim().to_string())
});
ToolVersion {
present: o.status.success() || line.is_some(),
version: line,
}
}
Err(_) => ToolVersion {
present: false,
version: None,
},
}
}
async fn get_tool_versions() -> HashMap<String, ToolVersion> {
let mut out = HashMap::new();
out.insert(
"python3".to_string(),
get_version("python3", &["--version"]).await,
);
out.insert(
"python".to_string(),
get_version("python", &["--version"]).await,
);
out.insert(
"node".to_string(),
get_version("node", &["--version"]).await,
);
out.insert("npm".to_string(), get_version("npm", &["--version"]).await);
out.insert(
"pnpm".to_string(),
get_version("pnpm", &["--version"]).await,
);
out.insert("pm2".to_string(), get_version("pm2", &["--version"]).await);
out.insert(
"docker".to_string(),
get_version("docker", &["--version"]).await,
);
out.insert(
"docker-compose".to_string(),
get_version("docker-compose", &["--version"]).await,
);
out.insert(
"rustc".to_string(),
get_version("rustc", &["--version"]).await,
);
out.insert(
"cargo".to_string(),
get_version("cargo", &["--version"]).await,
);
out.insert(
"rustup".to_string(),
get_version("rustup", &["--version"]).await,
);
out
}
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{} {}",
"XBP:".bright_white(),
format!(
"v{} (features: monitoring={}, kafka={})",
report.xbp_cli_version,
report
.feature_flags
.get("monitoring")
.copied()
.unwrap_or(false),
report.feature_flags.get("kafka").copied().unwrap_or(false),
)
.bright_cyan()
);
if let Some(os) = &report.os {
let os_line = [
os.name.clone().unwrap_or_else(|| "Unknown OS".to_string()),
os.version.clone().unwrap_or_default(),
]
.into_iter()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
println!(" {} {}", "OS:".bright_white(), os_line);
if let Some(k) = &os.kernel_version {
println!(" {} {}", "Kernel:".bright_white(), k);
}
if let Some(arch) = &os.arch {
println!(" {} {}", "Arch:".bright_white(), arch);
}
}
if let Some(cpu) = &report.cpu {
println!(
" {} {} ({} cores, {:.1}% avg)",
"CPU:".bright_white(),
cpu.brand.clone().unwrap_or_else(|| "Unknown".into()),
cpu.cores,
cpu.usage
);
}
if let Some(shell) = &report.shell {
if let Some(s) = &shell.shell {
println!(" {} {}", "Shell:".bright_white(), s);
}
if let Some(t) = &shell.term {
println!(" {} {}", "TERM:".bright_white(), t);
}
}
if !report.gpu_candidates.is_empty() {
println!("\n{}", "🖥️ GPU CANDIDATES".bright_yellow().bold());
println!(
"{}",
"─────────────────────────────────────────────────".bright_black()
);
for line in &report.gpu_candidates {
println!(" {}", line);
}
}
if !report.disks.is_empty() {
println!("\n{}", "💾 DISKS".bright_yellow().bold());
println!(
"{}",
"─────────────────────────────────────────────────".bright_black()
);
for d in &report.disks {
let current = if d.is_current { " *" } else { "" };
let fs = d.fs.clone().unwrap_or_else(|| "-".to_string());
println!(
" {}{} {} {:>5.1}% {} ({} / {} GB)",
d.mount,
current,
fs,
d.percent,
d.bar,
d.used / 1024 / 1024 / 1024,
d.total / 1024 / 1024 / 1024
);
}
println!(" {} current mount", "*".bright_cyan());
}
if !report.provider_manifests.is_empty() {
println!("\n{}", "📦 DETECTED MANIFESTS".bright_yellow().bold());
println!(
"{}",
"─────────────────────────────────────────────────".bright_black()
);
for m in &report.provider_manifests {
println!(" - {}", m);
}
}
if let Some(proxy) = &report.proxy_detection {
println!("\n{}", "🧩 PROXY STACK".bright_yellow().bold());
println!(
"{}",
"─────────────────────────────────────────────────".bright_black()
);
if let Some(n) = proxy.nginx_sites_available {
println!(" {} {}", "nginx sites-available:".bright_white(), n);
}
if let Some(v) = proxy.nginx_active {
println!(
" {} {}",
"nginx active:".bright_white(),
if v { "yes" } else { "no" }
);
}
if let Some(v) = proxy.traefik_active {
println!(
" {} {}",
"traefik active:".bright_white(),
if v { "yes" } else { "no" }
);
}
if let Some(v) = proxy.apache_active {
println!(
" {} {}",
"apache active:".bright_white(),
if v { "yes" } else { "no" }
);
}
}
if !report.clipboard_tools.is_empty() {
println!("\n{}", "📋 CLIPBOARD TOOLS".bright_yellow().bold());
println!(
"{}",
"─────────────────────────────────────────────────".bright_black()
);
let mut items: Vec<_> = report.clipboard_tools.iter().collect();
items.sort_by_key(|(k, _)| *k);
for (k, v) in items {
println!(" {:10} {}", k, if *v { "yes" } else { "no" });
}
}
if let Some(exposure) = &report.exposure {
println!(
"\n{}",
"🌍 NETWORK EXPOSURE (HEURISTIC)".bright_yellow().bold()
);
println!(
"{}",
"─────────────────────────────────────────────────".bright_black()
);
if let Some(ip) = &exposure.public_ipv4 {
println!(" {} {}", "Public IPv4:".bright_white(), ip);
}
if let Some(s) = &exposure.summary {
println!(" {} {}", "Summary:".bright_white(), s);
}
if let Some(h) = &exposure.firewall_hint {
println!(" {} {}", "Firewall:".bright_white(), h);
}
if !exposure.listening_on_all_interfaces.is_empty() {
println!(" {}:", "Listeners on 0.0.0.0/::".bright_white());
for l in &exposure.listening_on_all_interfaces {
println!(" - {}", l);
}
}
}
if let Some(count) = report.pm2_process_count {
println!("\n{}", "🧵 PM2".bright_yellow().bold());
println!(
"{}",
"─────────────────────────────────────────────────".bright_black()
);
println!(" {} {}", "Process count:".bright_white(), count);
}
if !report.tool_versions.is_empty() {
println!("\n{}", "🧰 TOOL VERSIONS".bright_yellow().bold());
println!(
"{}",
"─────────────────────────────────────────────────".bright_black()
);
let mut items: Vec<_> = report.tool_versions.iter().collect();
items.sort_by_key(|(k, _)| *k);
for (name, v) in items {
if v.present {
println!(
" {:12} {}",
name,
v.version.clone().unwrap_or_else(|| "present".to_string())
);
} else {
println!(" {:12} {}", name, "not found".dimmed());
}
}
}
if !report.service_statuses.is_empty() {
println!("\n{}", "🧩 SERVICES".bright_yellow().bold());
println!(
"{}",
"─────────────────────────────────────────────────".bright_black()
);
let mut items: Vec<_> = report.service_statuses.iter().collect();
items.sort_by_key(|(k, _)| *k);
for (name, status) in items {
println!(" {:15} {}", name, status);
}
}
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
);
println!("\n{}", "🔧 INSTALLED PROGRAMS".bright_yellow().bold());
println!(
"{}",
"─────────────────────────────────────────────────".bright_black()
);
let mut programs: Vec<_> = report.installed_programs.iter().collect();
programs.sort_by_key(|(name, _)| *name);
for (program, installed) in programs {
let status_icon = if *installed {
"✓".green()
} else {
"✗".red()
};
let status_text = if *installed {
"Installed".green()
} else {
"Not Found".red()
};
println!(" {} {:15} {}", status_icon, program, status_text);
}
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()
);
let (xbp_ports, other_ports): (Vec<_>, Vec<_>) = report
.port_checks
.iter()
.partition(|port_check| !port_check.xbp_projects.is_empty());
for port_check in xbp_ports.iter().chain(other_ports.iter()) {
let status_icon = if port_check.is_open {
"✓".green()
} else {
"✗".red()
};
let blocked_text = if port_check.is_blocked {
" (BLOCKED)".red().to_string()
} else {
String::new()
};
let xbp_suffix = if port_check.xbp_projects.is_empty() {
String::new()
} else {
format!(" [XBP: {}]", port_check.xbp_projects.join(", "))
};
let line = format!(
" {} Port {}: {}{}{}",
status_icon,
port_check.port,
if port_check.is_open {
"Available".green().to_string()
} else {
"In Use".red().to_string()
},
blocked_text,
xbp_suffix
);
if port_check.xbp_projects.is_empty() {
println!("{}", line);
} else {
println!("{}", line.bright_magenta());
}
}
}
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
);
if speed.upload_mbps > 0.0 {
println!(
" {} {:.2} Mbps",
"Upload:".bright_white(),
speed.upload_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 pb: ProgressBar = ProgressBar::new_spinner();
pb.enable_steady_tick(Duration::from_millis(80));
pb.set_style(
ProgressStyle::with_template("{spinner} {msg}")
.unwrap_or_else(|_| ProgressStyle::default_spinner()),
);
pb.set_message("Collecting system metrics...");
let system_metrics = get_system_metrics().await?;
pb.set_message("Collecting OS/CPU/GPU...");
let os = get_os_info().await;
let cpu = get_cpu_info().await;
let gpu_candidates = get_gpu_candidates().await;
pb.set_message("Collecting disks...");
let disks = get_disk_infos().await;
pb.set_message("Collecting shell + clipboard...");
let shell = get_shell_info();
let clipboard_tools = get_clipboard_tools().await;
pb.set_message("Detecting proxies...");
let proxy_detection = get_proxy_detection().await;
pb.set_message("Checking network/public IP...");
let connectivity = check_internet_connectivity().await.unwrap_or(false);
let exposure = get_exposure_info(connectivity).await;
pb.set_message("Checking tools...");
let installed_programs = check_installed_programs().await;
let tool_versions = get_tool_versions().await;
pb.set_message("Checking services...");
let service_statuses = get_service_statuses().await;
pb.set_message("Checking PM2...");
let pm2_process_count = get_pm2_process_count().await;
pb.set_message("Checking ports...");
let port_ownership = collect_listening_port_ownership().unwrap_or_default();
let mut port_checks = Vec::new();
for port in ports {
if let Ok(check) = check_port_availability(port, port_ownership.get(&port)).await {
port_checks.push(check);
}
}
pb.set_message("Measuring internet speed...");
let internet_speed = if connectivity {
measure_internet_speed().await.ok()
} else {
None
};
pb.set_message("Checking nginx...");
let nginx_status = check_nginx_status().await.ok();
let provider_manifests: Vec<String> = ProjectDetector::detect_provider_manifests(
&env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
);
let mut feature_flags: HashMap<String, bool> = HashMap::new();
feature_flags.insert("monitoring".to_string(), cfg!(feature = "monitoring"));
feature_flags.insert("kafka".to_string(), cfg!(feature = "kafka"));
let xbp_cli_version = env!("CARGO_PKG_VERSION").to_string();
pb.finish_and_clear();
Ok(DiagnosticReport {
system_metrics,
os,
cpu,
gpu_candidates,
disks,
shell,
proxy_detection,
clipboard_tools,
exposure,
pm2_process_count,
tool_versions,
service_statuses,
feature_flags,
xbp_cli_version,
provider_manifests,
nginx_status,
port_checks,
internet_speed,
connectivity,
installed_programs,
})
}