Initial commit: battlenet-manager tray daemon and CLI

This commit is contained in:
funman300
2026-04-16 13:28:17 -07:00
commit 1559ee5f2b
7 changed files with 791 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
use crate::config::Config;
use std::path::Path;
use std::process::Command;
struct Check {
label: &'static str,
pass: bool,
detail: String,
}
impl Check {
fn pass(label: &'static str, detail: impl Into<String>) -> Self {
Self { label, pass: true, detail: detail.into() }
}
fn fail(label: &'static str, detail: impl Into<String>) -> Self {
Self { label, pass: false, detail: detail.into() }
}
}
pub fn run(config: &Config) {
let mut checks: Vec<Check> = Vec::new();
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: battlenet-manager 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: battlenet-manager 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"])
.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 {
("", "\x1b[1;32m", "\x1b[0m")
} else {
("", "\x1b[1;31m", "\x1b[0m")
};
println!(" {colour}{symbol}{reset} {:16} {}", c.label, c.detail);
}
println!();
if issues == 0 {
println!(" \x1b[1;32mAll checks passed.\x1b[0m");
} else {
println!(" \x1b[1;31m{issues} issue(s) found — see ✗ items above.\x1b[0m");
}
println!();
}
// ── helpers ──────────────────────────────────────────────────────────────────
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 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 is_owned_by_current_user(path: &Path) -> bool {
// Compare stat uid with current euid via id command (avoids libc dependency)
let uid_output = Command::new("id").arg("-u").output().ok();
let stat_output = Command::new("stat")
.args(["-c", "%u", path.to_str().unwrap_or("")])
.output()
.ok();
match (uid_output, stat_output) {
(Some(u), Some(s)) => {
let uid = String::from_utf8_lossy(&u.stdout).trim().to_string();
let owner = String::from_utf8_lossy(&s.stdout).trim().to_string();
uid == owner
}
_ => true, // assume OK if we can't check
}
}