feat: spawn the Rust companion binaries, not Python scripts

The launcher shelled out to `python3 lsx_responder_v2.py` and `python3 autopatch.py`
from a configured tools directory. Both are now Rust binaries built from this
workspace (openfut-lsx, openfut-autopatch), so the launch contract loses the
interpreter and the script directory entirely: nothing to locate, nothing to
configure, and no way to run a stale checkout's copy of a responder.

Service::script() becomes Service::binary(), and resolve_binary() prefers a sibling
of the running launcher -- what a workspace build and any sane install layout both
produce -- falling back to the bare name so a PATH install still works. It returns the
bare name rather than failing so that spawn() stays the single place a missing binary
is reported, instead of two error paths for one condition.

foreign_pid() now matches an argv entry's FILE NAME rather than a suffix, so
`/path/to/openfut-lsx` matches while an unrelated argument that merely ends with the
same text does not. It deliberately still reads argv and not comm: comm is truncated
to 15 characters by the kernel, which would misreport both of these names -- the same
trap that made an earlier `pgrep -f` guard match its own shell.

Dead configuration removed rather than left vestigial: fifa17_python and
fifa17_tools_dir, their Settings controls, and validate_local_services(), whose only
two checks were those fields. A validation hook that can only return Ok(()) would
claim the launcher verifies local-service configuration when there is none. The
preflight tools-dir gate is gone too, while the ptrace_scope check it gated is kept --
that check is real and repairable via "Arm client"; only the gate died.

