mirror of
https://github.com/smittix/intercept.git
synced 2026-07-08 17:48:13 -07:00
Add bias_t parameter to SDR command builders
The bias_t parameter was being passed to build_fm_demod_command() but wasn't defined in the method signatures, causing an unexpected keyword argument error. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"version": "2026-01-04_e27bf619",
|
||||||
|
"downloaded": "2026-01-07T14:55:20.680977Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
"""
|
||||||
|
Process health monitoring and auto-restart functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Callable, Dict, Optional, Any
|
||||||
|
|
||||||
|
logger = logging.getLogger('intercept.process_monitor')
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProcessInfo:
|
||||||
|
"""Information about a monitored process."""
|
||||||
|
name: str
|
||||||
|
process: Any # subprocess.Popen
|
||||||
|
started_at: datetime = field(default_factory=datetime.now)
|
||||||
|
restart_count: int = 0
|
||||||
|
last_restart: Optional[datetime] = None
|
||||||
|
restart_callback: Optional[Callable] = None
|
||||||
|
max_restarts: int = 3
|
||||||
|
backoff_seconds: float = 5.0
|
||||||
|
enabled: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessMonitor:
|
||||||
|
"""
|
||||||
|
Monitor and auto-restart processes.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
monitor = ProcessMonitor()
|
||||||
|
monitor.register('pager', process, restart_callback=start_pager)
|
||||||
|
monitor.start()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, check_interval: float = 5.0):
|
||||||
|
self.processes: Dict[str, ProcessInfo] = {}
|
||||||
|
self.check_interval = check_interval
|
||||||
|
self._running = False
|
||||||
|
self._thread: Optional[threading.Thread] = None
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
def register(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
process: Any,
|
||||||
|
restart_callback: Optional[Callable] = None,
|
||||||
|
max_restarts: int = 3,
|
||||||
|
backoff_seconds: float = 5.0
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Register a process for monitoring.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Unique name for the process
|
||||||
|
process: The subprocess.Popen object
|
||||||
|
restart_callback: Function to call to restart the process
|
||||||
|
max_restarts: Maximum number of automatic restarts
|
||||||
|
backoff_seconds: Base backoff time between restarts
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
self.processes[name] = ProcessInfo(
|
||||||
|
name=name,
|
||||||
|
process=process,
|
||||||
|
restart_callback=restart_callback,
|
||||||
|
max_restarts=max_restarts,
|
||||||
|
backoff_seconds=backoff_seconds
|
||||||
|
)
|
||||||
|
logger.info(f"Registered process for monitoring: {name}")
|
||||||
|
|
||||||
|
def unregister(self, name: str) -> None:
|
||||||
|
"""Remove a process from monitoring."""
|
||||||
|
with self._lock:
|
||||||
|
if name in self.processes:
|
||||||
|
del self.processes[name]
|
||||||
|
logger.info(f"Unregistered process: {name}")
|
||||||
|
|
||||||
|
def update_process(self, name: str, process: Any) -> None:
|
||||||
|
"""Update the process object for a registered name."""
|
||||||
|
with self._lock:
|
||||||
|
if name in self.processes:
|
||||||
|
self.processes[name].process = process
|
||||||
|
self.processes[name].started_at = datetime.now()
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Start the monitoring thread."""
|
||||||
|
if self._running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._running = True
|
||||||
|
self._thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
logger.info("Process monitor started")
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Stop the monitoring thread."""
|
||||||
|
self._running = False
|
||||||
|
if self._thread:
|
||||||
|
self._thread.join(timeout=self.check_interval + 1)
|
||||||
|
logger.info("Process monitor stopped")
|
||||||
|
|
||||||
|
def _monitor_loop(self) -> None:
|
||||||
|
"""Main monitoring loop."""
|
||||||
|
while self._running:
|
||||||
|
self._check_all_processes()
|
||||||
|
time.sleep(self.check_interval)
|
||||||
|
|
||||||
|
def _check_all_processes(self) -> None:
|
||||||
|
"""Check health of all registered processes."""
|
||||||
|
with self._lock:
|
||||||
|
for name, info in list(self.processes.items()):
|
||||||
|
if not info.enabled:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if info.process is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if process has terminated
|
||||||
|
return_code = info.process.poll()
|
||||||
|
if return_code is not None:
|
||||||
|
logger.warning(
|
||||||
|
f"Process '{name}' terminated with code {return_code}"
|
||||||
|
)
|
||||||
|
self._handle_crash(name, info)
|
||||||
|
|
||||||
|
def _handle_crash(self, name: str, info: ProcessInfo) -> None:
|
||||||
|
"""Handle a crashed process."""
|
||||||
|
if info.restart_callback is None:
|
||||||
|
logger.info(f"No restart callback for '{name}', skipping auto-restart")
|
||||||
|
return
|
||||||
|
|
||||||
|
if info.restart_count >= info.max_restarts:
|
||||||
|
logger.error(
|
||||||
|
f"Process '{name}' exceeded max restarts ({info.max_restarts}), "
|
||||||
|
"disabling auto-restart"
|
||||||
|
)
|
||||||
|
info.enabled = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate backoff with exponential increase
|
||||||
|
backoff = info.backoff_seconds * (2 ** info.restart_count)
|
||||||
|
logger.info(
|
||||||
|
f"Attempting to restart '{name}' in {backoff:.1f}s "
|
||||||
|
f"(attempt {info.restart_count + 1}/{info.max_restarts})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for backoff period
|
||||||
|
time.sleep(backoff)
|
||||||
|
|
||||||
|
# Attempt restart
|
||||||
|
try:
|
||||||
|
info.restart_callback()
|
||||||
|
info.restart_count += 1
|
||||||
|
info.last_restart = datetime.now()
|
||||||
|
logger.info(f"Successfully restarted '{name}'")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to restart '{name}': {e}")
|
||||||
|
info.restart_count += 1
|
||||||
|
|
||||||
|
def get_status(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get status of all monitored processes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with process status information
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
status = {}
|
||||||
|
for name, info in self.processes.items():
|
||||||
|
is_running = (
|
||||||
|
info.process is not None and
|
||||||
|
info.process.poll() is None
|
||||||
|
)
|
||||||
|
status[name] = {
|
||||||
|
'running': is_running,
|
||||||
|
'started_at': info.started_at.isoformat() if info.started_at else None,
|
||||||
|
'restart_count': info.restart_count,
|
||||||
|
'last_restart': info.last_restart.isoformat() if info.last_restart else None,
|
||||||
|
'auto_restart_enabled': info.enabled,
|
||||||
|
'return_code': info.process.poll() if info.process else None
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
|
||||||
|
def reset_restart_count(self, name: str) -> None:
|
||||||
|
"""Reset the restart count for a process (e.g., after manual restart)."""
|
||||||
|
with self._lock:
|
||||||
|
if name in self.processes:
|
||||||
|
self.processes[name].restart_count = 0
|
||||||
|
self.processes[name].enabled = True
|
||||||
|
|
||||||
|
def is_healthy(self) -> bool:
|
||||||
|
"""Check if all processes are healthy."""
|
||||||
|
with self._lock:
|
||||||
|
for info in self.processes.values():
|
||||||
|
if info.process is not None and info.process.poll() is not None:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Global monitor instance
|
||||||
|
process_monitor = ProcessMonitor()
|
||||||
+5
-1
@@ -64,7 +64,8 @@ class AirspyCommandBuilder(CommandBuilder):
|
|||||||
gain: Optional[float] = None,
|
gain: Optional[float] = None,
|
||||||
ppm: Optional[int] = None,
|
ppm: Optional[int] = None,
|
||||||
modulation: str = "fm",
|
modulation: str = "fm",
|
||||||
squelch: Optional[int] = None
|
squelch: Optional[int] = None,
|
||||||
|
bias_t: bool = False
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Build SoapySDR rx_fm command for FM demodulation.
|
Build SoapySDR rx_fm command for FM demodulation.
|
||||||
@@ -87,6 +88,9 @@ class AirspyCommandBuilder(CommandBuilder):
|
|||||||
if squelch is not None and squelch > 0:
|
if squelch is not None and squelch > 0:
|
||||||
cmd.extend(['-l', str(squelch)])
|
cmd.extend(['-l', str(squelch)])
|
||||||
|
|
||||||
|
if bias_t:
|
||||||
|
cmd.extend(['-T'])
|
||||||
|
|
||||||
# Output to stdout
|
# Output to stdout
|
||||||
cmd.append('-')
|
cmd.append('-')
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -93,7 +93,8 @@ class CommandBuilder(ABC):
|
|||||||
gain: Optional[float] = None,
|
gain: Optional[float] = None,
|
||||||
ppm: Optional[int] = None,
|
ppm: Optional[int] = None,
|
||||||
modulation: str = "fm",
|
modulation: str = "fm",
|
||||||
squelch: Optional[int] = None
|
squelch: Optional[int] = None,
|
||||||
|
bias_t: bool = False
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Build FM demodulation command (for pager decoding).
|
Build FM demodulation command (for pager decoding).
|
||||||
@@ -106,6 +107,7 @@ class CommandBuilder(ABC):
|
|||||||
ppm: PPM frequency correction
|
ppm: PPM frequency correction
|
||||||
modulation: Modulation type (fm, am, etc.)
|
modulation: Modulation type (fm, am, etc.)
|
||||||
squelch: Squelch level
|
squelch: Squelch level
|
||||||
|
bias_t: Enable bias-T power (for active antennas)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Command as list of strings for subprocess
|
Command as list of strings for subprocess
|
||||||
|
|||||||
+5
-1
@@ -60,7 +60,8 @@ class HackRFCommandBuilder(CommandBuilder):
|
|||||||
gain: Optional[float] = None,
|
gain: Optional[float] = None,
|
||||||
ppm: Optional[int] = None,
|
ppm: Optional[int] = None,
|
||||||
modulation: str = "fm",
|
modulation: str = "fm",
|
||||||
squelch: Optional[int] = None
|
squelch: Optional[int] = None,
|
||||||
|
bias_t: bool = False
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Build SoapySDR rx_fm command for FM demodulation.
|
Build SoapySDR rx_fm command for FM demodulation.
|
||||||
@@ -84,6 +85,9 @@ class HackRFCommandBuilder(CommandBuilder):
|
|||||||
if squelch is not None and squelch > 0:
|
if squelch is not None and squelch > 0:
|
||||||
cmd.extend(['-l', str(squelch)])
|
cmd.extend(['-l', str(squelch)])
|
||||||
|
|
||||||
|
if bias_t:
|
||||||
|
cmd.extend(['-T'])
|
||||||
|
|
||||||
# Output to stdout
|
# Output to stdout
|
||||||
cmd.append('-')
|
cmd.append('-')
|
||||||
|
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ class LimeSDRCommandBuilder(CommandBuilder):
|
|||||||
gain: Optional[float] = None,
|
gain: Optional[float] = None,
|
||||||
ppm: Optional[int] = None,
|
ppm: Optional[int] = None,
|
||||||
modulation: str = "fm",
|
modulation: str = "fm",
|
||||||
squelch: Optional[int] = None
|
squelch: Optional[int] = None,
|
||||||
|
bias_t: bool = False
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Build SoapySDR rx_fm command for FM demodulation.
|
Build SoapySDR rx_fm command for FM demodulation.
|
||||||
|
|
||||||
For pager decoding with LimeSDR.
|
For pager decoding with LimeSDR.
|
||||||
|
Note: LimeSDR does not support bias-T, parameter is ignored.
|
||||||
"""
|
"""
|
||||||
device_str = self._build_device_string(device)
|
device_str = self._build_device_string(device)
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -45,7 +45,8 @@ class RTLSDRCommandBuilder(CommandBuilder):
|
|||||||
gain: Optional[float] = None,
|
gain: Optional[float] = None,
|
||||||
ppm: Optional[int] = None,
|
ppm: Optional[int] = None,
|
||||||
modulation: str = "fm",
|
modulation: str = "fm",
|
||||||
squelch: Optional[int] = None
|
squelch: Optional[int] = None,
|
||||||
|
bias_t: bool = False
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Build rtl_fm command for FM demodulation.
|
Build rtl_fm command for FM demodulation.
|
||||||
@@ -69,6 +70,9 @@ class RTLSDRCommandBuilder(CommandBuilder):
|
|||||||
if squelch is not None and squelch > 0:
|
if squelch is not None and squelch > 0:
|
||||||
cmd.extend(['-l', str(squelch)])
|
cmd.extend(['-l', str(squelch)])
|
||||||
|
|
||||||
|
if bias_t:
|
||||||
|
cmd.extend(['-T'])
|
||||||
|
|
||||||
# Output to stdout for piping
|
# Output to stdout for piping
|
||||||
cmd.append('-')
|
cmd.append('-')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user