some fixes
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user