The env contract is unchanged, so the binaries are drop-in: LSX still receives
FUT_PERSONA_ID/FUT_PERSONA_NAME (the persona has to agree with Blaze's
LoginResponse.SESS.PDTL and UTAS's userInfo.personaId), autopatch still receives
OPENFUT_AUTOPATCH_LOG under XDG_RUNTIME_DIR and --launcher-pid so it cannot outlive
its owner, and each companion still gets its own process group.

74 tests green.
This commit is contained in:
funman300
2026-08-18 05:30:35 +00:00
parent c5424158b9
commit 1cd4f18e92
5 changed files with 198 additions and 282 deletions
+76 -104
View File
@@ -717,9 +717,6 @@ impl LauncherApp {
if phase == launch::Phase::StartingServices {
return (launch::Readiness::Busy, "Starting…".into());
}
if self.config.fifa17_tools_dir.trim().is_empty() {
return (launch::Readiness::Attention, "Not configured".into());
}
let runtimes = self.observe_services();
let ready = runtimes.iter().filter(|(_, r)| r.ready()).count();
let blocked = runtimes
@@ -764,8 +761,6 @@ impl LauncherApp {
/// OpenFUT is actively being reverse engineered, so independent control of
/// each moving part stays available — it is just no longer the front door.
fn ui_advanced(&mut self, ui: &mut Ui) {
let tools_configured = !self.config.fifa17_tools_dir.trim().is_empty();
ui.label(
RichText::new(
"Everything here happens automatically when you press Launch. These \
@@ -789,87 +784,84 @@ impl LauncherApp {
);
ui.add_space(6.0);
if !tools_configured {
ui.colored_label(
theme::WARN,
"FIFA 17 tools dir not set — configure it in Settings.",
);
} else {
let runtimes = self.observe_services();
egui::Grid::new("advanced_services_grid")
.num_columns(4)
.spacing([12.0, 10.0])
.min_col_width(90.0)
.show(ui, |ui| {
for (service, runtime) in runtimes {
ui.label(RichText::new(service.label()).color(theme::TEXT).strong());
if self.controller.services.lock().stopping(service) {
theme::status_pill(ui, "Stopping", Status::Busy);
} else if runtime.running && runtime.started_by_launcher {
theme::status_pill(ui, "Running", Status::Ok);
} else if runtime.running {
theme::status_pill(ui, "Running (foreign)", Status::Warn);
} else if runtime.detail.is_some() {
theme::status_pill(ui, "Blocked", Status::Error);
} else {
theme::status_pill(ui, "Stopped", Status::Idle);
}
// Only ever facts the launcher established.
let mut facts = Vec::new();
if let Some(pid) = runtime.pid {
facts.push(format!("pid {pid}"));
}
if let Some(detail) = &runtime.detail {
facts.push(detail.clone());
}
ui.label(
RichText::new(if facts.is_empty() {
"".to_string()
} else {
facts.join(" · ")
})
.color(theme::TEXT_FAINT)
.small(),
);
ui.horizontal(|ui| {
if runtime.running {
if ui
.add_enabled(
runtime.started_by_launcher,
egui::Button::new("Restart"),
)
.clicked()
{
self.advanced_stop_service(service);
self.restart_queue.push(service);
}
if ui
.add_enabled(
runtime.started_by_launcher,
egui::Button::new("Stop"),
)
.on_disabled_hover_text(
"Started outside this launcher — stop it where it \
was started.",
)
.clicked()
{
self.advanced_stop_service(service);
}
} else if ui.button("Start").clicked() {
self.advanced_start_service(service);
}
});
ui.end_row();
// Rendered unconditionally: the companions are workspace binaries
// resolved relative to this launcher, so there is no configuration that
// could make this panel inapplicable. A binary that is genuinely absent
// surfaces as that service's own spawn error, not as a hidden panel.
let runtimes = self.observe_services();
egui::Grid::new("advanced_services_grid")
.num_columns(4)
.spacing([12.0, 10.0])
.min_col_width(90.0)
.show(ui, |ui| {
for (service, runtime) in runtimes {
ui.label(RichText::new(service.label()).color(theme::TEXT).strong());
if self.controller.services.lock().stopping(service) {
theme::status_pill(ui, "Stopping", Status::Busy);
} else if runtime.running && runtime.started_by_launcher {
theme::status_pill(ui, "Running", Status::Ok);
} else if runtime.running {
theme::status_pill(ui, "Running (foreign)", Status::Warn);
} else if runtime.detail.is_some() {
theme::status_pill(ui, "Blocked", Status::Error);
} else {
theme::status_pill(ui, "Stopped", Status::Idle);
}
});
if let Some((ok, msg)) = &self.local_services_message {
ui.add_space(6.0);
ui.colored_label(if *ok { theme::SUCCESS } else { theme::ERROR }, msg);
}
// Only ever facts the launcher established.
let mut facts = Vec::new();
if let Some(pid) = runtime.pid {
facts.push(format!("pid {pid}"));
}
if let Some(detail) = &runtime.detail {
facts.push(detail.clone());
}
ui.label(
RichText::new(if facts.is_empty() {
"".to_string()
} else {
facts.join(" · ")
})
.color(theme::TEXT_FAINT)
.small(),
);
ui.horizontal(|ui| {
if runtime.running {
if ui
.add_enabled(
runtime.started_by_launcher,
egui::Button::new("Restart"),
)
.clicked()
{
self.advanced_stop_service(service);
self.restart_queue.push(service);
}
if ui
.add_enabled(
runtime.started_by_launcher,
egui::Button::new("Stop"),
)
.on_disabled_hover_text(
"Started outside this launcher — stop it where it \
was started.",
)
.clicked()
{
self.advanced_stop_service(service);
}
} else if ui.button("Start").clicked() {
self.advanced_start_service(service);
}
});
ui.end_row();
}
});
if let Some((ok, msg)) = &self.local_services_message {
ui.add_space(6.0);
ui.colored_label(if *ok { theme::SUCCESS } else { theme::ERROR }, msg);
}
ui.add_space(14.0);
@@ -1079,8 +1071,6 @@ impl LauncherApp {
/// sequence uses, so the two can never disagree about what is running.
fn advanced_start_service(&mut self, service: crate::local_services::Service) {
let spec = crate::local_services::SpawnSpec {
python: self.config.fifa17_python.clone(),
tools_dir: self.config.fifa17_tools_dir.clone(),
persona_id: self.config.fut_persona_id,
persona_name: self.config.fut_persona_name.clone(),
capability: match service {
@@ -1467,24 +1457,6 @@ impl LauncherApp {
.text_edit_singleline(&mut self.config.fifa_game_dir)
.changed();
ui.end_row();
ui.label(RichText::new("FIFA17 tools dir:").color(theme::TEXT_WEAK));
changed |= ui
.add(
egui::TextEdit::singleline(&mut self.config.fifa17_tools_dir)
.hint_text("fifa17-recon/tools (LSX + autopatch scripts)"),
)
.changed();
ui.end_row();
ui.label(RichText::new("Python:").color(theme::TEXT_WEAK));
changed |= ui
.add(
egui::TextEdit::singleline(&mut self.config.fifa17_python)
.hint_text("python3"),
)
.changed();
ui.end_row();
});
});
+2 -76
View File
@@ -168,26 +168,6 @@ pub struct LauncherConfig {
/// Dead EA hostnames that must resolve to `openfut_server_host`.
#[serde(default)]
pub ea_hostnames: Vec<String>,
// ── FIFA 17 local companion services (client-side, run on THIS machine) ──
// FIFA 17's FUT flow needs two pieces that are inherently local to the game
// box and cannot move to the server: the LSX Origin emulator (the game dials
// it on the hardcoded loopback 127.0.0.1:4216) and autopatch (patches
// FIFA17.exe process memory for ProtoSSL cert-verify). The launcher manages
// both as child processes. The heavy responders (Blaze/UTAS/roster/POW) run
// in the server container; these two stay here.
/// Directory holding the FIFA 17 Python responders (fifa17-recon `tools/`).
/// Empty means the local-services feature is unconfigured and its controls
/// stay disabled.
#[serde(default)]
pub fifa17_tools_dir: String,
/// Python interpreter used to run the local companion services.
#[serde(default = "default_python")]
pub fifa17_python: String,
}
fn default_python() -> String {
"python3".to_string()
}
fn default_https_port() -> u16 {
@@ -271,11 +251,6 @@ impl Default for LauncherConfig {
game_profile: GameProfile::default(),
ea_redirect_probe_ip: String::new(),
ea_hostnames: Vec::new(),
fifa17_tools_dir: base
.join("fifa17-recon/tools")
.to_string_lossy()
.into(),
fifa17_python: default_python(),
}
}
}
@@ -354,19 +329,6 @@ impl LauncherConfig {
self.server_config().validate().map_err(|e| e.to_string())
}
/// Validate the client-local FIFA 17 service configuration. Filesystem
/// existence is checked by the process launcher immediately before spawn;
/// this ensures required user configuration is never silently invented.
pub fn validate_local_services(&self) -> Result<(), String> {
if self.fifa17_tools_dir.trim().is_empty() {
return Err("No FIFA 17 tools dir configured. Set it in Settings.".into());
}
if self.fifa17_python.trim().is_empty() {
return Err("No Python interpreter configured. Set it in Settings.".into());
}
Ok(())
}
/// Validate every configuration value required by the one-button FIFA 17
/// launch path. Runtime state such as hook deployment is checked by the UI.
pub fn validate_launch_config(&self) -> Result<(), String> {
@@ -384,7 +346,7 @@ impl LauncherConfig {
.into(),
);
}
self.validate_local_services()
Ok(())
}
pub fn validate_account(&self) -> Result<(), String> {
@@ -508,31 +470,7 @@ mod tests {
}
#[test]
fn local_services_require_tools_dir_and_python() {
let mut c = LauncherConfig::default();
c.fifa17_tools_dir.clear();
assert!(c
.validate_local_services()
.unwrap_err()
.contains("tools dir"));
c.fifa17_tools_dir = "/tmp/fifa17-tools".into();
c.fifa17_python.clear();
assert!(c.validate_local_services().unwrap_err().contains("Python"));
}
#[test]
fn local_services_accept_explicit_configuration() {
let c = LauncherConfig {
fifa17_tools_dir: "/tmp/fifa17-tools".into(),
fifa17_python: "/usr/bin/python3".into(),
..LauncherConfig::default()
};
assert!(c.validate_local_services().is_ok());
}
#[test]
fn launch_config_requires_server_local_services_and_command() {
fn launch_config_requires_server_account_and_command() {
let mut c = LauncherConfig::default();
assert!(c.validate_launch_config().is_err());
@@ -545,14 +483,6 @@ mod tests {
.contains("launch command"));
c.game_launch_command = "/home/alex/Desktop/launch-fifa17.sh".into();
c.fifa17_tools_dir.clear();
assert!(c
.validate_launch_config()
.unwrap_err()
.contains("tools dir"));
c.fifa17_tools_dir = "/home/alex/Documents/OpenFUT/fifa17-recon/tools".into();
c.fifa17_python = "/usr/bin/python3".into();
assert!(c.validate_launch_config().is_ok());
}
@@ -580,8 +510,6 @@ mod tests {
openfut_server_host: "10.10.0.120".into(),
fut_persona_id: 1,
fut_persona_name: "X".into(),
fifa17_tools_dir: "/tmp/tools".into(),
fifa17_python: "/usr/bin/python3".into(),
..LauncherConfig::default()
};
c.game_launch_command.clear();
@@ -609,8 +537,6 @@ mod tests {
openfut_server_host: "10.10.0.120".into(),
fut_persona_id: 1,
fut_persona_name: "X".into(),
fifa17_tools_dir: "/tmp/tools".into(),
fifa17_python: "/usr/bin/python3".into(),
game_launch_command: "/home/u/launch.sh".into(),
..LauncherConfig::default()
};
-2
View File
@@ -481,8 +481,6 @@ impl LaunchOps for RealOps {
fn ensure_service(&mut self, service: Service) -> Result<Ensured, String> {
let spec = SpawnSpec {
python: self.config.fifa17_python.clone(),
tools_dir: self.config.fifa17_tools_dir.clone(),
persona_id: self.config.fut_persona_id,
persona_name: self.config.fut_persona_name.clone(),
// Only autopatch advertises the verified resolver guard, so only it
+103 -69
View File
@@ -16,7 +16,7 @@
use parking_lot::Mutex;
use std::{
net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener},
path::Path,
path::{Path, PathBuf},
process::{Child, Command, Stdio},
sync::{mpsc, Arc},
time::{Duration, Instant},
@@ -56,25 +56,48 @@ impl Service {
}
}
/// The responder script filename inside the tools dir.
fn script(self) -> &'static str {
/// The companion's executable name.
///
/// These were Python responder scripts run through a configured interpreter. They
/// are now Rust binaries built from this workspace (`openfut-lsx`,
/// `openfut-autopatch`), which removes the interpreter and the tools directory
/// from the launch contract entirely: no `python3` to locate, no script path to
/// configure, and no chance of running a stale checkout's copy.
fn binary(self) -> &'static str {
match self {
Service::Lsx => "lsx_responder_v2.py",
Service::Autopatch => "autopatch.py",
Service::Lsx => "openfut-lsx",
Service::Autopatch => "openfut-autopatch",
}
}
}
fn command_parts(service: Service, python: &str, tools_dir: &Path) -> CommandParts {
let mut args = vec![tools_dir
.join(service.script())
.to_string_lossy()
.into_owned()];
/// Absolute path to a companion binary.
///
/// Prefers a sibling of the running launcher, which is what a workspace build and any
/// sane install layout both produce, and falls back to the bare name so a
/// PATH-installed binary still works. Returning the bare name rather than failing
/// keeps `spawn` responsible for reporting a missing binary, with one error message
/// instead of two.
fn resolve_binary(service: Service) -> PathBuf {
let name = service.binary();
if let Some(dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(Path::to_path_buf)) {
let sibling = dir.join(name);
if sibling.is_file() {
return sibling;
}
}
PathBuf::from(name)
}
fn command_parts(service: Service) -> CommandParts {
let mut args = Vec::new();
if service == Service::Autopatch {
// autopatch exits when the launcher does, so it cannot outlive its owner and
// keep writing to a client the launcher no longer manages.
args.extend(["--launcher-pid".to_string(), std::process::id().to_string()]);
}
CommandParts {
program: python.to_string(),
program: resolve_binary(service).to_string_lossy().into_owned(),
args,
}
}
@@ -265,14 +288,17 @@ pub fn lsx_port_busy() -> bool {
}
}
/// PID of a process running `service`'s responder script that this launcher does
/// PID of a process running `service`'s companion binary that this launcher does
/// not own, if there is one.
///
/// Scans `/proc` — no extra dependency, no privilege, and no guessing: a service
/// left running by a previous launcher instance or started by hand from a shell
/// is a real state the UI has to be able to report, and cleanup has to respect.
///
/// Matches argv entries rather than `comm`, because `comm` is truncated to 15
/// characters by the kernel and would misreport these names.
pub fn foreign_pid(service: Service, ours: Option<u32>) -> Option<u32> {
let script = service.script();
let binary = service.binary();
let self_pid = std::process::id();
let entries = std::fs::read_dir("/proc").ok()?;
for entry in entries.flatten() {
@@ -285,10 +311,13 @@ pub fn foreign_pid(service: Service, ours: Option<u32>) -> Option<u32> {
let Ok(cmdline) = std::fs::read(entry.path().join("cmdline")) else {
continue;
};
if cmdline
.split(|b| *b == 0)
.any(|arg| String::from_utf8_lossy(arg).ends_with(script))
{
if cmdline.split(|b| *b == 0).any(|arg| {
// Compare the file name, so `/path/to/openfut-lsx` matches while an
// unrelated argument that merely ends with the same text does not.
Path::new(&*String::from_utf8_lossy(arg))
.file_name()
.is_some_and(|n| n == binary)
}) {
return Some(pid);
}
}
@@ -420,8 +449,6 @@ impl ServiceSupervisor {
}
let child = spawn(
service,
&spec.python,
&spec.tools_dir,
spec.persona_id,
&spec.persona_name,
spec.capability,
@@ -450,24 +477,22 @@ impl ServiceSupervisor {
/// Everything [`spawn`] needs, bundled so the launch sequence can hand it over
/// as one value per service.
pub struct SpawnSpec {
pub python: String,
pub tools_dir: String,
pub persona_id: u64,
pub persona_name: String,
pub capability: Option<CapabilityWiring>,
}
/// Spawn a companion service. `python` is the interpreter, `tools_dir` the
/// directory holding the responder scripts. Streams stdout+stderr into `log`.
/// Returns an error (without spawning) if the tools dir or script is missing.
/// Spawn a companion service and stream its stdout+stderr into `log`.
///
/// Returns an error without spawning if the binary is missing, which is the only
/// precondition left now that the companions are workspace binaries rather than
/// Python scripts run from a configured tools directory.
///
/// `capability` is the backend-registration wiring + shared per-FIFA-process
/// capability sink — `Some(..)` for autopatch (whose stdout advertises the
/// verified resolver guard) and `None` for LSX.
pub fn spawn(
service: Service,
python: &str,
tools_dir: &str,
persona_id: u64,
persona_name: &str,
capability: Option<CapabilityWiring>,
@@ -475,35 +500,28 @@ pub fn spawn(
) -> anyhow::Result<Child> {
use std::io::{BufRead, BufReader};
let dir = Path::new(tools_dir);
if !dir.is_dir() {
anyhow::bail!(
"FIFA 17 tools dir not found: {} (set it in Settings)",
dir.display()
);
}
let script_path = dir.join(service.script());
if !script_path.exists() {
anyhow::bail!(
"{} not found in tools dir: {}",
service.script(),
script_path.display()
);
}
let label = service.label();
let parts = command_parts(service);
let program = Path::new(&parts.program);
// Only a resolved absolute path can be checked up front; a bare name is left to
// the OS to resolve through PATH, and a failure there is reported by spawn below.
if program.is_absolute() && !program.is_file() {
anyhow::bail!(
"{label} binary not found: {} — build the workspace so it sits beside the launcher",
program.display()
);
}
// Both services use the configured interpreter and absolute script path;
// neither invents a Python installation path. Autopatch receives launcher
// ownership and a per-user runtime log so stale root-owned /tmp files cannot
// block startup.
let parts = command_parts(service, python, dir);
let mut cmd = Command::new(&parts.program);
cmd.args(&parts.args);
if service == Service::Lsx {
// The persona LSX reports has to equal what Blaze returns in
// LoginResponse.SESS.PDTL and what UTAS serves as userInfo.personaId; the
// constraint is cross-layer agreement, not any particular value.
cmd.env("FUT_PERSONA_ID", persona_id.to_string())
.env("FUT_PERSONA_NAME", persona_name);
} else if service == Service::Autopatch {
// A per-user runtime log, so a stale root-owned /tmp file cannot block startup.
let log_path = std::env::var_os("XDG_RUNTIME_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(std::env::temp_dir)
@@ -512,19 +530,24 @@ pub fn spawn(
}
// Put each companion in its own process group for lifecycle isolation.
cmd.process_group(0);
cmd.current_dir(dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
log.lock().push(format!(
"[launcher] starting {label}: {} {}",
python,
script_path.display(),
"[launcher] starting {label}: {}{}",
parts.program,
parts
.args
.iter()
.fold(String::new(), |mut acc, a| {
acc.push(' ');
acc.push_str(a);
acc
}),
));
let mut child = cmd
.spawn()
.map_err(|e| anyhow::anyhow!("failed to start {label} ({}): {e}", service.script()))?;
.map_err(|e| anyhow::anyhow!("failed to start {label} ({}): {e}", service.binary()))?;
if let Some(out) = child.stdout.take() {
let buf = Arc::clone(&log);
@@ -606,27 +629,38 @@ mod tests {
use super::*;
#[test]
fn lsx_runs_python_directly() {
let parts = command_parts(Service::Lsx, "/usr/bin/python3", Path::new("/tmp/tools"));
assert_eq!(parts.program, "/usr/bin/python3");
assert_eq!(parts.args, vec!["/tmp/tools/lsx_responder_v2.py"]);
fn lsx_runs_its_own_binary_with_no_arguments() {
let parts = command_parts(Service::Lsx);
assert_eq!(
Path::new(&parts.program).file_name().unwrap(),
"openfut-lsx"
);
assert!(parts.args.is_empty(), "{:?}", parts.args);
}
#[test]
fn autopatch_runs_python_directly_with_launcher_ownership() {
let parts = command_parts(
Service::Autopatch,
"/usr/bin/python3",
Path::new("/tmp/tools"),
fn autopatch_runs_its_own_binary_with_launcher_ownership() {
let parts = command_parts(Service::Autopatch);
assert_eq!(
Path::new(&parts.program).file_name().unwrap(),
"openfut-autopatch"
);
assert_eq!(parts.program, "/usr/bin/python3");
// The launcher pid is how autopatch learns to exit with its owner.
assert_eq!(
parts.args,
vec![
"/tmp/tools/autopatch.py",
"--launcher-pid",
&std::process::id().to_string(),
]
vec!["--launcher-pid", &std::process::id().to_string()]
);
}
#[test]
fn a_companion_binary_is_looked_up_by_file_name_not_a_suffix_match() {
// Guards the foreign-process scan: an argv entry that merely ends with the
// binary name (a log path, say) must not be mistaken for the service.
assert_eq!(Service::Lsx.binary(), "openfut-lsx");
assert_eq!(Service::Autopatch.binary(), "openfut-autopatch");
assert_eq!(
Path::new("/var/log/my-openfut-lsx").file_name().unwrap(),
"my-openfut-lsx"
);
}
+17 -31
View File
@@ -88,7 +88,7 @@ impl Check {
/// Run every applicable check. Order is the order the game exercises them.
pub fn run(cfg: &LauncherConfig) -> Vec<Check> {
vec![
ptrace_scope(cfg),
ptrace_scope(),
ea_redirect(cfg),
hostname_mapping(cfg),
backend_reachable(cfg),
@@ -109,16 +109,12 @@ pub fn warnings(checks: &[Check]) -> usize {
/// autopatch writes to FIFA's process memory; Yama blocks that unless
/// `ptrace_scope` is 0. At 1 the patch silently does nothing and the game fails
/// its TLS handshake much later, with no message naming the cause.
fn ptrace_scope(cfg: &LauncherConfig) -> Check {
///
/// Unconditional. autopatch is a workspace binary that ships alongside the
/// launcher, so there is no configuration that could make this inapplicable —
/// every launch runs it.
fn ptrace_scope() -> Check {
const NAME: &str = "ptrace_scope (autopatch)";
// `fifa17_tools_dir` carries a conventional default, so a non-empty value
// does not mean the tools are installed. Key off the directory actually
// existing: that is what decides whether autopatch will run at all, and it
// keeps this from failing on a machine that never uses local services.
let tools = cfg.fifa17_tools_dir.trim();
if tools.is_empty() || !std::path::Path::new(tools).is_dir() {
return Check::skip(NAME, "no local services installed");
}
match std::fs::read_to_string(PTRACE_SCOPE) {
Ok(v) => ptrace_verdict(&v),
// Not every kernel has Yama. Absent means unenforced, which is what we want.
@@ -336,12 +332,19 @@ mod tests {
fn an_unconfigured_launcher_skips_rather_than_passes() {
// The distinction that matters: a fresh config must not display a column
// of green ticks. "Not checked" is not "checked and fine".
//
// `ptrace_scope` is excluded because it is no longer configuration
// dependent: it reads this machine's Yama setting and reports a real
// verdict either way. `only_ptrace_scope_zero_lets_autopatch_work`
// covers it.
let mut c = cfg();
// `default()` points these at conventional paths whose existence varies
// by machine. Pin them so the assertion is about the code, not this box.
c.fifa17_tools_dir = "/nonexistent/openfut-tools".into();
// `default()` points this at a conventional path whose existence varies
// by machine. Pin it so the assertion is about the code, not this box.
c.fifa_game_dir = "/nonexistent/fifa-game-dir".into();
let checks = run(&c);
let checks: Vec<Check> = run(&c)
.into_iter()
.filter(|k| k.name != "ptrace_scope (autopatch)")
.collect();
assert!(
checks.iter().all(|k| k.state == State::Skipped),
"{checks:#?}"
@@ -360,16 +363,6 @@ mod tests {
assert!(ptrace_verdict("1").detail.contains("Arm client"));
}
#[test]
fn ptrace_is_skipped_when_the_tools_dir_does_not_exist() {
// Regression: the gate used to be "is the field non-empty", and the
// field has a default — so this check ran (and failed) on machines that
// never use autopatch at all.
let mut c = cfg();
c.fifa17_tools_dir = "/nonexistent/openfut-tools".into();
assert_eq!(ptrace_scope(&c).state, State::Skipped);
}
#[test]
fn a_malformed_probe_ip_fails_loudly_instead_of_being_skipped() {
let mut c = cfg();
@@ -432,13 +425,6 @@ mod tests {
assert_eq!(hostname_mapping(&c).state, State::Pass);
}
#[test]
fn ptrace_check_is_skipped_when_local_services_are_not_configured() {
let mut c = cfg();
c.fifa17_tools_dir.clear();
assert_eq!(ptrace_scope(&c).state, State::Skipped);
}
#[test]
fn a_dead_backend_port_is_reported_as_a_failure() {
let mut c = cfg();