use anyhow::Result;
pub async fn execute() -> Result<()> {
println!("ArcBox Doctor\n");
let mut passed = 0u32;
let mut warned = 0u32;
let mut failed = 0u32;
macro_rules! check {
($name:expr, $result:expr) => {
match $result {
CheckResult::Pass(msg) => {
println!(" [PASS] {}: {}", $name, msg);
passed += 1;
}
CheckResult::Warn(msg) => {
println!(" [WARN] {}: {}", $name, msg);
warned += 1;
}
CheckResult::Fail(msg) => {
println!(" [FAIL] {}: {}", $name, msg);
failed += 1;
}
}
};
}
check!("Daemon", check_daemon().await);
check!("VM", check_vm().await);
check!("Docker API", check_docker_api().await);
check!("DNS resolver", check_dns_resolver());
#[cfg(target_os = "macos")]
{
check!("Bridge NIC", check_bridge_nic());
check!("Container route", check_container_route().await);
check!("ArcBoxHelper", check_helper().await);
}
println!();
if failed > 0 {
println!("{passed} passed, {warned} warnings, {failed} failed");
std::process::exit(1);
} else if warned > 0 {
println!("{passed} passed, {warned} warnings");
} else {
println!("All {passed} checks passed");
}
Ok(())
}
enum CheckResult {
Pass(String),
Warn(String),
Fail(String),
}
async fn check_daemon() -> CheckResult {
let sock = daemon_socket();
if !sock.exists() {
return CheckResult::Fail(format!("socket not found at {}", sock.display()));
}
match tokio::process::Command::new("curl")
.args([
"-s",
"--unix-socket",
&sock.to_string_lossy(),
"http://localhost/_ping",
])
.output()
.await
{
Ok(out) if out.status.success() => {
let body = String::from_utf8_lossy(&out.stdout);
if body.trim() == "OK" {
CheckResult::Pass("responding".into())
} else {
CheckResult::Warn(format!("unexpected response: {}", body.trim()))
}
}
_ => CheckResult::Fail("socket exists but not responding".into()),
}
}
async fn check_vm() -> CheckResult {
let sock = daemon_socket();
match tokio::process::Command::new("curl")
.args([
"-s",
"--max-time",
"3",
"--unix-socket",
&sock.to_string_lossy(),
"http://localhost/info",
])
.output()
.await
{
Ok(out) if out.status.success() => {
let body = String::from_utf8_lossy(&out.stdout);
if body.contains("KernelVersion") {
CheckResult::Pass("running".into())
} else {
CheckResult::Warn("daemon responded but VM may not be ready".into())
}
}
_ => CheckResult::Fail("VM not responding (Docker /info failed)".into()),
}
}
async fn check_docker_api() -> CheckResult {
let sock = daemon_socket();
match tokio::process::Command::new("curl")
.args([
"-s",
"--unix-socket",
&sock.to_string_lossy(),
"http://localhost/version",
])
.output()
.await
{
Ok(out) if out.status.success() => {
let body = String::from_utf8_lossy(&out.stdout);
if body.contains("ApiVersion") {
CheckResult::Pass("Docker Engine API reachable".into())
} else {
CheckResult::Warn("unexpected /version response".into())
}
}
_ => CheckResult::Fail("Docker API not responding".into()),
}
}
fn check_dns_resolver() -> CheckResult {
let resolver = std::path::Path::new("/etc/resolver/arcbox.local");
if resolver.exists() {
match std::fs::read_to_string(resolver) {
Ok(content) if content.contains("nameserver") => {
CheckResult::Pass(format!("{}", resolver.display()))
}
_ => CheckResult::Warn("file exists but looks invalid".into()),
}
} else {
CheckResult::Warn(
"not installed — run 'sudo abctl dns install' for *.arcbox.local resolution".into(),
)
}
}
#[cfg(target_os = "macos")]
fn check_bridge_nic() -> CheckResult {
match arcbox_core::bridge_discovery::find_bridge_with_vmenet() {
Some((bridge, member)) => CheckResult::Pass(format!("{bridge} with {member} member")),
None => CheckResult::Fail("no bridge interface with vmenet member found".into()),
}
}
#[cfg(target_os = "macos")]
async fn check_container_route() -> CheckResult {
let Some((bridge, _)) = arcbox_core::bridge_discovery::find_bridge_with_vmenet() else {
return CheckResult::Fail("cannot identify ArcBox bridge".into());
};
match arcbox_core::route_reconciler::container_route_mode(&bridge).await {
Ok(Some(arcbox_core::route_reconciler::RouteMode::Preferred)) => {
CheckResult::Pass(format!("172.16.0.0/12 → {bridge}"))
}
Ok(Some(arcbox_core::route_reconciler::RouteMode::SplitFallback)) => CheckResult::Pass(
format!("172.16.0.0/13 + 172.24.0.0/13 → {bridge} (VPN-compatible fallback)"),
),
Ok(None) => CheckResult::Fail(format!(
"neither 172.16.0.0/12 nor both split /13 routes point to {bridge}"
)),
Err(error) => CheckResult::Fail(format!("route query failed: {error}")),
}
}
#[cfg(target_os = "macos")]
async fn check_helper() -> CheckResult {
let min = arcbox_constants::helper::MIN_HELPER_VERSION;
match arcbox_helper::client::Client::probe_version().await {
Ok(version) => match arcbox_constants::helper::parse_helper_version(&version) {
Some(installed) => {
let Some(minimum) = arcbox_constants::helper::parse_semver_triple(min) else {
return CheckResult::Fail(format!(
"invalid MIN_HELPER_VERSION constant {min:?}"
));
};
if arcbox_constants::helper::helper_version_satisfies(installed, minimum) {
CheckResult::Pass(format!("reachable ({version})"))
} else {
CheckResult::Fail(format!(
"{version} is older than required {min}; run 'sudo abctl _install --no-daemon --no-shell'"
))
}
}
None => CheckResult::Fail(format!(
"unrecognized version {version:?}; run 'sudo abctl _install --no-daemon --no-shell'"
)),
},
Err(arcbox_helper::client::ClientError::Connection(e)) => {
CheckResult::Fail(format!("not reachable via launchd socket: {e}"))
}
Err(e) => CheckResult::Fail(format!("version RPC failed: {e}")),
}
}
fn daemon_socket() -> std::path::PathBuf {
if let Ok(val) = std::env::var("ARCBOX_SOCKET") {
return std::path::PathBuf::from(val);
}
arcbox_constants::paths::HostLayout::from_env_or_default().docker_socket
}