From ba3e1b1ae0a4e1ca9d47cbc0587794cba7001dd2 Mon Sep 17 00:00:00 2001 From: c4ch3c4d3 Date: Sat, 22 Aug 2026 16:51:33 -0600 Subject: [PATCH] fix(reliability): clear boot marker on graceful shutdown (I1) --- .../user/remote_access/pager-webui/server.py | 15 +++++++ tests/test_reliability_api.py | 41 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/payload/user/remote_access/pager-webui/server.py b/payload/user/remote_access/pager-webui/server.py index 1bf2780..9fbd110 100644 --- a/payload/user/remote_access/pager-webui/server.py +++ b/payload/user/remote_access/pager-webui/server.py @@ -5106,6 +5106,17 @@ def check_boot_marker(): return False +def _clear_boot_marker(): + """Best-effort: a clean shutdown must not read as an unexpected reboot.""" + marker = globals().get('BOOT_MARKER') + if not marker: + return + try: + os.unlink(marker) + except Exception: + pass + + def startup_env_check(attempts=STARTUP_CHECK_ATTEMPTS, delay=STARTUP_CHECK_DELAY): """Run and print the startup contract, allowing boot dependencies time. @@ -7143,6 +7154,10 @@ def _request_shutdown(signum=None, frame=None): LIVE_STOP.set() HEALTH_STOP.set() _recon_hopper_stop.set() + try: + _clear_boot_marker() + except Exception: + pass def _uci_iface_present(name): diff --git a/tests/test_reliability_api.py b/tests/test_reliability_api.py index 9d8307c..3eeb9eb 100644 --- a/tests/test_reliability_api.py +++ b/tests/test_reliability_api.py @@ -63,6 +63,47 @@ class ReliabilityApiTest(unittest.TestCase): os.unlink(marker) server.BOOT_MARKER = old + def test_clean_cycle_boot_shutdown_next_boot_not_unexpected(self): + import tempfile + import threading + import mk8_events as events_mod + marker = tempfile.mktemp() + old = (server.BOOT_MARKER, server.LIVE_STOP, server.HEALTH_STOP, + server._recon_hopper_stop, events_mod.mark_boot) + stops = (threading.Event(), threading.Event(), threading.Event()) + server.BOOT_MARKER = marker + server.LIVE_STOP, server.HEALTH_STOP, \ + server._recon_hopper_stop = stops + booted = [] + events_mod.mark_boot = \ + lambda unexpected=False: booted.append(unexpected) + try: + # Simulate a previous run's marker left behind: boot is unexpected. + open(marker, 'w').write('0') + self.assertTrue(server.check_boot_marker()) + self.assertEqual(booted, [True]) + # Clean shutdown clears the marker... + self.assertTrue(os.path.exists(marker)) + server._request_shutdown() + self.assertFalse(os.path.exists(marker), + 'graceful shutdown must clear the boot marker') + for ev in stops: + self.assertTrue(ev.is_set()) + # ...so the next boot is clean and re-arms the marker. + self.assertFalse(server.check_boot_marker()) + self.assertEqual(booted[-1], False) + self.assertTrue(os.path.exists(marker)) + # _clear_boot_marker is best-effort on missing/None markers. + server._clear_boot_marker() + self.assertFalse(os.path.exists(marker)) + server.BOOT_MARKER = None + server._clear_boot_marker() + finally: + (server.BOOT_MARKER, server.LIVE_STOP, server.HEALTH_STOP, + server._recon_hopper_stop, events_mod.mark_boot) = old + if os.path.exists(marker): + os.unlink(marker) + def test_profile_routes_registered(self): handler, _ = server.ROUTER.dispatch('GET', '/api/reliability/profiles')