Files
umutray/src/diagnose.rs
T
funman300 9b7e474e80 refactor: apply CLAUDE.md code quality improvements and add packaging
- Add #![forbid(unsafe_code)] to main.rs (issue #3)
- Replace raw ANSI escape codes with owo-colors crate (issue #2)
- Replace manual HOME path construction with dirs::home_dir() (issue #5)
- Ship umutray.service as a static file; service::install() substitutes
  the binary path at install time instead of generating the unit at runtime
- Add packaging/PKGBUILD following Arch Rust package guidelines
- Add CLAUDE.md tracking refactor tasks
- setup.rs: clean up downloaded temp files on abort/back, save launcher
  to config only after successful install, auto-start download when a
  preset has an installer_url
- util.rs: add pick_folder() using zenity/kdialog subprocesses (no rfd)
- config.rs: populate installer_url for all 6 built-in presets with
  official download URLs
- Document the Option<Option<Vec<String>>> gamescope pattern at main.rs:307

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 19:28:10 -07:00

255 lines
7.3 KiB
Rust

use crate::config::{Config, Launcher};
use anyhow::Result;
use owo_colors::OwoColorize;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::process::{Command, Stdio};
#[derive(Debug, Clone)]
pub struct CheckResult {
pub label: String,
pub pass: bool,
pub detail: String,
}
impl CheckResult {
fn pass(label: impl Into<String>, detail: impl Into<String>) -> Self {
Self { label: label.into(), pass: true, detail: detail.into() }
}
fn fail(label: impl Into<String>, detail: impl Into<String>) -> Self {
Self { label: label.into(), pass: false, detail: detail.into() }
}
}
pub fn run_checks(config: &Config, name: Option<&str>) -> Result<Vec<CheckResult>> {
let mut checks = vec![
global_umu_check(),
global_vulkan_check(),
global_display_check(),
compat_dir_check(config),
wineserver_check(config),
];
let launchers: Vec<&Launcher> = if let Some(n) = name {
let l = config
.find(n)
.ok_or_else(|| anyhow::anyhow!("unknown launcher '{n}'"))?;
vec![l]
} else {
config.launchers.iter().collect()
};
for l in launchers {
checks.extend(launcher_checks(l));
}
Ok(checks)
}
pub fn run(config: &Config, name: Option<&str>) -> Result<()> {
let checks = run_checks(config, name)?;
let mut issues = 0u32;
println!();
for c in &checks {
if !c.pass {
issues += 1;
}
if c.pass {
println!(" {} {:24} {}", "".green().bold(), c.label, c.detail);
} else {
println!(" {} {:24} {}", "".red().bold(), c.label, c.detail);
}
}
println!();
if issues == 0 {
println!(" {}", "All checks passed.".green().bold());
} else {
println!(" {}", format!("{issues} issue(s) found — see ✗ items above.").red().bold());
}
println!();
Ok(())
}
fn global_umu_check() -> CheckResult {
match which("umu-run") {
Some(p) => CheckResult::pass("umu-run", format!("found at {p}")),
None => CheckResult::fail("umu-run", "not found — install umu-launcher"),
}
}
fn global_vulkan_check() -> CheckResult {
let ok = Command::new("vulkaninfo")
.arg("--summary")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
CheckResult::pass("vulkan", "vulkaninfo OK")
} else {
CheckResult::fail(
"vulkan",
"vulkaninfo failed — check GPU drivers / vulkan-tools",
)
}
}
fn global_display_check() -> CheckResult {
let display = std::env::var("DISPLAY").ok();
let wayland = std::env::var("WAYLAND_DISPLAY").ok();
match (display, wayland) {
(Some(d), Some(_)) => CheckResult::pass("display", format!("XWayland (DISPLAY={d})")),
(Some(d), None) => CheckResult::pass("display", format!("X11 (DISPLAY={d})")),
(None, Some(_)) => CheckResult::fail(
"display",
"Wayland session but DISPLAY unset; XWayland needed",
),
(None, None) => CheckResult::fail("display", "neither DISPLAY nor WAYLAND_DISPLAY set"),
}
}
fn compat_dir_check(config: &Config) -> CheckResult {
let n = count_ge_proton(&config.proton_compat_dir);
if config.proton_version == "GE-Proton" {
CheckResult::pass(
"proton",
format!(
"tracking latest; {n} version(s) in {}",
config.proton_compat_dir.display()
),
)
} else {
let path = config.proton_compat_dir.join(&config.proton_version);
if path.exists() {
CheckResult::pass("proton", format!("{} installed", config.proton_version))
} else {
CheckResult::fail(
"proton",
format!(
"{} missing — run: umutray update-proton --version={}",
config.proton_version, config.proton_version
),
)
}
}
}
fn wineserver_check(config: &Config) -> CheckResult {
let count = wineserver_count();
if count == 0 {
return CheckResult::pass("wine procs", "no wineserver running");
}
let any_running = config.launchers.iter().any(crate::launcher::is_running);
if any_running {
CheckResult::pass(
"wine procs",
format!("{count} wineserver process(es); launcher active"),
)
} else {
CheckResult::fail(
"wine procs",
format!("{count} stale wineserver process(es) — try: umutray kill"),
)
}
}
fn wineserver_count() -> usize {
Command::new("pgrep")
.args(["-c", "-f", "wineserver"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.trim().parse::<usize>().ok())
.unwrap_or(0)
}
fn launcher_checks(l: &Launcher) -> Vec<CheckResult> {
let mut out = Vec::new();
let tag = format!("[{}]", l.name);
if !l.prefix_dir.exists() {
out.push(CheckResult::fail(
format!("{tag} prefix"),
format!(
"{} missing — run: umutray setup {}",
l.prefix_dir.display(),
l.name
),
));
return out;
}
out.push(CheckResult::pass(
format!("{tag} prefix"),
l.prefix_dir.display().to_string(),
));
let exe = l.full_exe_path();
if exe.exists() {
out.push(CheckResult::pass(format!("{tag} exe"), "installed"));
} else {
out.push(CheckResult::fail(
format!("{tag} exe"),
format!("missing — run: umutray setup {}", l.name),
));
}
if is_owned_by_current_user(&l.prefix_dir) {
out.push(CheckResult::pass(format!("{tag} owner"), "owned by current user"));
} else {
out.push(CheckResult::fail(
format!("{tag} owner"),
"not owned by current user",
));
}
if crate::launcher::is_running(l) {
out.push(CheckResult::pass(format!("{tag} process"), "currently running"));
} else {
out.push(CheckResult::pass(format!("{tag} process"), "not running"));
}
out
}
fn count_ge_proton(dir: &Path) -> usize {
std::fs::read_dir(dir)
.ok()
.map(|entries| {
entries
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_str()
.map(|s| s.starts_with("GE-Proton"))
.unwrap_or(false)
})
.count()
})
.unwrap_or(0)
}
fn which(cmd: &str) -> Option<String> {
Command::new("which")
.arg(cmd)
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
}
fn is_owned_by_current_user(path: &Path) -> bool {
let file_uid = match std::fs::metadata(path) {
Ok(m) => m.uid(),
Err(_) => return true,
};
let current_uid: Option<u32> = Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.trim().parse().ok());
match current_uid {
Some(uid) => uid == file_uid,
None => true,
}
}