use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use crate::slim;
use crate::solver::session;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
Ok,
Warn,
Fail,
}
#[derive(Debug, Clone)]
pub struct Check {
pub name: &'static str,
pub status: Status,
pub detail: String,
pub hint: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Report {
pub checks: Vec<Check>,
}
impl Report {
pub fn ok(&self) -> bool {
!self.checks.iter().any(|c| c.status == Status::Fail)
}
}
pub fn run() -> Report {
Report {
checks: vec![
xvfb_check(),
chrome_check(),
fingerprint_check(),
gpu_check(),
],
}
}
fn xvfb_check() -> Check {
match on_path("Xvfb") {
Some(p) => ok("Xvfb", format!("found at {}", p.display())),
None => fail(
"Xvfb",
"not found on PATH".into(),
"install Xvfb — e.g. `apt install xvfb` (Debian/Ubuntu) or \
`dnf install xorg-x11-server-Xvfb` (Fedora). The off-screen display is required.",
),
}
}
fn chrome_check() -> Check {
match session::locate_chrome() {
Some((bin, version)) => ok("Chrome", format!("{version} (via `{bin}`)")),
None => fail(
"Chrome",
"no google-chrome / chromium found".into(),
"install Google Chrome, or point mara at it with CHROME=/path/to/chrome. \
Don't use Chrome-for-Testing — it's a more detectable build.",
),
}
}
fn fingerprint_check() -> Check {
match (session::installed_chrome_major(), slim::profile_major()) {
(Some(installed), Some(pinned)) if installed == pinned => ok(
"Fingerprint pin",
format!(
"Chrome {installed} ↔ pinned slim profile {} (matched)",
slim::PROFILE
),
),
(Some(installed), Some(pinned)) => Check {
name: "Fingerprint pin",
status: Status::Fail,
detail: format!(
"installed Chrome {installed} ≠ pinned slim profile {pinned} ({})",
slim::PROFILE
),
hint: Some(format!(
"slim clearance replay will be rejected on Cloudflare-challenged pages until you bump \
wreq/wreq-util AND slim::EMULATION/PROFILE to Chrome{installed}, in lockstep with the \
binary. The headed solver and the `fetch` lease are unaffected."
)),
},
_ => Check {
name: "Fingerprint pin",
status: Status::Warn,
detail: "skipped — Chrome version unreadable".into(),
hint: Some(
"install/repair Chrome so its `--version` is readable to verify the slim pin."
.into(),
),
},
}
}
fn gpu_check() -> Check {
match render_node() {
Some(node) => ok("GPU render node", format!("found at {}", node.display())),
None => Check {
name: "GPU render node",
status: Status::Warn,
detail: "no /dev/dri/renderD* found".into(),
hint: Some(
"the Xvfb path needs a GPU DRM render node for GPU-backed WebGL; without it WebGL is \
null — a bot tell that can stop pages clearing. Ensure /dev/dri/renderD* exists and \
this user is in the `render`/`video` group. (Not needed for --real-display.)"
.into(),
),
},
}
}
fn render_node() -> Option<PathBuf> {
std::fs::read_dir("/dev/dri")
.ok()?
.flatten()
.map(|e| e.path())
.find(|p| {
p.file_name()
.and_then(OsStr::to_str)
.is_some_and(|n| n.starts_with("renderD"))
})
}
fn on_path(bin: &str) -> Option<PathBuf> {
search_path(&std::env::var_os("PATH")?, bin)
}
fn search_path(path: &OsStr, bin: &str) -> Option<PathBuf> {
std::env::split_paths(path)
.map(|dir| dir.join(bin))
.find(|p| is_executable(p))
}
fn is_executable(p: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(p)
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
fn ok(name: &'static str, detail: String) -> Check {
Check {
name,
status: Status::Ok,
detail,
hint: None,
}
}
fn fail(name: &'static str, detail: String, hint: &str) -> Check {
Check {
name,
status: Status::Fail,
detail,
hint: Some(hint.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn report_ok_tolerates_warn_but_not_fail() {
let warn = Check {
name: "w",
status: Status::Warn,
detail: String::new(),
hint: None,
};
let okc = Check {
name: "o",
status: Status::Ok,
detail: String::new(),
hint: None,
};
let failc = Check {
name: "f",
status: Status::Fail,
detail: String::new(),
hint: None,
};
assert!(
Report {
checks: vec![okc.clone(), warn.clone()]
}
.ok()
);
assert!(
!Report {
checks: vec![okc, warn, failc]
}
.ok()
);
}
#[test]
fn search_path_finds_executable_and_misses_absent() {
let dir = tempfile::tempdir().unwrap();
let bin = dir.path().join("mara-doctor-fixture");
fs::write(&bin, b"#!/bin/sh\n").unwrap();
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&bin, fs::Permissions::from_mode(0o755)).unwrap();
let path = std::env::join_paths([dir.path()]).unwrap();
assert_eq!(
search_path(&path, "mara-doctor-fixture").as_deref(),
Some(bin.as_path())
);
assert_eq!(search_path(&path, "nope-not-here"), None);
}
#[test]
fn non_executable_is_not_on_path() {
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("data.txt");
fs::write(&f, b"x").unwrap();
let path = std::env::join_paths([dir.path()]).unwrap();
assert_eq!(search_path(&path, "data.txt"), None);
}
#[test]
fn run_is_total_and_covers_every_dependency() {
let report = run();
let names: Vec<_> = report.checks.iter().map(|c| c.name).collect();
assert_eq!(
names,
["Xvfb", "Chrome", "Fingerprint pin", "GPU render node"]
);
}
}