new kamil adc

This commit is contained in:
Ayzen
2026-06-11 13:02:02 +03:00
parent 21f76d7cd2
commit 9661504e51
46 changed files with 12097 additions and 1063 deletions
@@ -23,6 +23,10 @@ logger = logging.getLogger(__name__)
_LOG_MAX_BYTES = 8 * 1024 * 1024
# Per-process force-kill deadline used on stop; each child gets its own window.
_STOP_GRACE_SECONDS = 2.0
# Cmdline marker for the Kamil ADC collector binary. The collector runs in its own
# session, so it can outlive a force-killed acquisition producer while still holding
# the L-Card E-502; we hunt it by name as a guaranteed device-release backstop.
_KAMIL_COLLECTOR_MARKER = b"kamil_adc_collector"
@dataclass(slots=True)
@@ -135,6 +139,9 @@ class ProcessSupervisor:
"""Start required pipeline binaries and wait until they are ready."""
if self.is_running():
raise RuntimeError("Acquisition processes are already running")
# Clean slate: an ADC collector orphaned by a prior crash/force-kill can
# still hold the E-502 and make a fresh acquisition fail to open.
self._kill_kamil_collectors()
logger.info(
"Starting pipeline from config %s (allow_clean_orchestrator_exit=%s)",
@@ -186,6 +193,7 @@ class ProcessSupervisor:
def stop(self) -> None:
"""Stop acquisition-side processes, keep processor process intact."""
self._stop_processes(["sweep_orchestrator", "data_preprocessor"])
self._kill_kamil_collectors()
def stop_orchestrator(self) -> None:
"""Stop orchestrator process only."""
@@ -198,6 +206,7 @@ class ProcessSupervisor:
def stop_all(self) -> None:
"""Stop all managed processes."""
self._stop_processes(["sweep_orchestrator", "data_preprocessor", "data_processor"])
self._kill_kamil_collectors()
def _spawn(self, name: str, command: list[str], *, allow_clean_exit: bool) -> None:
"""Spawn one process unless same process is already alive.
@@ -353,6 +362,32 @@ class ProcessSupervisor:
except (ProcessLookupError, PermissionError):
pass
def _kill_kamil_collectors(self) -> None:
"""SIGKILL any Kamil ADC collector found by name — a guaranteed device backstop.
The collector survives a force-killed producer (own session) and can hang
the E-502, so it is hunted by cmdline and its group killed hard, regardless
of who its parent is. Best-effort and never raises.
"""
own_pid = os.getpid()
try:
pids = [int(entry.name) for entry in Path("/proc").iterdir() if entry.name.isdigit()]
except OSError:
return
killed = 0
for pid in pids:
if pid <= 1 or pid == own_pid:
continue
try:
cmdline = (Path("/proc") / str(pid) / "cmdline").read_bytes()
except OSError:
continue
if _KAMIL_COLLECTOR_MARKER in cmdline:
self._signal_group(pid, signal.SIGKILL)
killed += 1
if killed:
logger.warning("Force-killed %d orphaned Kamil ADC collector process(es)", killed)
@staticmethod
def _log_abnormal_stop_exit(process: ManagedProcess) -> None:
"""Log a process that self-exited abnormally around stop time.
@@ -523,6 +558,9 @@ class ProcessSupervisor:
self._pidfile_path.write_text("", encoding="utf-8")
except OSError:
pass
# The ADC collector is a grandchild (spawned by the producer), not in the
# pidfile, so reap it separately by name.
self._kill_kamil_collectors()
def _is_stale_pipeline_pid(self, pid: int) -> bool:
"""Return whether `pid` still runs one of our pipeline binaries/scripts.