Initial commit: battlenet-manager tray daemon and CLI
This commit is contained in:
+227
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user