diff --git a/payload/user/remote_access/pager-webui/server.py b/payload/user/remote_access/pager-webui/server.py index f6173f1..90ac91c 100644 --- a/payload/user/remote_access/pager-webui/server.py +++ b/payload/user/remote_access/pager-webui/server.py @@ -4894,6 +4894,21 @@ def h_profile_save(ctx): return (200, {'ok': True}) if ok else (502, {'ok': False}) +def _post_restore_converge(): + """After any profile restore: re-apply invariants the restored config may + not carry. Factory-default configs enable dummy_radio0 (the phy0 + channel-pinner) and may drop monitors; park and re-raise best-effort. + Uses ungated primitives — the restore is already inside its own gate.""" + try: + _park_dummy_sta() + except Exception: + pass + try: + _raise_monitors() + except Exception: + pass + + def h_profile_restore(ctx): import mk8_events import mk8_gate @@ -4911,6 +4926,7 @@ def h_profile_restore(ctx): ok = bool(result.get('ok')) if ok: device_run(['wifi', 'reload'], timeout=45) + _post_restore_converge() try: mk8_events.log_event('profile_restore', sev='info' if ok else 'warn', @@ -5001,6 +5017,16 @@ def _sta_uplink_enabled(): return cfg.get('mode') == 'sta' and cfg.get('disabled') != '1' +def _park_dummy_sta(): + """Ungated primitive: disable dummy_radio0 + take wlan0 down. Callers + that already run inside a gate (post-restore convergence) use this to + avoid spawning a nested watchdog.""" + device_run(['uci', 'set', 'wireless.dummy_radio0.disabled=1']) + device_run(['uci', 'commit', 'wireless']) + for iface in ('wlan0',): + rc, out, err = device_run(['ip', 'link', 'set', iface, 'down'], timeout=10) + + def _disable_sta_uplink(): """Disable the dummy_radio0 STA without bouncing the radios. The UCI flag keeps it off across reboots/wifi reloads; taking wlan0 down immediately @@ -5008,10 +5034,7 @@ def _disable_sta_uplink(): tears down live APs and drops the monitors mid-assessment.""" import mk8_gate mk8_gate.enter('uplink_disable') - device_run(['uci', 'set', 'wireless.dummy_radio0.disabled=1']) - device_run(['uci', 'commit', 'wireless']) - for iface in ('wlan0',): - rc, out, err = device_run(['ip', 'link', 'set', iface, 'down'], timeout=10) + _park_dummy_sta() def env_check(): @@ -7402,6 +7425,7 @@ if __name__ == '__main__': import mk8_profiles result = mk8_profiles.restore(name) device_run(['wifi', 'reload'], timeout=90) + _post_restore_converge() import mk8_events mk8_events.log_event('rollback', sev='warn', msg='watchdog restored %s' % name, diff --git a/tests/test_reliability_api.py b/tests/test_reliability_api.py index 304e54d..8241bd6 100644 --- a/tests/test_reliability_api.py +++ b/tests/test_reliability_api.py @@ -208,6 +208,17 @@ class ReliabilityApiTest(unittest.TestCase): self.assertEqual(calls['restored'], 'p1') reloads = [r for r in self.runs if r[0][:2] == ['wifi', 'reload']] self.assertEqual(len(reloads), 1) + # post-restore convergence parks the factory-enabled dummy STA + parked = [r for r in self.runs + if r[0][:3] == ['uci', 'set', + 'wireless.dummy_radio0.disabled=1']] + self.assertEqual(len(parked), 1) + downs = [r for r in self.runs if r[0][:4] == ['ip', 'link', + 'set', 'wlan0']] + self.assertTrue(downs) + raises = [r for r in self.runs if r[0][:4] == ['ip', 'link', + 'set', 'wlan0mon']] + self.assertTrue(raises) self.assertEqual(calls['events'][-1][0], 'profile_restore') status, result = server.h_profile_restore(