fix: non-disruptive startup checks + clean service lifecycle (v1.3.1)
- Env check is read-only when state is sane: no pineapd command-socket writes, no live pool-list commits, no wifi reload; pineapd restarts only when a runtime-sensitive UCI value changed or the daemon was down - Failed monitor repairs now fail the startup contract instead of being reported as fixed; runtime pool state is read from active config - Enterprise AP recovery runs only on device boot (PAGER_WEBUI_BOOT), not on every web-service restart - serve() gates the HTTP port on startup checks with bounded retries and shuts down cleanly on SIGTERM/SIGINT; the recon watchdog waits interruptibly - Recon uses a bounded userspace channel scheduler that drives both monitor radios over non-DFS channels, with preflight verification, serialized starts, and per-cycle error reporting - payload.sh waits for real readiness on start, fully removes the boot service (stop + disable + delete) on stop, and surfaces a stopped-but-enabled boot service; deploy.sh refreshes the installed init script even when the service is stopped - Bump version to 1.3.1 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
6e3968c19a
commit
cd26553d21
@@ -8,7 +8,7 @@
|
||||
"title": "Mark VIII",
|
||||
"author": "c4ch3c4d3",
|
||||
"description": "Mark VII-style web management UI for the WiFi Pineapple Pager",
|
||||
"version": "1.3",
|
||||
"version": "1.3.1",
|
||||
"category": "remote_access",
|
||||
"tags": ["remote-access", "web-interface", "device-management", "pineap"],
|
||||
"firmware": "Pineapple Pager 24.10.1"
|
||||
|
||||
@@ -7,18 +7,24 @@ USE_PROCD=1
|
||||
PAGER_WEBUI_DIR="/root/payloads/user/remote_access/pager-webui"
|
||||
[ -f "$PAGER_WEBUI_DIR/server.py" ] || PAGER_WEBUI_DIR="/mmc/root/payloads/user/remote_access/pager-webui"
|
||||
|
||||
boot() {
|
||||
PAGER_WEBUI_BOOT=1
|
||||
start
|
||||
}
|
||||
|
||||
start_service() {
|
||||
[ -f "$PAGER_WEBUI_DIR/server.py" ] || return 1
|
||||
chmod -R 755 "$PAGER_WEBUI_DIR" 2>/dev/null
|
||||
procd_open_instance pagerwebui
|
||||
procd_set_param command /usr/bin/python3 "$PAGER_WEBUI_DIR/server.py"
|
||||
procd_set_param env PAGER_WEBUI_BOOT="${PAGER_WEBUI_BOOT:-0}"
|
||||
procd_set_param respawn
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param pidfile /tmp/pagerwebui.pid
|
||||
procd_set_param term_timeout 10
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
rm -f /tmp/pagerwebui.pid
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Title: Mark VIII
|
||||
# Description: Mark VII-style web management UI for the WiFi Pineapple Pager
|
||||
# Author: c4ch3c4d3
|
||||
# Version: 1.3
|
||||
# Version: 1.3.1
|
||||
# Category: Remote-Access
|
||||
# Tags: remote-access, web-interface, device-management, pineap
|
||||
# Firmware: Pineapple Pager 24.10.1
|
||||
@@ -28,7 +28,7 @@ get_pager_ip() {
|
||||
}
|
||||
|
||||
LOG "cyan" "+---------------------------+"
|
||||
LOG "cyan" "| Mark VIII v1.3 |"
|
||||
LOG "cyan" "| Mark VIII v1.3.1 |"
|
||||
LOG "cyan" "+---------------------------+"
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
@@ -46,6 +46,36 @@ run_env_check() {
|
||||
LOG "green" "Environment check passed."
|
||||
}
|
||||
|
||||
wait_for_server() {
|
||||
attempts="${1:-30}"
|
||||
while [ "$attempts" -gt 0 ]; do
|
||||
if curl -fsS "http://127.0.0.1:$PORT/" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
attempts=$((attempts - 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_server_stop() {
|
||||
attempts="${1:-12}"
|
||||
while [ "$attempts" -gt 0 ]; do
|
||||
if ! curl -fsS "http://127.0.0.1:$PORT/" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
attempts=$((attempts - 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
remove_boot_service() {
|
||||
"$INIT_SCRIPT" stop 2>/dev/null || true
|
||||
"$INIT_SCRIPT" disable 2>/dev/null || true
|
||||
rm -f "$INIT_SCRIPT"
|
||||
}
|
||||
|
||||
if [ -f "$INIT_SCRIPT" ] && "$INIT_SCRIPT" running 2>/dev/null; then
|
||||
PAGER_IP=$(get_pager_ip)
|
||||
LOG "green" "Mark VIII service is running"
|
||||
@@ -53,14 +83,26 @@ if [ -f "$INIT_SCRIPT" ] && "$INIT_SCRIPT" running 2>/dev/null; then
|
||||
resp=$(CONFIRMATION_DIALOG "Stop service?")
|
||||
if user_confirmed "$resp"; then
|
||||
LOG "yellow" "Stopping service..."
|
||||
"$INIT_SCRIPT" stop
|
||||
"$INIT_SCRIPT" disable
|
||||
rm -f "$INIT_SCRIPT"
|
||||
LOG "cyan" "Service stopped"
|
||||
remove_boot_service
|
||||
if ! wait_for_server_stop 12; then
|
||||
LOG "red" "Service is still listening on port $PORT"
|
||||
exit 1
|
||||
fi
|
||||
LOG "cyan" "Service stopped and removed from boot"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f "$INIT_SCRIPT" ] && "$INIT_SCRIPT" enabled 2>/dev/null; then
|
||||
LOG "yellow" "Mark VIII boot service is installed but not running"
|
||||
resp=$(CONFIRMATION_DIALOG "Remove boot service?")
|
||||
if user_confirmed "$resp"; then
|
||||
remove_boot_service
|
||||
LOG "cyan" "Boot service removed"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
AUTO_MODE=$(PAYLOAD_GET_CONFIG pager_webui auto_mode 2>/dev/null)
|
||||
RUN_MODE=$(PAYLOAD_GET_CONFIG pager_webui run_mode 2>/dev/null)
|
||||
|
||||
@@ -83,8 +125,15 @@ if user_confirmed "$resp"; then
|
||||
cp "$SCRIPT_DIR/pagerwebui.init" "$INIT_SCRIPT"
|
||||
chmod +x "$INIT_SCRIPT"
|
||||
"$INIT_SCRIPT" enable
|
||||
"$INIT_SCRIPT" start
|
||||
sleep 1
|
||||
if ! "$INIT_SCRIPT" start; then
|
||||
LOG "red" "Service start command failed"
|
||||
exit 1
|
||||
fi
|
||||
if ! wait_for_server 90; then
|
||||
remove_boot_service
|
||||
LOG "red" "Service failed startup checks; inspect logread"
|
||||
exit 1
|
||||
fi
|
||||
PAGER_IP=$(get_pager_ip)
|
||||
LOG "green" "Service started!"
|
||||
LOG "green" "http://$PAGER_IP:$PORT"
|
||||
@@ -106,10 +155,10 @@ trap cleanup EXIT INT TERM
|
||||
[ ! -f "$SCRIPT_DIR/server.py" ] && { LOG "red" "server.py not found!"; exit 1; }
|
||||
python3 "$SCRIPT_DIR/server.py" >/tmp/pagerwebui.log 2>&1 &
|
||||
echo $! > "$PID_FILE"
|
||||
sleep 1
|
||||
|
||||
PAGER_IP=$(get_pager_ip)
|
||||
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||
if wait_for_server 90 && [ -f "$PID_FILE" ] &&
|
||||
kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||
LOG "green" "http://$PAGER_IP:$PORT"
|
||||
LOG ""
|
||||
LOG "magenta" "Press B to stop"
|
||||
|
||||
@@ -41,6 +41,15 @@ PORT = int(os.environ.get('PAGER_PORT', '8080'))
|
||||
|
||||
_recon_scan_state = {'active': False, 'started': 0, 'duration': 0}
|
||||
DEFAULT_RECON_DURATION = 30
|
||||
RECON_HOP_INTERVAL = 0.8
|
||||
RECON_CHANNELS = {
|
||||
'wlan0mon': (1, 6, 11),
|
||||
# Avoid DFS channels: changing to one can trigger CAC or fail entirely.
|
||||
'wlan1mon': (36, 40, 44, 48, 149, 153, 157, 161, 165),
|
||||
}
|
||||
_recon_scan_lock = threading.Lock()
|
||||
_recon_hopper_stop = threading.Event()
|
||||
_recon_hop_state = {'active': False, 'error': None}
|
||||
_recon_scans_cache = {'db': None, 'updated': 0, 'data': {'scans': []}}
|
||||
_recon_status_cache = {
|
||||
'db': None, 'updated': 0, 'last_scan': None, 'last_activity': None}
|
||||
@@ -1231,6 +1240,11 @@ def recon_scan_data(scan_id, _timeout=20, _limit=None, db=None):
|
||||
'aps': aps, 'clients': clients, 'handshakes': handshakes,
|
||||
'unassociated': unassociated}
|
||||
def h_recon_start(ctx):
|
||||
with _recon_scan_lock:
|
||||
return _h_recon_start_locked(ctx)
|
||||
|
||||
|
||||
def _h_recon_start_locked(ctx):
|
||||
# Serialize starts: the firmware has no abort for a timed scan, so a
|
||||
# second /recon/new while one is running just stacks another empty scan
|
||||
# (the 3s-apart rows we saw). Refuse instead of piling on.
|
||||
@@ -1249,6 +1263,12 @@ def h_recon_start(ctx):
|
||||
return 400, {'error': 'scan_time must be an integer'}
|
||||
if scan_time < 1 or scan_time > 86400:
|
||||
return 400, {'error': 'scan_time is out of range'}
|
||||
hop_ok, hop_detail = _recon_hopper_preflight()
|
||||
if not hop_ok:
|
||||
return 503, {
|
||||
'error': 'recon radio preflight failed',
|
||||
'detail': hop_detail,
|
||||
}
|
||||
body = {'scan_time': scan_time}
|
||||
# log/recon/start restarts the recon logger and can rotate the existing
|
||||
# database. recon/new is the Pager's native "start another scan" action and
|
||||
@@ -1265,6 +1285,7 @@ def h_recon_start(ctx):
|
||||
_recon_scan_state['active'] = True
|
||||
_recon_scan_state['started'] = time.time()
|
||||
_recon_scan_state['duration'] = scan_time
|
||||
_start_recon_hopper(scan_time)
|
||||
return 200, {'ok': True}
|
||||
|
||||
|
||||
@@ -1301,13 +1322,70 @@ def _recon_watchdog_tick():
|
||||
st['active'] = False
|
||||
|
||||
|
||||
def _set_monitor_channel(interface, channel):
|
||||
rc, out, err = device_run(
|
||||
['iw', 'dev', interface, 'set', 'channel', str(channel)], timeout=5)
|
||||
if rc == 0:
|
||||
return True, ''
|
||||
detail = (err or out or 'command failed').strip()
|
||||
return False, '%s channel %s: %s' % (interface, channel, detail[-160:])
|
||||
|
||||
|
||||
def _recon_hopper_preflight():
|
||||
"""Prove both monitor PHYs can change channel before creating a scan.
|
||||
|
||||
pineapd can report ``Hop fast`` while both radios remain fixed. Direct
|
||||
channel changes are the only reliable contract on this Pager firmware.
|
||||
"""
|
||||
for interface, channels in RECON_CHANNELS.items():
|
||||
for channel in channels:
|
||||
ok, detail = _set_monitor_channel(interface, channel)
|
||||
if not ok:
|
||||
return False, detail
|
||||
return True, 'monitor channel control ready'
|
||||
|
||||
|
||||
def _recon_hopper_loop(duration, stop_event):
|
||||
deadline = time.monotonic() + duration
|
||||
offsets = dict((interface, 1) for interface in RECON_CHANNELS)
|
||||
_recon_hop_state.update({'active': True, 'error': None})
|
||||
try:
|
||||
while not stop_event.is_set() and time.monotonic() < deadline:
|
||||
cycle_error = None
|
||||
for interface, channels in RECON_CHANNELS.items():
|
||||
index = offsets[interface] % len(channels)
|
||||
ok, detail = _set_monitor_channel(interface, channels[index])
|
||||
if not ok:
|
||||
cycle_error = detail
|
||||
offsets[interface] = index + 1
|
||||
_recon_hop_state['error'] = cycle_error
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
break
|
||||
stop_event.wait(min(RECON_HOP_INTERVAL, remaining))
|
||||
finally:
|
||||
if stop_event is _recon_hopper_stop:
|
||||
_recon_hop_state['active'] = False
|
||||
|
||||
|
||||
def _start_recon_hopper(duration):
|
||||
global _recon_hopper_stop
|
||||
_recon_hopper_stop.set()
|
||||
stop_event = threading.Event()
|
||||
_recon_hopper_stop = stop_event
|
||||
thread = threading.Thread(
|
||||
target=_recon_hopper_loop, args=(duration, stop_event), daemon=True,
|
||||
name='recon-channel-hopper')
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
|
||||
_hopper_cache = {'updated': 0, 'online': None}
|
||||
HOPPER_CACHE_SECONDS = 10.0
|
||||
HOPPER_IFACE = 'wlan2mon'
|
||||
|
||||
|
||||
def _hopper_online():
|
||||
"""Whether the fast-hopping radio (wlan2mon per pineapd config) exists.
|
||||
"""Whether both monitor interfaces required by the scheduler exist.
|
||||
|
||||
iwinfo is a subprocess, so the answer is cached for a few seconds; the
|
||||
UI polls status every 5s.
|
||||
@@ -1316,7 +1394,8 @@ def _hopper_online():
|
||||
if now - _hopper_cache['updated'] < HOPPER_CACHE_SECONDS:
|
||||
return _hopper_cache['online']
|
||||
try:
|
||||
online = HOPPER_IFACE in wifi_ifaces()
|
||||
interfaces = wifi_ifaces()
|
||||
online = all(name in interfaces for name in RECON_CHANNELS)
|
||||
except Exception:
|
||||
online = None
|
||||
_hopper_cache.update({'updated': now, 'online': online})
|
||||
@@ -1358,6 +1437,7 @@ def h_recon_status(ctx):
|
||||
'active': last_activity is not None and int(time.time()) - last_activity < 300,
|
||||
'scanning': scanning, 'scan_remaining': remaining, 'stale': stale,
|
||||
'hopper_online': _hopper_online(),
|
||||
'hopper_error': _recon_hop_state.get('error'),
|
||||
'wlan0_pinned': _wlan0_pinned(),
|
||||
'wlan0_sta': _sta_uplink_enabled(),
|
||||
'history_reset': _recon_history_reset()}
|
||||
@@ -3518,6 +3598,10 @@ PINEAPD_SAFE_UCI = {
|
||||
'pineapd.wlan0mon.bands': '2',
|
||||
'pineapd.wlan1mon.hop': '0',
|
||||
}
|
||||
PINEAPD_RUNTIME_UCI = {
|
||||
key for key in PINEAPD_SAFE_UCI if '.wlan' in key
|
||||
}
|
||||
PINEAPD_RUNTIME_UCI.add('pineapd.@ssidpool[0].disable')
|
||||
|
||||
|
||||
def _apply_uci_wanted(wanted):
|
||||
@@ -3527,19 +3611,30 @@ def _apply_uci_wanted(wanted):
|
||||
rc, out, err = device_run(['uci', 'get', key])
|
||||
if rc != 0 or out.strip() != value:
|
||||
device_run(['uci', 'set', '%s=%s' % (key, value)])
|
||||
actions.append(key.split('.')[-1])
|
||||
actions.append(key)
|
||||
return actions
|
||||
|
||||
|
||||
def _stabilize_uci():
|
||||
def _pending_uci(wanted):
|
||||
pending = []
|
||||
for key, value in wanted.items():
|
||||
rc, out, err = device_run(['uci', 'get', key])
|
||||
if rc != 0 or out.strip() != value:
|
||||
pending.append(key)
|
||||
return pending
|
||||
|
||||
|
||||
def _stabilize_uci(clear_pool=True):
|
||||
"""Sane-off UCI pass shared by the health monitor and the startup env
|
||||
check. Clears the refilled pool list too. Commits when changed and returns
|
||||
the action labels."""
|
||||
check. Clear the refilled pool only while pineapd is down or undergoing a
|
||||
controlled restart; committing that list while it is live makes the stock
|
||||
watchdog SIGTERM pineapd."""
|
||||
actions = _apply_uci_wanted(PINEAPD_SAFE_UCI)
|
||||
rc, out, err = device_run(['uci', 'get', 'pineapd.@ssidpool[0].ssid'])
|
||||
if rc == 0 and out.strip():
|
||||
device_run(['uci', 'delete', 'pineapd.@ssidpool[0].ssid'])
|
||||
actions.append('pool-list cleared')
|
||||
if clear_pool:
|
||||
rc, out, err = device_run(['uci', 'get', 'pineapd.@ssidpool[0].ssid'])
|
||||
if rc == 0 and out.strip():
|
||||
device_run(['uci', 'delete', 'pineapd.@ssidpool[0].ssid'])
|
||||
actions.append('pool-list cleared')
|
||||
if actions:
|
||||
device_run(['uci', 'commit', 'pineapd'])
|
||||
return actions
|
||||
@@ -3615,19 +3710,27 @@ def health_check():
|
||||
|
||||
|
||||
def _raise_monitors():
|
||||
"""Bring any down monitor interfaces up. Returns the interfaces raised."""
|
||||
"""Bring down monitor interfaces up and return those verified up."""
|
||||
raised = []
|
||||
for name in ('wlan1mon', 'wlan0mon'):
|
||||
if _monitor_down(name):
|
||||
device_run(['ip', 'link', 'set', name, 'up'], timeout=10)
|
||||
raised.append(name)
|
||||
rc, out, err = device_run(
|
||||
['ip', 'link', 'set', name, 'up'], timeout=10)
|
||||
if rc == 0 and not _monitor_down(name):
|
||||
raised.append(name)
|
||||
return raised
|
||||
|
||||
|
||||
def _bring_monitors_up(h):
|
||||
_raise_monitors()
|
||||
h['last_action'] = 'monitor interfaces brought up'
|
||||
h['monitor_fixes'] = h.get('monitor_fixes', 0) + 1
|
||||
raised = _raise_monitors()
|
||||
unavailable = [
|
||||
name for name in ('wlan0mon', 'wlan1mon') if _monitor_down(name)]
|
||||
if unavailable:
|
||||
h['last_action'] = 'monitor repair failed: ' + ', '.join(unavailable)
|
||||
else:
|
||||
h['last_action'] = 'monitor interfaces brought up'
|
||||
if raised:
|
||||
h['monitor_fixes'] = h.get('monitor_fixes', 0) + len(raised)
|
||||
|
||||
|
||||
def h_health(ctx):
|
||||
@@ -3668,6 +3771,8 @@ def start_health_monitor():
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
ENV_CHECK_STATE = {'report': None, 'overall': None, 'updated': 0, 'pool_runtime': None}
|
||||
STARTUP_CHECK_ATTEMPTS = 6
|
||||
STARTUP_CHECK_DELAY = 5
|
||||
|
||||
|
||||
def _env_step(report, ok, detail, action=None):
|
||||
@@ -3696,14 +3801,29 @@ def _pineapd_alive():
|
||||
return rc == 0 and bool((out or '').strip())
|
||||
|
||||
|
||||
def _daemon_alive():
|
||||
"""Passive stock-daemon readiness check.
|
||||
|
||||
Even a config GET makes the stock daemon write to pineapd's command
|
||||
socket on this firmware, which can trigger its watchdog. Process state is
|
||||
the safe startup contract; functional API calls report their own failures.
|
||||
"""
|
||||
rc, out, err = device_run(['pidof', 'pineapple'], timeout=10)
|
||||
return rc == 0 and bool((out or '').strip())
|
||||
|
||||
|
||||
def _sync_pool_runtime():
|
||||
"""Force pineapd's runtime SSID-pool broadcast off so it matches the
|
||||
sane-off UCI default the UI derives from. Returns (state, detail)."""
|
||||
rc, out, err = _pineap('SSIDPOOL', 'DISABLE', timeout=15)
|
||||
if rc != 0:
|
||||
detail = (err or out or '').strip() or 'no output'
|
||||
return 'unknown', 'SSIDPOOL DISABLE failed: %s' % detail[-200:]
|
||||
return 'disabled', 'SSIDPOOL DISABLE sent'
|
||||
"""Report the crash-safe pool state without writing pineapd's socket.
|
||||
|
||||
Runtime-sensitive UCI changes cause a controlled pineapd restart in
|
||||
``env_check``. When UCI is already safe, writing either directly or
|
||||
through the stock daemon races its own command traffic and makes its
|
||||
watchdog SIGTERM pineapd.
|
||||
"""
|
||||
pool = _uci_section('pineapd.@ssidpool[0]')
|
||||
if pool.get('disable') == '1':
|
||||
return 'disabled', 'SSID-pool broadcast disabled by active configuration'
|
||||
return 'unknown', 'SSID-pool disable setting is not active'
|
||||
|
||||
|
||||
def _wlan0_pinned():
|
||||
@@ -3739,29 +3859,36 @@ def _disable_sta_uplink():
|
||||
def env_check():
|
||||
"""Full environment pass, run at service startup and via
|
||||
``server.py --env-check``. Auto-fixes everything fixable, re-verifies, and
|
||||
returns a list of step reports (ok: pass|fixed|warn|fail). Only core
|
||||
dependencies (daemon, pineapd, recon DB) can fail."""
|
||||
returns a list of step reports (ok: pass|fixed|warn|fail). Core
|
||||
dependencies (daemon, pineapd, monitors, recon DB) can fail."""
|
||||
report = []
|
||||
|
||||
status, data = daemon_sock_call('GET', '/api/pineap/get_config')
|
||||
if status != 200:
|
||||
_env_step(report, 'fail', 'daemon unreachable (status %s)' % status)
|
||||
if not _daemon_alive():
|
||||
_env_step(report, 'fail', 'daemon unreachable (process not running)')
|
||||
else:
|
||||
_env_step(report, 'pass', 'daemon reachable')
|
||||
|
||||
if _pineapd_alive():
|
||||
_env_step(report, 'pass', 'pineapd running')
|
||||
else:
|
||||
actions = _stabilize_uci()
|
||||
pineap_was_alive = _pineapd_alive()
|
||||
pending = _pending_uci(PINEAPD_SAFE_UCI)
|
||||
runtime_changed = any(action in PINEAPD_RUNTIME_UCI
|
||||
for action in pending)
|
||||
if pineap_was_alive and runtime_changed:
|
||||
device_run(['/etc/init.d/pineapd', 'stop'], timeout=30)
|
||||
actions = _stabilize_uci(
|
||||
clear_pool=not pineap_was_alive or runtime_changed)
|
||||
if not pineap_was_alive or runtime_changed:
|
||||
device_run(['/etc/init.d/pineapd', 'restart'], timeout=30)
|
||||
if _pineapd_alive():
|
||||
_env_step(report, 'fixed', 'pineapd was down; stabilized and restarted',
|
||||
reason = ('runtime safety settings changed' if runtime_changed
|
||||
else 'pineapd was down')
|
||||
_env_step(report, 'fixed', '%s; pineapd restarted' % reason,
|
||||
', '.join(actions) or 'restart')
|
||||
else:
|
||||
_env_step(report, 'fail', 'pineapd did not come back after restart',
|
||||
', '.join(actions) or 'restart')
|
||||
else:
|
||||
_env_step(report, 'pass', 'pineapd running')
|
||||
|
||||
actions = _stabilize_uci()
|
||||
if actions:
|
||||
_env_step(report, 'fixed', 'pineapd sane-off UCI defaults applied',
|
||||
', '.join(actions))
|
||||
@@ -3782,8 +3909,15 @@ def env_check():
|
||||
else:
|
||||
_env_step(report, 'pass', 'no STA uplink pinning phy0')
|
||||
|
||||
monitors_before = [
|
||||
name for name in ('wlan0mon', 'wlan1mon') if _monitor_down(name)]
|
||||
raised = _raise_monitors()
|
||||
if raised:
|
||||
monitors_down = [
|
||||
name for name in ('wlan0mon', 'wlan1mon') if _monitor_down(name)]
|
||||
if monitors_down:
|
||||
_env_step(report, 'fail', 'monitor interfaces unavailable after repair: %s' %
|
||||
', '.join(monitors_down))
|
||||
elif monitors_before:
|
||||
_env_step(report, 'fixed', 'monitor interfaces brought up', ', '.join(raised))
|
||||
else:
|
||||
_env_step(report, 'pass', 'monitors up (wlan0mon, wlan1mon)')
|
||||
@@ -3807,6 +3941,36 @@ def env_check():
|
||||
return report
|
||||
|
||||
|
||||
def startup_env_check(attempts=STARTUP_CHECK_ATTEMPTS,
|
||||
delay=STARTUP_CHECK_DELAY):
|
||||
"""Run and print the startup contract, allowing boot dependencies time.
|
||||
|
||||
A failed core dependency must prevent the HTTP server from presenting a
|
||||
healthy-looking UI. procd can then respawn it instead of leaving a broken
|
||||
process bound to the port.
|
||||
"""
|
||||
last_report = []
|
||||
for attempt in range(1, attempts + 1):
|
||||
try:
|
||||
last_report = env_check()
|
||||
except Exception as exc:
|
||||
last_report = []
|
||||
ENV_CHECK_STATE['overall'] = 'fail'
|
||||
print('[FAIL] environment check raised: %s' % exc, flush=True)
|
||||
_print_env_report(last_report, flush=True)
|
||||
if ENV_CHECK_STATE.get('overall') != 'fail':
|
||||
print('ENVIRONMENT CHECK: %s' %
|
||||
ENV_CHECK_STATE['overall'].upper(), flush=True)
|
||||
return last_report
|
||||
if attempt < attempts:
|
||||
print('ENVIRONMENT CHECK: FAIL; retry %d/%d in %ds' %
|
||||
(attempt + 1, attempts, delay), flush=True)
|
||||
if LIVE_STOP.wait(delay):
|
||||
raise RuntimeError('startup environment check interrupted')
|
||||
raise RuntimeError('startup environment check failed after %d attempts' %
|
||||
attempts)
|
||||
|
||||
|
||||
def h_attacks_clients(ctx):
|
||||
"""Clients (recon devices) plus the APs matching an SSID, for targeting."""
|
||||
ssid = ((ctx.query or {}).get('ssid') or '').strip()
|
||||
@@ -5347,37 +5511,49 @@ def _handle_conn(conn, addr):
|
||||
|
||||
def _recon_watchdog_loop():
|
||||
while not LIVE_STOP.is_set():
|
||||
time.sleep(1)
|
||||
LIVE_STOP.wait(1)
|
||||
_recon_watchdog_tick()
|
||||
|
||||
|
||||
def _request_shutdown(signum=None, frame=None):
|
||||
LIVE_STOP.set()
|
||||
HEALTH_STOP.set()
|
||||
_recon_hopper_stop.set()
|
||||
|
||||
|
||||
def serve():
|
||||
try:
|
||||
env_check()
|
||||
except Exception:
|
||||
pass
|
||||
LIVE_STOP.clear()
|
||||
HEALTH_STOP.clear()
|
||||
_recon_hopper_stop.set()
|
||||
startup_env_check()
|
||||
threading.Thread(target=live_loop, daemon=True).start()
|
||||
threading.Thread(target=_recon_watchdog_loop, daemon=True).start()
|
||||
start_health_monitor()
|
||||
_enterprise_boot_recover()
|
||||
if os.environ.get('PAGER_WEBUI_BOOT') == '1':
|
||||
_enterprise_boot_recover()
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((HOST, PORT))
|
||||
sock.listen(16)
|
||||
while True:
|
||||
conn, addr = sock.accept()
|
||||
threading.Thread(target=_handle_conn, args=(conn, addr), daemon=True).start()
|
||||
sock.settimeout(1)
|
||||
try:
|
||||
while not LIVE_STOP.is_set():
|
||||
try:
|
||||
conn, addr = sock.accept()
|
||||
except socket.timeout:
|
||||
continue
|
||||
threading.Thread(
|
||||
target=_handle_conn, args=(conn, addr), daemon=True).start()
|
||||
finally:
|
||||
sock.close()
|
||||
_request_shutdown()
|
||||
|
||||
|
||||
def env_check_cli():
|
||||
"""Run the environment check and print a verbose report to stdout.
|
||||
Returns the process exit code (0 = pass/warn, 1 = core failure)."""
|
||||
report = env_check()
|
||||
for step in report:
|
||||
line = '[%s] %s' % (step['ok'].upper(), step['detail'])
|
||||
if step.get('action'):
|
||||
line += ' (%s)' % step['action']
|
||||
print(line)
|
||||
_print_env_report(report)
|
||||
counts = {k: _env_count(report, k) for k in ('pass', 'fixed', 'warn', 'fail')}
|
||||
print('ENVIRONMENT CHECK: %s (%d pass, %d fixed, %d warn, %d fail)' % (
|
||||
ENV_CHECK_STATE['overall'].upper(), counts['pass'], counts['fixed'],
|
||||
@@ -5385,7 +5561,17 @@ def env_check_cli():
|
||||
return 0 if ENV_CHECK_STATE['overall'] != 'fail' else 1
|
||||
|
||||
|
||||
def _print_env_report(report, flush=False):
|
||||
for step in report:
|
||||
line = '[%s] %s' % (step['ok'].upper(), step['detail'])
|
||||
if step.get('action'):
|
||||
line += ' (%s)' % step['action']
|
||||
print(line, flush=flush)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if '--env-check' in sys.argv:
|
||||
sys.exit(env_check_cli())
|
||||
signal.signal(signal.SIGTERM, _request_shutdown)
|
||||
signal.signal(signal.SIGINT, _request_shutdown)
|
||||
serve()
|
||||
|
||||
Reference in New Issue
Block a user