use std::fs::File;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use microsandbox_utils::copy::{FastCopyStrategy, fast_copy_with_strategy};
use crate::config::{self, LocalConfig};
const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckState {
Pass,
Fail,
Warn,
Info,
}
#[derive(Debug, Clone)]
pub struct Check {
pub label: String,
pub state: CheckState,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct Section {
pub title: String,
pub checks: Vec<Check>,
}
#[derive(Debug, Clone)]
pub struct Problem {
pub headline: String,
pub hints: Vec<String>,
pub fix: Option<Fix>,
}
#[derive(Debug, Clone)]
pub struct Fix {
pub description: String,
pub commands: Vec<FixCommand>,
pub requires_relogin: bool,
}
#[derive(Debug, Clone)]
pub struct FixCommand {
pub program: String,
pub args: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Diagnosis {
pub sections: Vec<Section>,
pub problems: Vec<Problem>,
}
impl Check {
pub(crate) fn pass(label: &str, value: &str) -> Self {
Self {
label: label.to_string(),
state: CheckState::Pass,
value: value.to_string(),
}
}
pub(crate) fn fail(label: &str, value: &str) -> Self {
Self {
label: label.to_string(),
state: CheckState::Fail,
value: value.to_string(),
}
}
pub(crate) fn warn(label: &str, value: &str) -> Self {
Self {
label: label.to_string(),
state: CheckState::Warn,
value: value.to_string(),
}
}
pub(crate) fn info(label: &str, value: &str) -> Self {
Self {
label: label.to_string(),
state: CheckState::Info,
value: value.to_string(),
}
}
}
impl Problem {
pub(crate) fn new(headline: impl Into<String>, hints: Vec<String>) -> Self {
Self {
headline: headline.into(),
hints,
fix: None,
}
}
pub fn with_fix(mut self, fix: Fix) -> Self {
self.fix = Some(fix);
self
}
}
impl Fix {
pub fn new(description: impl Into<String>, commands: Vec<FixCommand>) -> Self {
Self {
description: description.into(),
commands,
requires_relogin: false,
}
}
pub fn requires_relogin(mut self) -> Self {
self.requires_relogin = true;
self
}
}
impl FixCommand {
pub fn sudo(args: &[&str]) -> Self {
Self {
program: "sudo".to_string(),
args: args.iter().map(|a| a.to_string()).collect(),
}
}
pub fn display(&self) -> String {
let mut parts = Vec::with_capacity(self.args.len() + 1);
parts.push(self.program.as_str());
parts.extend(self.args.iter().map(String::as_str));
parts.join(" ")
}
}
impl Diagnosis {
pub fn is_healthy(&self) -> bool {
self.problems.is_empty()
}
}
pub fn diagnose() -> Diagnosis {
let mut sections = Vec::new();
let mut problems = Vec::new();
let (runtime, mut runtime_problems) = runtime_section();
sections.push(runtime);
problems.append(&mut runtime_problems);
let (host, mut host_problems) = host_section();
sections.push(host);
problems.append(&mut host_problems);
Diagnosis { sections, problems }
}
fn runtime_section() -> (Section, Vec<Problem>) {
let (config, config_error) = match config::load_persisted_config_or_default() {
Ok(config) => (config, None),
Err(error) => (LocalConfig::default(), Some(error.to_string())),
};
let base = config.home();
let msb = resolve_msb_runtime_file(&config);
let libkrunfw = resolve_libkrunfw_runtime_file(&config);
let (mut section, problems) = runtime_section_from_results(&base, config_error, msb, libkrunfw);
if base.is_dir() {
section.checks.push(root_clone_check(&base));
}
(section, problems)
}
fn runtime_section_from_results(
base: &Path,
config_error: Option<String>,
msb: Result<PathBuf, String>,
libkrunfw: Result<PathBuf, String>,
) -> (Section, Vec<Problem>) {
let mut checks = vec![
Check::info("Version", &format!("v{PACKAGE_VERSION}")),
Check::info("MSB_HOME", &base.display().to_string()),
];
if config_error.is_some() {
checks.push(Check::fail("config", "invalid"));
}
checks.extend([
runtime_file_check("msb", &msb),
runtime_file_check("libkrunfw", &libkrunfw),
]);
let mut problems = Vec::new();
if let Some(error) = config_error {
problems.push(Problem::new(
"microsandbox config could not be read",
vec![
error,
"fix the config file or set MSB_CONFIG_PATH to a valid config".to_string(),
],
));
}
if msb.is_err() || libkrunfw.is_err() {
let mut hints = Vec::new();
if let Err(error) = &msb {
hints.push(format!("msb: {error}"));
}
if let Err(error) = &libkrunfw {
hints.push(format!("libkrunfw: {error}"));
}
hints.push("libkrunfw may live beside the resolved msb binary or under ../lib".to_string());
hints.push("standalone install: repair with msb self update".to_string());
hints.push(
"package-manager install: reinstall or repair the microsandbox package".to_string(),
);
problems.push(Problem::new(
"microsandbox runtime could not be resolved",
hints,
));
}
(
Section {
title: "Runtime".to_string(),
checks,
},
problems,
)
}
fn runtime_file_check(label: &str, result: &Result<PathBuf, String>) -> Check {
match result {
Ok(path) => Check::pass(label, &path.display().to_string()),
Err(_) => Check::fail(label, "not found"),
}
}
fn root_clone_check(base: &Path) -> Check {
match probe_root_clone(base) {
Ok(FastCopyStrategy::Reflink) => Check::pass("Root clone", "reflink supported"),
Ok(FastCopyStrategy::SparseCopy) => {
Check::warn("Root clone", "copy fallback — reflink unavailable")
}
Err(error) => Check::warn(
"Root clone",
&format!("not checked — {}", concise_io_error(&error)),
),
}
}
fn probe_root_clone(base: &Path) -> io::Result<FastCopyStrategy> {
const PROBE_LEN: usize = 64 * 1024;
let probe = tempfile::Builder::new()
.prefix(".msb-doctor-clone-")
.tempdir_in(base)?;
let source = probe.path().join("source");
let destination = probe.path().join("destination");
let mut source_file = File::create(&source)?;
source_file.write_all(&vec![0xa5; PROBE_LEN])?;
source_file.sync_all()?;
drop(source_file);
let (_, strategy) = fast_copy_with_strategy(&source, &destination)?;
probe.close()?;
Ok(strategy)
}
fn concise_io_error(error: &io::Error) -> &'static str {
match error.kind() {
io::ErrorKind::NotFound => "MSB_HOME is unavailable",
io::ErrorKind::PermissionDenied => "permission denied",
io::ErrorKind::StorageFull => "storage is full",
_ => "probe failed",
}
}
fn resolve_msb_runtime_file(config: &LocalConfig) -> Result<PathBuf, String> {
let path = config
.resolve_msb_path()
.map_err(|error| error.to_string())?;
if path.is_file() {
Ok(path)
} else {
Err(format!("resolved path is not a file: {}", path.display()))
}
}
fn resolve_libkrunfw_runtime_file(config: &LocalConfig) -> Result<PathBuf, String> {
config
.resolve_libkrunfw_path()
.map_err(|error| error.to_string())
}
fn host_section() -> (Section, Vec<Problem>) {
#[cfg(target_os = "linux")]
{
super::linux::host_section()
}
#[cfg(target_os = "macos")]
{
super::macos::host_section()
}
#[cfg(target_os = "windows")]
{
super::windows::host_section()
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
unsupported_host_section()
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn unsupported_host_section() -> (Section, Vec<Problem>) {
let label = format!("{} {}", std::env::consts::OS, std::env::consts::ARCH);
(
Section {
title: "Host".to_string(),
checks: vec![Check::fail("Platform", &label)],
},
vec![Problem::new(
"this platform is not supported for local sandboxes",
vec!["local execution is supported on Linux, macOS (arm64), and Windows".to_string()],
)],
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn root_clone_probe_cleans_up_its_temporary_files() {
let base = tempfile::tempdir().unwrap();
let strategy = probe_root_clone(base.path()).unwrap();
assert!(matches!(
strategy,
FastCopyStrategy::Reflink | FastCopyStrategy::SparseCopy
));
assert_eq!(std::fs::read_dir(base.path()).unwrap().count(), 0);
}
#[test]
fn runtime_section_accepts_resolved_side_by_side_runtime() {
let dir = PathBuf::from("C:/Tools/microsandbox");
let msb = dir.join(microsandbox_utils::msb_binary_filename("windows"));
let libkrunfw = dir.join(microsandbox_utils::libkrunfw_filename("windows"));
let (section, problems) = runtime_section_from_results(
Path::new("C:/Users/me/.microsandbox"),
None,
Ok(msb.clone()),
Ok(libkrunfw.clone()),
);
assert!(problems.is_empty());
assert_eq!(section.checks[0].label, "Version");
assert_eq!(section.checks[0].value, format!("v{PACKAGE_VERSION}"));
assert_eq!(section.checks[1].label, "MSB_HOME");
assert_eq!(section.checks[2].state, CheckState::Pass);
assert_eq!(section.checks[2].value, msb.display().to_string());
assert_eq!(section.checks[3].state, CheckState::Pass);
assert_eq!(section.checks[3].value, libkrunfw.display().to_string());
}
#[test]
fn runtime_section_reports_resolution_errors() {
let (section, problems) = runtime_section_from_results(
Path::new("/home/me/.microsandbox"),
None,
Err("resolved path is not a file: /tmp/msb".to_string()),
Err("searched: /tmp/libkrunfw.so.5.6.1".to_string()),
);
assert_eq!(section.checks[2].state, CheckState::Fail);
assert_eq!(section.checks[3].state, CheckState::Fail);
assert_eq!(problems.len(), 1);
assert!(problems[0].hints[0].contains("/tmp/msb"));
assert!(problems[0].hints[1].contains("/tmp/libkrunfw.so.5.6.1"));
}
#[test]
fn runtime_section_reports_config_errors() {
let (section, problems) = runtime_section_from_results(
Path::new("/home/me/.microsandbox"),
Some("failed to parse config `/home/me/.microsandbox/config.json`".to_string()),
Ok(PathBuf::from("/usr/bin/msb")),
Ok(PathBuf::from("/usr/lib/libkrunfw.so.5.6.1")),
);
assert_eq!(section.checks[2].label, "config");
assert_eq!(section.checks[2].state, CheckState::Fail);
assert_eq!(problems.len(), 1);
assert_eq!(
problems[0].headline,
"microsandbox config could not be read"
);
}
}