some fixes

This commit is contained in:
Ayzen
2026-06-04 18:33:38 +03:00
parent eacea436a4
commit 22942d9dc9
26 changed files with 1352 additions and 153 deletions
+50 -11
View File
@@ -20,7 +20,9 @@ class ManagedProcess:
name: str
command: list[str]
allow_clean_exit: bool
handle: subprocess.Popen[str]
handle: subprocess.Popen[bytes]
stdout_path: Path
stderr_path: Path
@dataclass(slots=True)
@@ -141,30 +143,52 @@ class ProcessSupervisor:
self._stop_processes(["sweep_orchestrator", "data_preprocessor", "data_processor"])
def _spawn(self, name: str, command: list[str], *, allow_clean_exit: bool) -> None:
"""Spawn one process unless same process is already alive."""
"""Spawn one process unless same process is already alive.
Child stdout/stderr are redirected to per-process log files rather than
captured pipes: a long-running child (e.g. an acquisition producer waiting
for its device) would otherwise fill the OS pipe buffer once nobody drains
it and block on write. Files never back-pressure the child, and they keep
a persistent log we can read for exit reports and tail for diagnostics.
"""
existing = self._processes.get(name)
if existing is not None and existing.handle.poll() is None:
return
logs_dir = self._project_root / "python_app/runtime/logs"
logs_dir.mkdir(parents=True, exist_ok=True)
stdout_path = logs_dir / f"{name}.out.log"
stderr_path = logs_dir / f"{name}.err.log"
stdout_file = open(stdout_path, "wb")
stderr_file = open(stderr_path, "wb")
try:
handle = subprocess.Popen(
command,
cwd=self._project_root,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
stdout=stdout_file,
stderr=stderr_file,
)
except OSError as exc:
stdout_file.close()
stderr_file.close()
command_text = shlex.join(command)
raise RuntimeError(
f"Failed to spawn {name} with command `{command_text}` from `{self._project_root}`: "
f"{type(exc).__name__}: {exc}"
) from exc
finally:
# The child holds its own dup'd fds; the parent's copies are not needed.
stdout_file.close()
stderr_file.close()
self._processes[name] = ManagedProcess(
name=name,
command=command,
allow_clean_exit=allow_clean_exit,
handle=handle,
stdout_path=stdout_path,
stderr_path=stderr_path,
)
def _acquisition_command(self, config_path: Path) -> list[str]:
@@ -236,6 +260,25 @@ class ProcessSupervisor:
for name in exited_names:
self._processes.pop(name, None)
@staticmethod
def _read_log_tail(path: Path, max_bytes: int = 16384) -> str:
"""Return the trailing `max_bytes` of a child log file, decoded best-effort.
Bounded so a large/long-lived log never produces an enormous exit report.
"""
try:
with open(path, "rb") as handle:
handle.seek(0, 2)
size = handle.tell()
if size > max_bytes:
handle.seek(-max_bytes, 2)
else:
handle.seek(0)
data = handle.read()
except OSError:
return ""
return data.decode("utf-8", errors="replace").strip()
def _is_alive(self, name: str) -> bool:
"""Return `True` when named process handle exists and is running."""
process = self._processes.get(name)
@@ -253,12 +296,8 @@ class ProcessSupervisor:
if return_code is None:
continue
stderr = ""
stdout = ""
if process.handle.stdout is not None:
stdout = process.handle.stdout.read().strip()
if process.handle.stderr is not None:
stderr = process.handle.stderr.read().strip()
stdout = self._read_log_tail(process.stdout_path)
stderr = self._read_log_tail(process.stderr_path)
reports.append(
ProcessExitReport(