feat(launcher): one-click client arming + modular preflight/services
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.
This commit is contained in:
+79
-37
@@ -1,25 +1,7 @@
|
||||
use std::{path::{Path, PathBuf}, process::Command};
|
||||
|
||||
// ── Port 443 capability ───────────────────────────────────────────────────────
|
||||
|
||||
/// Check whether the bridge binary already has cap_net_bind_service set.
|
||||
pub fn bridge_has_cap443(binary: &Path) -> bool {
|
||||
std::process::Command::new("getcap")
|
||||
.arg(binary)
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).contains("cap_net_bind_service"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Grant cap_net_bind_service to the bridge binary so it can bind port 443
|
||||
/// without running as root. Uses pkexec (or sudo as fallback).
|
||||
pub fn setcap_bridge_443(binary: &Path) -> anyhow::Result<()> {
|
||||
let script = format!(
|
||||
"setcap cap_net_bind_service=+ep '{}'",
|
||||
binary.to_string_lossy()
|
||||
);
|
||||
run_elevated(&script)
|
||||
}
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
// ── Cert installation ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -67,17 +49,13 @@ fn try_wine_certutil(cert_src: &Path) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_elevated(script: &str) -> anyhow::Result<()> {
|
||||
let status = Command::new("pkexec")
|
||||
.args(["sh", "-c", script])
|
||||
.status();
|
||||
pub(crate) fn run_elevated(script: &str) -> anyhow::Result<()> {
|
||||
let status = Command::new("pkexec").args(["sh", "-c", script]).status();
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => Ok(()),
|
||||
_ => {
|
||||
let s = Command::new("sudo")
|
||||
.args(["sh", "-c", script])
|
||||
.status()?;
|
||||
let s = Command::new("sudo").args(["sh", "-c", script]).status()?;
|
||||
if s.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -90,10 +68,13 @@ fn run_elevated(script: &str) -> anyhow::Result<()> {
|
||||
// ── DLL hook deployment ───────────────────────────────────────────────────────
|
||||
|
||||
/// Deploy openfut_hook.dll into the FIFA 23 game directory and write
|
||||
/// openfut.cfg with the redirect IP the hook will use.
|
||||
/// openfut.cfg with the structured server configuration the hook reads.
|
||||
/// `cfg_contents` must be the full `openfut.cfg` body (see
|
||||
/// `LauncherConfig::hook_cfg_contents`) — this function does not invent any
|
||||
/// address itself, so a missing server can never silently become loopback.
|
||||
/// Uses `version.dll` as the hijack name — FIFA 23 loads it but defers to
|
||||
/// the system copy, so Proton picks up our local one first.
|
||||
pub fn deploy_hook_dll(dll_src: &Path, game_dir: &Path, redirect_ip: &str) -> anyhow::Result<()> {
|
||||
pub fn deploy_hook_dll(dll_src: &Path, game_dir: &Path, cfg_contents: &str) -> anyhow::Result<()> {
|
||||
if !dll_src.exists() {
|
||||
anyhow::bail!(
|
||||
"Hook DLL not found at {}. Build it first with:\n\
|
||||
@@ -104,17 +85,18 @@ pub fn deploy_hook_dll(dll_src: &Path, game_dir: &Path, redirect_ip: &str) -> an
|
||||
}
|
||||
std::fs::create_dir_all(game_dir)?;
|
||||
std::fs::copy(dll_src, game_dir.join("version.dll"))?;
|
||||
std::fs::write(game_dir.join("openfut.cfg"), redirect_ip)?;
|
||||
std::fs::write(game_dir.join("openfut.cfg"), cfg_contents)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update only openfut.cfg without redeploying the DLL.
|
||||
pub fn update_hook_config(game_dir: &Path, redirect_ip: &str) -> anyhow::Result<()> {
|
||||
/// Update only openfut.cfg without redeploying the DLL. `cfg_contents` is the
|
||||
/// full structured `openfut.cfg` body.
|
||||
pub fn update_hook_config(game_dir: &Path, cfg_contents: &str) -> anyhow::Result<()> {
|
||||
let cfg = game_dir.join("openfut.cfg");
|
||||
if !cfg.exists() {
|
||||
anyhow::bail!("Hook DLL not deployed yet — deploy first.");
|
||||
}
|
||||
std::fs::write(cfg, redirect_ip)?;
|
||||
std::fs::write(cfg, cfg_contents)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -134,5 +116,65 @@ pub fn hook_dll_deployed(game_dir: &Path) -> bool {
|
||||
|
||||
/// The Steam launch options the user needs to paste in to enable the override.
|
||||
/// Proton loads local DLLs named in WINEDLLOVERRIDES ahead of system ones.
|
||||
pub const STEAM_LAUNCH_OPTIONS: &str =
|
||||
"WINEDLLOVERRIDES=\"version=n,b\" %command%";
|
||||
pub const STEAM_LAUNCH_OPTIONS: &str = "WINEDLLOVERRIDES=\"version=n,b\" %command%";
|
||||
|
||||
// ── Game launch ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Launch the game via the user-provided shell command. Runs `sh -c <command>`
|
||||
/// (optionally from `workdir`), streaming stdout+stderr into `log_buf` on a
|
||||
/// background thread. The launcher does not assume Steam vs umu-run vs a custom
|
||||
/// script — whatever the user configured is what runs.
|
||||
pub fn launch_game(
|
||||
command: &str,
|
||||
workdir: &str,
|
||||
log_buf: std::sync::Arc<std::sync::Mutex<crate::logs::LogBuffer>>,
|
||||
) -> anyhow::Result<()> {
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
if command.trim().is_empty() {
|
||||
anyhow::bail!("No game launch command configured (set it in the Config tab).");
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("sh");
|
||||
cmd.arg("-c").arg(command);
|
||||
if !workdir.trim().is_empty() {
|
||||
cmd.current_dir(workdir);
|
||||
}
|
||||
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
|
||||
log_buf
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("[launcher] launching game: {command}"));
|
||||
|
||||
let mut child = cmd.spawn()?;
|
||||
|
||||
if let Some(out) = child.stdout.take() {
|
||||
let buf = std::sync::Arc::clone(&log_buf);
|
||||
std::thread::spawn(move || {
|
||||
for line in BufReader::new(out).lines().map_while(Result::ok) {
|
||||
buf.lock().unwrap().push(line);
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some(err) = child.stderr.take() {
|
||||
let buf = std::sync::Arc::clone(&log_buf);
|
||||
std::thread::spawn(move || {
|
||||
for line in BufReader::new(err).lines().map_while(Result::ok) {
|
||||
buf.lock().unwrap().push(line);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Reap the child in the background so a finished game doesn't linger as a
|
||||
// zombie; we don't block the UI on it.
|
||||
std::thread::spawn(move || {
|
||||
let _ = child.wait();
|
||||
log_buf
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push("[launcher] game process exited.".to_string());
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user