"""Boot-time reconciliation of crash-prone PineAP settings.""" import time from server import (_apply_uci_wanted, _monitor_down, _raise_monitors, PINEAPD_SAFE_UCI, device_run) WANTED_EXTRA = {'pineapd.@pineapd[0].autossidpool': '0'} POOL_CLEAR_MAX = 20 MONITORS = ('wlan0mon', 'wlan1mon') # pineapd.wlan1mon.hop is intentionally NOT part of the applied set: channel # hopping is owned by the RF role manager (_pause_hop/_resume_hop). Hop=1 is # the healthy recon baseline, so reconciling it here would silently disable a # pager-enabled setting at every boot and latch GUARD PENDING after any # attack-role switch. GR_TTL_SECONDS = 30 _GR_CACHE = {'t': 0.0, 'data': None} def _wanted(): wanted = {k: v for k, v in PINEAPD_SAFE_UCI.items() if k != 'pineapd.wlan1mon.hop'} wanted.update(WANTED_EXTRA) return wanted def _pool_size(): rc, out, err = device_run( ['uci', 'get', 'pineapd.@ssidpool[0].ssid']) if rc != 0 or not (out or '').strip(): return 0 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. Probes SECTION existence (`uci -q show @pineapd[0]`) — never an option, which may legitimately be absent from a rewritten section.""" rc, out, err = device_run(['uci', '-q', 'show', 'pineapd.@pineapd[0]']) if rc == 0: return False device_run(['uci', 'add', 'pineapd', 'pineapd']) device_run(['uci', 'commit', 'pineapd']) return True def reconcile(clear_pool=True): 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']) pool_cleared = True if changed or pool_cleared: device_run(['uci', 'commit', 'pineapd']) raised = _raise_monitors() if any(_monitor_down(m) for m in MONITORS) else [] _GR_CACHE['data'] = None return {'changed': changed, 'pool_cleared': pool_cleared, 'monitors_raised': raised} def guard_report(): now = time.time() cached = _GR_CACHE['data'] 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()} _GR_CACHE['t'] = now _GR_CACHE['data'] = report return report