modified: README.md

modified:   src/config.rs
	modified:   src/diagnose.rs
	modified:   src/launcher.rs
	modified:   src/main.rs
	new file:   src/setup.rs
	modified:   src/tray.rs
This commit is contained in:
funman300
2026-04-16 21:43:58 -07:00
parent 336c5d908e
commit 7e5ed3d447
7 changed files with 690 additions and 414 deletions
+156 -169
View File
@@ -1,176 +1,56 @@
use crate::config::Config;
use crate::config::{Config, Launcher};
use anyhow::Result;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::process::Command;
use std::process::{Command, Stdio};
struct Check {
label: &'static str,
label: String,
pass: bool,
detail: String,
}
impl Check {
fn pass(label: &'static str, detail: impl Into<String>) -> Self {
Self { label, pass: true, detail: detail.into() }
fn pass(label: impl Into<String>, detail: impl Into<String>) -> Self {
Self { label: label.into(), pass: true, detail: detail.into() }
}
fn fail(label: &'static str, detail: impl Into<String>) -> Self {
Self { label, pass: false, 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(config: &Config) {
let mut checks: Vec<Check> = Vec::new();
pub fn run(config: &Config, name: Option<&str>) -> Result<()> {
let mut checks: Vec<Check> = vec![
global_umu_check(),
global_vulkan_check(),
global_display_check(),
compat_dir_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));
}
let mut issues = 0u32;
// ── umu-run ──────────────────────────────────────────────────────────────
match which("umu-run") {
Some(p) => checks.push(Check::pass("umu-run", format!("found at {p}"))),
None => {
checks.push(Check::fail("umu-run", "not found — sudo pacman -S umu-launcher"));
issues += 1;
}
}
// ── Wine prefix ──────────────────────────────────────────────────────────
if config.prefix_dir.exists() {
checks.push(Check::pass("prefix", format!("{:?}", config.prefix_dir)));
let exe = config.launcher_exe();
if exe.exists() {
checks.push(Check::pass("launcher", "Battle.net Launcher.exe present"));
} else {
checks.push(Check::fail(
"launcher",
format!("not found at {exe:?} — run battlenet-umu-setup.sh"),
));
issues += 1;
}
// Stale agent lock
let lock = config.prefix_dir
.join("drive_c/ProgramData/Battle.net/Agent/agent.lock");
if lock.exists() {
if let Ok(meta) = std::fs::metadata(&lock) {
if let Ok(modified) = meta.modified() {
let age = std::time::SystemTime::now()
.duration_since(modified)
.unwrap_or_default()
.as_secs();
if age > 300 {
checks.push(Check::fail(
"agent.lock",
format!("stale lock ({age}s old) — may cause BLZBNTBNA00000005; run: umutray kill"),
));
issues += 1;
} else {
checks.push(Check::pass("agent.lock", format!("fresh ({age}s) — launcher may be running")));
}
}
}
}
// Prefix ownership
if is_owned_by_current_user(&config.prefix_dir) {
checks.push(Check::pass("prefix owner", "owned by current user"));
} else {
checks.push(Check::fail(
"prefix owner",
"not owned by current user — Wine will misbehave",
));
issues += 1;
}
} else {
checks.push(Check::fail(
"prefix",
format!("{:?} does not exist — run battlenet-umu-setup.sh", config.prefix_dir),
));
issues += 1;
}
// ── GE-Proton ────────────────────────────────────────────────────────────
let installed_count = count_ge_proton(&config.proton_compat_dir);
if config.proton_version == "GE-Proton" {
if installed_count > 0 {
checks.push(Check::pass(
"proton",
format!("tracking latest; {installed_count} version(s) in {:?}", config.proton_compat_dir),
));
} else {
checks.push(Check::pass(
"proton",
"tracking latest — will auto-download on first launch",
));
}
} else {
let vpath = config.proton_compat_dir.join(&config.proton_version);
if vpath.exists() {
checks.push(Check::pass("proton", format!("{} installed", config.proton_version)));
} else {
checks.push(Check::fail(
"proton",
format!(
"{} not found — run: umutray update-proton --version={}",
config.proton_version, config.proton_version
),
));
issues += 1;
}
}
// ── Vulkan ───────────────────────────────────────────────────────────────
let vulkan_ok = Command::new("vulkaninfo")
.arg("--summary")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if vulkan_ok {
checks.push(Check::pass("vulkan", "vulkaninfo OK"));
} else {
checks.push(Check::fail("vulkan", "vulkaninfo failed or not installed — check GPU drivers and vulkan-tools"));
issues += 1;
}
// ── Display / XWayland ───────────────────────────────────────────────────
let display = std::env::var("DISPLAY").ok();
let wayland = std::env::var("WAYLAND_DISPLAY").ok();
match (display, wayland) {
(Some(d), Some(_)) => checks.push(Check::pass("display", format!("XWayland (DISPLAY={d})"))),
(Some(d), None) => checks.push(Check::pass("display", format!("X11 (DISPLAY={d})"))),
(None, Some(_)) => {
checks.push(Check::fail(
"display",
"Wayland session but DISPLAY not set — XWayland not running; Battle.net needs it",
));
issues += 1;
}
(None, None) => {
checks.push(Check::fail("display", "neither DISPLAY nor WAYLAND_DISPLAY is set"));
issues += 1;
}
}
// ── Running processes ────────────────────────────────────────────────────
let bnet_running = Command::new("pgrep")
.args(["-fi", "Battle\\.net"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if bnet_running {
checks.push(Check::pass("process", "Battle.net is currently running"));
} else {
checks.push(Check::pass("process", "Battle.net is not running"));
}
// ── Print ────────────────────────────────────────────────────────────────
println!();
for c in &checks {
let (symbol, colour, reset) = if c.pass {
if !c.pass {
issues += 1;
}
let (sym, col, rst) = if c.pass {
("", "\x1b[1;32m", "\x1b[0m")
} else {
("", "\x1b[1;31m", "\x1b[0m")
};
println!(" {colour}{symbol}{reset} {:16} {}", c.label, c.detail);
println!(" {col}{sym}{rst} {:24} {}", c.label, c.detail);
}
println!();
if issues == 0 {
@@ -179,18 +59,120 @@ pub fn run(config: &Config) {
println!(" \x1b[1;31m{issues} issue(s) found — see ✗ items above.\x1b[0m");
}
println!();
Ok(())
}
// ── helpers ──────────────────────────────────────────────────────────────────
fn global_umu_check() -> Check {
match which("umu-run") {
Some(p) => Check::pass("umu-run", format!("found at {p}")),
None => Check::fail("umu-run", "not found — install umu-launcher"),
}
}
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 global_vulkan_check() -> Check {
let ok = Command::new("vulkaninfo")
.arg("--summary")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
Check::pass("vulkan", "vulkaninfo OK")
} else {
Check::fail("vulkan", "vulkaninfo failed — check GPU drivers / vulkan-tools")
}
}
fn global_display_check() -> Check {
let display = std::env::var("DISPLAY").ok();
let wayland = std::env::var("WAYLAND_DISPLAY").ok();
match (display, wayland) {
(Some(d), Some(_)) => Check::pass("display", format!("XWayland (DISPLAY={d})")),
(Some(d), None) => Check::pass("display", format!("X11 (DISPLAY={d})")),
(None, Some(_)) => Check::fail(
"display",
"Wayland session but DISPLAY unset; XWayland needed",
),
(None, None) => Check::fail("display", "neither DISPLAY nor WAYLAND_DISPLAY set"),
}
}
fn compat_dir_check(config: &Config) -> Check {
let n = count_ge_proton(&config.proton_compat_dir);
if config.proton_version == "GE-Proton" {
Check::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() {
Check::pass("proton", format!("{} installed", config.proton_version))
} else {
Check::fail(
"proton",
format!(
"{} missing — run: umutray update-proton --version={}",
config.proton_version, config.proton_version
),
)
}
}
}
fn launcher_checks(l: &Launcher) -> Vec<Check> {
let mut out = Vec::new();
let tag = format!("[{}]", l.name);
if !l.prefix_dir.exists() {
out.push(Check::fail(
format!("{tag} prefix"),
format!(
"{} missing — run: umutray setup {}",
l.prefix_dir.display(),
l.name
),
));
return out;
}
out.push(Check::pass(
format!("{tag} prefix"),
l.prefix_dir.display().to_string(),
));
let exe = l.full_exe_path();
if exe.exists() {
out.push(Check::pass(format!("{tag} exe"), "installed"));
} else {
out.push(Check::fail(
format!("{tag} exe"),
format!("missing — run: umutray setup {}", l.name),
));
}
if is_owned_by_current_user(&l.prefix_dir) {
out.push(Check::pass(
format!("{tag} owner"),
"owned by current user",
));
} else {
out.push(Check::fail(
format!("{tag} owner"),
"not owned by current user",
));
}
if crate::launcher::is_running(l) {
out.push(Check::pass(format!("{tag} process"), "currently running"));
} else {
out.push(Check::pass(format!("{tag} process"), "not running"));
}
out
}
fn count_ge_proton(dir: &Path) -> usize {
@@ -210,22 +192,27 @@ fn count_ge_proton(dir: &Path) -> usize {
.unwrap_or(0)
}
fn is_owned_by_current_user(path: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
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, // assume OK if we can't check
Err(_) => return true,
};
// No std API for the current process uid; shell out once to `id -u`.
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,