d619c992c1
Add a GUI "Arm client" button that reproduces client_arm.sh in a single pkexec batch: kernel.yama.ptrace_scope=0, DNAT of EA's hardcoded redirector IP to the OpenFUT server (+ MASQUERADE reply path), and /etc/hosts rewrites for every dead EA hostname (removing foreign shadow lines first, so glibc's first-match resolution can't land on a stale loopback entry). All steps are idempotent (delete-then-add) and injection-safe: config values are charset- validated and rejected on a surprising character, never shell-escaped. arm() returns the concrete change list, which the button logs line-by-line and echoes as an inline pass/fail status on the pre-launch tab (no tab jump, no reuse of the local-services toast). This necessarily lands the surrounding launcher modularization the arm feature is built on, extracted from the former monolithic app.rs/process.rs: - preflight: advisory pre-launch checks (ptrace, redirector DNAT, hostnames, backend reachability) that colour rows but never block Launch - local_services: launcher-owned LSX/autopatch child processes - game_launch, account_sync, health, netcheck helpers - openfut-common: dependency-free shared server-destination/port mapping, used by both the launcher and (separately) openfut_hook.dll openfut-hook RE changes are intentionally left uncommitted (separate concern). fmt + clippy -D warnings clean; 46 tests pass.
134 lines
4.2 KiB
Rust
134 lines
4.2 KiB
Rust
//! Read-only health monitoring of the (remote) OpenFUT server.
|
|
//!
|
|
//! The launcher no longer *controls* the servers — they run elsewhere (e.g. in
|
|
//! Docker on the server host). This module polls the configured server in a
|
|
//! background thread and exposes a snapshot the UI can render. It never starts,
|
|
//! stops, or assumes anything about how the server is hosted; it only asks
|
|
//! "can the FIFA client reach it right now?".
|
|
|
|
use std::{
|
|
net::{TcpStream, ToSocketAddrs},
|
|
sync::{
|
|
atomic::{AtomicBool, Ordering},
|
|
Arc, Mutex,
|
|
},
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
const POLL_INTERVAL: Duration = Duration::from_secs(3);
|
|
const CONNECT_TIMEOUT: Duration = Duration::from_secs(3);
|
|
|
|
/// A snapshot of the last health probe, rendered by the dashboard.
|
|
#[derive(Clone)]
|
|
pub struct HealthState {
|
|
/// None = not yet checked / no target; Some(true/false) = reachable or not.
|
|
pub reachable: Option<bool>,
|
|
pub detail: String,
|
|
pub last_checked: Option<Instant>,
|
|
}
|
|
|
|
impl Default for HealthState {
|
|
fn default() -> Self {
|
|
Self {
|
|
reachable: None,
|
|
detail: "No server configured.".into(),
|
|
last_checked: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Background poller. Holds a shared target (host, port) the UI can update when
|
|
/// the user changes the server address, and a shared state the UI reads.
|
|
pub struct HealthMonitor {
|
|
pub state: Arc<Mutex<HealthState>>,
|
|
target: Arc<Mutex<Option<(String, u16)>>>,
|
|
running: Arc<AtomicBool>,
|
|
}
|
|
|
|
impl HealthMonitor {
|
|
pub fn new() -> Self {
|
|
let state = Arc::new(Mutex::new(HealthState::default()));
|
|
let target: Arc<Mutex<Option<(String, u16)>>> = Arc::new(Mutex::new(None));
|
|
let running = Arc::new(AtomicBool::new(true));
|
|
|
|
let t_state = Arc::clone(&state);
|
|
let t_target = Arc::clone(&target);
|
|
let t_running = Arc::clone(&running);
|
|
thread::spawn(move || {
|
|
while t_running.load(Ordering::Relaxed) {
|
|
let target = t_target.lock().unwrap().clone();
|
|
match target {
|
|
None => {
|
|
*t_state.lock().unwrap() = HealthState::default();
|
|
}
|
|
Some((host, port)) => {
|
|
let snapshot = probe(&host, port);
|
|
*t_state.lock().unwrap() = snapshot;
|
|
}
|
|
}
|
|
thread::sleep(POLL_INTERVAL);
|
|
}
|
|
});
|
|
|
|
Self {
|
|
state,
|
|
target,
|
|
running,
|
|
}
|
|
}
|
|
|
|
/// Point the monitor at a new server address (host + bridge port). Passing
|
|
/// None (e.g. no server configured) puts it back into the idle state.
|
|
pub fn set_target(&self, target: Option<(String, u16)>) {
|
|
*self.target.lock().unwrap() = target;
|
|
}
|
|
|
|
pub fn snapshot(&self) -> HealthState {
|
|
self.state.lock().unwrap().clone()
|
|
}
|
|
}
|
|
|
|
impl Drop for HealthMonitor {
|
|
fn drop(&mut self) {
|
|
self.running.store(false, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
/// A single reachability probe: DNS-resolve host:port and attempt a bounded TCP
|
|
/// connect. A successful connect proves the FIFA client can reach the bridge.
|
|
fn probe(host: &str, port: u16) -> HealthState {
|
|
let now = Some(Instant::now());
|
|
let addrs = match (host, port).to_socket_addrs() {
|
|
Ok(a) => a.collect::<Vec<_>>(),
|
|
Err(e) => {
|
|
return HealthState {
|
|
reachable: Some(false),
|
|
detail: format!("Cannot resolve {host}: {e}"),
|
|
last_checked: now,
|
|
};
|
|
}
|
|
};
|
|
if addrs.is_empty() {
|
|
return HealthState {
|
|
reachable: Some(false),
|
|
detail: format!("{host} resolved to no addresses"),
|
|
last_checked: now,
|
|
};
|
|
}
|
|
for addr in &addrs {
|
|
if TcpStream::connect_timeout(addr, CONNECT_TIMEOUT).is_ok() {
|
|
return HealthState {
|
|
reachable: Some(true),
|
|
detail: format!("Reachable at {addr}"),
|
|
last_checked: now,
|
|
};
|
|
}
|
|
}
|
|
HealthState {
|
|
reachable: Some(false),
|
|
detail: format!("{host}:{port} not reachable"),
|
|
last_checked: now,
|
|
}
|
|
}
|