From 5a2011c48ffc767d8a576f7dfee04473f4ab4e13 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 2 Jun 2026 14:53:16 +0000 Subject: [PATCH] fix: close child stdout pipes on restart and loop convergence (PRD 0043) Closes #140. In restart_daemon, the old process's stdout pipe was never explicitly closed after p.wait() returned, leaking the fd until the supervisor object was GC'd. Similarly, when the watch loop converged (all children dead), no pipe was closed. Both paths now call p.stdout.close() immediately after the process is confirmed exited. Tests enforce this with warnings.simplefilter("error", ResourceWarning) in TestSupervisor.setUp. --- bot_bottle/sidecar_init.py | 9 ++++++++- tests/unit/test_sidecar_init.py | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/bot_bottle/sidecar_init.py b/bot_bottle/sidecar_init.py index 8bfa984..2b303a6 100644 --- a/bot_bottle/sidecar_init.py +++ b/bot_bottle/sidecar_init.py @@ -245,7 +245,12 @@ class _Supervisor: except ProcessLookupError: pass - return all(p.poll() is not None for _, p in self.procs) + done = all(p.poll() is not None for _, p in self.procs) + if done: + for _, p in self.procs: + if p.stdout is not None: + p.stdout.close() + return done def exit_code(self) -> int: """Positive child failures win; otherwise report success. @@ -335,6 +340,8 @@ class _Supervisor: except ProcessLookupError: pass p.wait() + if p.stdout is not None: + p.stdout.close() self._logged_dead.discard(daemon_name) new_proc = _spawn(spec) self.procs[idx] = (spec, new_proc) diff --git a/tests/unit/test_sidecar_init.py b/tests/unit/test_sidecar_init.py index 88f0b9b..1256af0 100644 --- a/tests/unit/test_sidecar_init.py +++ b/tests/unit/test_sidecar_init.py @@ -14,6 +14,7 @@ import subprocess import sys import time import unittest +import warnings from pathlib import Path from unittest.mock import patch @@ -135,6 +136,10 @@ class TestSupervisor(unittest.TestCase): We don't go through `main()` because main installs signal handlers process-wide, which collides with the test runner.""" + def setUp(self): + warnings.simplefilter("error", ResourceWarning) + self.addCleanup(warnings.resetwarnings) + def _drive(self, sup: _Supervisor, max_wait_s: float = 6.0) -> int: deadline = time.monotonic() + max_wait_s while not sup.tick():