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:
+76
-104
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user