use std::path::{Path, PathBuf};
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 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);
runtime_section_from_results(&base, config_error, msb, libkrunfw)
}
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 resolve_msb_runtime_file(config: &LocalConfig) -> Result<PathBuf, String> {
let path = config::resolve_msb_path(config).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(config).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 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.2.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.2.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.2.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"
);
}
}