using System.Runtime.InteropServices; namespace Aeshnidae.GracefulStop; /// /// Makes `systemctl stop|restart ace` graceful. /// /// Found 2026-09-27: the units stop ACE with SIGINT, and ACE registers no handler for it, so /// .NET simply ends the process. ACE's ProcessExit handler then only warns "Unsafe server /// shutdown detected! Data loss is possible!" and stops the database - unless the process /// runs with DOTNET_RUNNING_IN_CONTAINER, which would also move ACE's config, content and /// database-update paths to /ace/..., so that flag is not the fix. No player was logged out /// and saved, no landblock saved, no mod shutdown hook ran; live's 15:58 restart that day /// went down and came back inside one second. /// /// This mod catches SIGINT and SIGTERM, cancels the default exit, and runs ACE's own /// shutdown instead - ServerManager.DoShutdownNow, what an admin's "/shutdown now" does: every /// player logged off (and saved), every landblock unloaded (and saved), the world stopped, /// mods shut down, offline players saved, the database queue drained, then Environment.Exit. /// A second signal while that runs is ignored; systemd's TimeoutStopSec is the backstop. /// public class Mod : IHarmonyMod { public static readonly string Name = typeof(Mod).Assembly.GetName().Name!; private static PosixSignalRegistration? _sigint, _sigterm; private static int _started; private bool _disposed; public void Initialize() { try { _sigint = PosixSignalRegistration.Create(PosixSignal.SIGINT, OnSignal); _sigterm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, OnSignal); ModManager.Log($"[{Name}] ready - SIGINT and SIGTERM now run ACE's graceful shutdown"); } catch (Exception ex) { // Never throw out of Initialize; without the handler ACE behaves as it always has. ModManager.Log($"[{Name}] could not register the signal handlers ({ex.Message}); a stop is still an instant exit", ModManager.LogLevel.Error); } } private static void OnSignal(PosixSignalContext context) { // Keep the process alive: ACE's shutdown ends it with Environment.Exit when it is done. context.Cancel = true; if (Interlocked.Exchange(ref _started, 1) == 1) { ModManager.Log($"[{Name}] {context.Signal} again - the graceful shutdown is already running"); return; } ModManager.Log($"[{Name}] {context.Signal} received - logging everyone out and saving before exit"); // Off the signal thread: the shutdown waits for players, landblocks and the database. var thread = new Thread(() => { try { ServerManager.DoShutdownNow(); } catch (Exception ex) { ModManager.Log($"[{Name}] graceful shutdown failed: {ex} - exiting", ModManager.LogLevel.Error); Environment.Exit(1); } }) { Name = "GracefulStop shutdown", IsBackground = false, }; thread.Start(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // Mods are shut down part way through the graceful shutdown itself; dropping the // handlers then is harmless, the shutdown thread carries on regardless. _sigint?.Dispose(); _sigterm?.Dispose(); _sigint = _sigterm = null; ModManager.Log($"[{Name}] shut down"); } _disposed = true; } }