use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
pub trait DoctorCheck: Send {
fn name(&self) -> &str;
fn category(&self) -> &str;
fn run(&self) -> CheckResult;
}
#[derive(Clone, Debug)]
pub struct CheckResult {
pub status: CheckStatus,
pub message: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CheckStatus {
Ok,
Warn,
Error,
}
impl CheckStatus {
pub const fn is_ok(self) -> bool {
matches!(self, Self::Ok)
}
}
#[derive(Clone, Debug)]
pub struct NamedResult {
pub name: String,
pub category: String,
pub result: CheckResult,
}
#[derive(Clone, Debug, Default)]
pub struct DoctorSummary {
pub passed: usize,
pub warned: usize,
pub failed: usize,
}
pub struct DoctorRunner {
checks: Vec<Box<dyn DoctorCheck>>,
}
impl DoctorRunner {
pub fn new() -> Self {
Self { checks: Vec::new() }
}
pub fn add(&mut self, check: Box<dyn DoctorCheck>) {
self.checks.push(check);
}
pub fn check_count(&self) -> usize {
self.checks.len()
}
pub fn run_all(&self) -> Vec<NamedResult> {
self.checks
.iter()
.map(|check| {
let name = check.name().to_string();
let category = check.category().to_string();
tracing::debug!(check = %name, category = %category, "running doctor check");
let result = check.run();
tracing::debug!(check = %name, status = ?result.status, "check complete");
NamedResult {
name,
category,
result,
}
})
.collect()
}
pub fn summarize(results: &[NamedResult]) -> DoctorSummary {
let mut summary = DoctorSummary::default();
for r in results {
match r.result.status {
CheckStatus::Ok => summary.passed += 1,
CheckStatus::Warn => summary.warned += 1,
CheckStatus::Error => summary.failed += 1,
}
}
summary
}
pub fn format_report(results: &[NamedResult]) -> String {
let mut buf = String::new();
let mut current_category = "";
for r in results {
if r.category != current_category {
if !buf.is_empty() {
buf.push('\n');
}
buf.push_str(&r.category);
buf.push_str(":\n");
current_category = &r.category;
}
let icon = match r.result.status {
CheckStatus::Ok => "OK",
CheckStatus::Warn => "WARN",
CheckStatus::Error => "FAIL",
};
buf.push_str(&format!(" [{icon}] {}: {}\n", r.name, r.result.message));
}
let summary = Self::summarize(results);
buf.push_str(&format!(
"\n{} passed, {} warnings, {} failed\n",
summary.passed, summary.warned, summary.failed
));
buf
}
}
impl Default for DoctorRunner {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct DebugBundle {
app_name: String,
dir: PathBuf,
files: Vec<(String, Vec<u8>)>,
}
impl DebugBundle {
pub fn new(app_name: &str, dir: &Path) -> Self {
Self {
app_name: app_name.to_string(),
dir: dir.to_path_buf(),
files: Vec::new(),
}
}
pub fn add_text(&mut self, name: &str, content: &str) -> &mut Self {
self.files
.push((name.to_string(), content.as_bytes().to_vec()));
self
}
pub fn add_bytes(&mut self, name: &str, data: &[u8]) -> &mut Self {
self.files.push((name.to_string(), data.to_vec()));
self
}
pub fn add_doctor_results(&mut self, results: &[NamedResult]) -> &mut Self {
let report = DoctorRunner::format_report(results);
self.add_text("doctor-report.txt", &report)
}
pub fn finish(self) -> Result<PathBuf> {
std::fs::create_dir_all(&self.dir).map_err(Error::Diagnostic)?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let filename = format!("{}-debug-{timestamp}.tar.gz", self.app_name);
let path = self.dir.join(&filename);
let file = std::fs::File::create(&path).map_err(Error::Diagnostic)?;
let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut archive = tar::Builder::new(encoder);
for (name, data) in &self.files {
let mut header = tar::Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o644);
header.set_cksum();
archive
.append_data(&mut header, name, data.as_slice())
.map_err(Error::Diagnostic)?;
}
archive
.into_inner()
.map_err(Error::Diagnostic)?
.finish()
.map_err(Error::Diagnostic)?;
tracing::info!(path = %path.display(), "debug bundle created");
Ok(path)
}
}