Merge pull request #2116 from kacpherek/fix/2095-signal-save

🤖 Agent: Read against the vanilla 26.3 sources (Save the world on SIGTERM, Ctrl+C and SIGHUP) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
This commit is contained in:
Ludovic Marie
2026-10-27 11:12:54 +01:00
committed by GitHub
2 changed files with 54 additions and 0 deletions
+22
View File
@@ -8772,6 +8772,10 @@ impl Game<'_> {
}
self.lan_close(false);
self.save_game()?;
if let Some(mut remote) = self.remote.take() {
// Minecraft.disconnect closes the connection, so the host sees a clean leave.
remote.session.disconnect();
}
self.saved_for_exit = true;
Ok(())
}
@@ -12773,6 +12777,16 @@ impl ApplicationHandler for Game<'_> {
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
if horizon_threads::stop_requests() > 0
&& self.pending_save.as_ref().is_none_or(|save| !save.quit)
{
// SIGTERM, SIGINT or SIGHUP: the shutdown hook's server halt, as a window close.
if let Err(error) = self.begin_closing_save(true) {
self.error = Some(error);
event_loop.exit();
return;
}
}
if self.pending_save.is_some() {
match self.poll_closing_save() {
Ok(true) => event_loop.exit(),
@@ -14263,6 +14277,11 @@ impl Loading {
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
if horizon_threads::stop_requests() > 0 {
// No world is open yet: nothing to save.
event_loop.exit();
return;
}
if let Err(error) = self.poll_menu_resources() {
self.error = Some(error);
event_loop.exit();
@@ -14540,6 +14559,9 @@ pub fn run() -> Result<()> {
{
horizon_client::heartbeat::start();
}
if options.execution_role.is_client() {
horizon_threads::catch_stop_signals();
}
let started = Instant::now();
horizon_client::spans::enable(options.spikes.is_some());
let vanilla_root = options.resources.clone();
+32
View File
@@ -163,6 +163,38 @@ fn idle() -> bool {
false
}
/// How many stop requests (Ctrl-C, SIGTERM, SIGHUP) arrived since `catch_stop_signals`.
pub fn stop_requests() -> u32 {
STOP_REQUESTS.load(std::sync::atomic::Ordering::SeqCst)
}
static STOP_REQUESTS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
/// Count stop requests from outside instead of dying on them: vanilla's "Client Shutdown Thread"
/// (`Main.java`) halts the integrated server when the JVM shuts down, and the JVM does that on
/// these signals, so a killed game still saves. The caller's event loop turns the first request
/// into a normal quit; a second one (the save is stuck) ends the process at once.
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
pub fn catch_stop_signals() {
extern "C" fn handle(_: libc::c_int) {
if STOP_REQUESTS.fetch_add(1, Ordering::SeqCst) >= 1 {
// SAFETY: _exit is async-signal-safe; 130 is the shell's code for a Ctrl-C death.
unsafe { libc::_exit(130) }
}
}
let handler = handle as extern "C" fn(libc::c_int);
for signal in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP] {
// SAFETY: the handler only touches an atomic and calls _exit, both async-signal-safe.
unsafe {
libc::signal(signal, handler as libc::sighandler_t);
}
}
}
/// Windows and the other systems keep their default: a closed window is a `CloseRequested`.
#[cfg(not(any(target_vendor = "apple", target_os = "linux", target_os = "android")))]
pub fn catch_stop_signals() {}
/// Give back to the OS the memory the allocator holds free, after something
/// large was dropped: glibc keeps freed pages in the arena of the thread that
/// allocated them, and macOS in its zones until memory runs short. Nothing