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:
+156
-169
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user