From 1133068a09fb58aff3dcd559b18482e09a772aa2 Mon Sep 17 00:00:00 2001 From: c4ch3c4d3 Date: Sat, 22 Aug 2026 18:45:44 -0600 Subject: [PATCH] fix(reliability): recreate missing pineapd section; honest uci-set reporting --- .../remote_access/pager-webui/mk8_guard.py | 19 +++++++- tests/test_mk8_guard.py | 47 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/payload/user/remote_access/pager-webui/mk8_guard.py b/payload/user/remote_access/pager-webui/mk8_guard.py index d8aef14..9a564b7 100644 --- a/payload/user/remote_access/pager-webui/mk8_guard.py +++ b/payload/user/remote_access/pager-webui/mk8_guard.py @@ -31,8 +31,24 @@ def _pool_size(): return len(out.split()) +def _ensure_pineapd_section(): + """Stock daemon rewrites and profile restores can drop the whole + `config pineapd` section; every @pineapd[0] option write fails with + 'Invalid argument' until it exists again.""" + rc, out, err = device_run(['uci', '-q', 'get', + 'pineapd.@pineapd[0].logrecon']) + if rc == 0: + return False + device_run(['uci', 'add', 'pineapd', 'pineapd']) + device_run(['uci', 'commit', 'pineapd']) + return True + + def reconcile(clear_pool=True): - changed = _apply_uci_wanted(_wanted()) + changed = [] + if _ensure_pineapd_section(): + changed.append('pineapd.@pineapd[0] (section recreated)') + changed += _apply_uci_wanted(_wanted()) pool_cleared = False if clear_pool and _pool_size() > POOL_CLEAR_MAX: device_run(['uci', 'delete', 'pineapd.@ssidpool[0].ssid']) @@ -51,6 +67,7 @@ def guard_report(): if cached is not None and now - _GR_CACHE['t'] < GR_TTL_SECONDS: return cached from server import _pending_uci + _ensure_pineapd_section() pending = _pending_uci(_wanted()) report = {'in_sync': not pending, 'pending': pending, 'pool_size': _pool_size()} diff --git a/tests/test_mk8_guard.py b/tests/test_mk8_guard.py index 93eb1bd..55d254f 100644 --- a/tests/test_mk8_guard.py +++ b/tests/test_mk8_guard.py @@ -96,3 +96,50 @@ class GuardTest(unittest.TestCase): if __name__ == '__main__': unittest.main() + + +class SectionRecreateTest(unittest.TestCase): + def setUp(self): + self.calls = [] + self.uci = {} + self.old_server_run = server.device_run + self.old_iface = server._iface_up + server._iface_up = lambda name: True + + def fake_run(args, timeout=20, input_data=None): + a = list(args) + self.calls.append(a) + if a[:4] == ['uci', '-q', 'get', + 'pineapd.@pineapd[0].logrecon']: + return (1, '', '') # section missing + if a[:2] == ['uci', 'get']: + got = self.uci.get(a[2]) + return (0, (got if got is not None else '') + '\n', '') + if a[:2] == ['uci', 'set']: + k, _, v = a[2].partition('=') + self.uci[k] = v + return (0, '', '') + + def dual_run(args, timeout=20, input_data=None): + # install the same fake for server-side helpers + self.__dict__.setdefault('_srv', server) + return fake_run(args, timeout=timeout, input_data=input_data) + + mk8_guard.device_run = fake_run + server.device_run = fake_run + + def tearDown(self): + server.device_run = self.old_server_run + server._iface_up = self.old_iface + + def test_recreates_missing_pineapd_section(self): + result = mk8_guard.reconcile(clear_pool=False) + self.assertIn(['uci', 'add', 'pineapd', 'pineapd'], self.calls) + self.assertTrue(any('section recreated' in c for c in result['changed'])) + sets = [c for c in self.calls if c[:2] == ['uci', 'set']] + self.assertEqual(len(sets), + len(server.PINEAPD_SAFE_UCI) - 1 + + len(mk8_guard.WANTED_EXTRA)) + +if __name__ == '__main__': + unittest.main()