From adfe8f784f7a800637a898bc05d374cb572cb99b Mon Sep 17 00:00:00 2001 From: c4ch3c4d3 Date: Tue, 18 Aug 2026 20:26:16 -0500 Subject: [PATCH] fix: health monitor detects down monitors via ip link flags + disables wlan2mon operstate reports 'unknown' on monitors (normal), so _iface_up now parses admin flags from ip link. Discovered a second pineapd SIGSEGV source: the wlan2mon 6GHz monitor this hardware never creates, hopping on the missing iface (~85s crash cadence even with the SSID pool off). The monitor now disables it in the fix path. --- .../user/remote_access/pager-webui/server.py | 35 +++++++++++++++---- tests/test_health.py | 15 ++++---- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/payload/user/remote_access/pager-webui/server.py b/payload/user/remote_access/pager-webui/server.py index 58a3538..a5ae128 100644 --- a/payload/user/remote_access/pager-webui/server.py +++ b/payload/user/remote_access/pager-webui/server.py @@ -3341,9 +3341,20 @@ def _sigsegv_count(): return out.count('SIGSEGV') +def _iface_up(name): + """True when the interface's admin flags contain UP. Monitor interfaces + report operstate 'unknown' even when usable, so parse `ip link` flags.""" + rc, out, err = device_run(['ip', 'link', 'show', name], timeout=10) + if rc != 0: + return False + m = re.search(r'<([^>]+)>', out or '') + if not m: + return False + return 'UP' in m.group(1).split(',') + + def _monitor_down(name): - rc, out, err = device_run(['iw', 'dev'], timeout=10) - return name not in out + return not _iface_up(name) def health_check(): @@ -3366,11 +3377,23 @@ def health_check(): device_run(['uci', 'set', 'pineapd.@ssidpool[0].disable=1']) device_run(['uci', 'commit', 'pineapd']) h['last_action'] = 'SSID pool broadcast disabled (pineapd crash-loop fix)' - elif _monitor_down('wlan1mon'): - device_run(['ip', 'link', 'set', 'wlan1mon', 'up'], timeout=10) - h['last_action'] = 'wlan1mon brought up' + elif _monitor_down('wlan1mon') or _monitor_down('wlan0mon'): + for name in ('wlan1mon', 'wlan0mon'): + if _monitor_down(name): + device_run(['ip', 'link', 'set', name, 'up'], timeout=10) + h['last_action'] = 'monitor interfaces brought up' else: - h['last_action'] = 'pineapd restart' + # wlan2mon is a 6GHz monitor this hardware never materializes; with + # hop enabled pineapd crashes on the missing iface (second SIGSEGV + # source after the SSID pool). Keep it disabled. + wlan2 = _uci_section('pineapd.wlan2mon') + if wlan2.get('disable') != '1': + device_run(['uci', 'set', 'pineapd.wlan2mon.disable=1']) + device_run(['uci', 'set', 'pineapd.wlan2mon.hop=0']) + device_run(['uci', 'commit', 'pineapd']) + h['last_action'] = 'wlan2mon disabled (missing 6GHz iface crash source)' + else: + h['last_action'] = 'pineapd restart' device_run(['/etc/init.d/pineapd', 'restart'], timeout=30) h['sigsegv_last'] = _sigsegv_count() h['last_fix'] = now diff --git a/tests/test_health.py b/tests/test_health.py index dd2b102..51c7184 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -15,11 +15,13 @@ class HealthCheckTest(unittest.TestCase): self.runs = [] self.ping_ok = True self.sigsegvs = 0 - self.iw_out = 'wlan0mon\nwlan1mon\n' + self.iface_up = {'wlan0mon': True, 'wlan1mon': True} self.uci_state = {} server._health.update({ 'sigsegv_last': None, 'last_fix': 0.0, 'fixes': 0, 'last_action': None, 'pineap_up': False}) + self.old_iface_up = server._iface_up + server._iface_up = lambda name: self.iface_up.get(name, True) def fake_run(args, timeout=20, input_data=None): self.runs.append((list(args), timeout)) @@ -30,8 +32,6 @@ class HealthCheckTest(unittest.TestCase): return (1, '', 'could not connect to pineap: connection refused') if a[0] == 'logread': return (0, 'SIGSEGV\n' * self.sigsegs if hasattr(self, 'sigsegs') else '', '') - if a[0] == 'iw': - return (0, self.iw_out, '') if a[:2] == ['uci', 'set']: k, _, v = a[2].partition('=') self.uci_state[k] = v @@ -45,6 +45,9 @@ class HealthCheckTest(unittest.TestCase): server.device_run = fake_run + def tearDown(self): + server._iface_up = self.old_iface_up + def test_pineap_up_reports_no_action(self): result = server.health_check() self.assertTrue(result['pineap_up']) @@ -66,12 +69,12 @@ class HealthCheckTest(unittest.TestCase): self.assertEqual(result['last_action'], 'pineapd restart') self.assertIn(['/etc/init.d/pineapd', 'restart'], [r[0] for r in self.runs]) - def test_down_without_crash_brings_monitor_up(self): + def test_down_without_crash_brings_monitors_up(self): self.ping_ok = False self.uci_state['pineapd.@ssidpool[0].disable'] = '1' - self.iw_out = 'wlan0mon\n' + self.iface_up = {'wlan0mon': True, 'wlan1mon': False} result = server.health_check() - self.assertEqual(result['last_action'], 'wlan1mon brought up') + self.assertEqual(result['last_action'], 'monitor interfaces brought up') self.assertIn(['ip', 'link', 'set', 'wlan1mon', 'up'], [r[0] for r in self.runs]) def test_down_disables_pool_regardless_of_sigsegv_history(self):