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.
This commit is contained in:
2026-06-02 14:53:16 +00:00
parent 85e73e013d
commit c2176117fa
2 changed files with 13 additions and 1 deletions
+8 -1
View File
@@ -245,7 +245,12 @@ class _Supervisor:
except ProcessLookupError: except ProcessLookupError:
pass 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: def exit_code(self) -> int:
"""Positive child failures win; otherwise report success. """Positive child failures win; otherwise report success.
@@ -335,6 +340,8 @@ class _Supervisor:
except ProcessLookupError: except ProcessLookupError:
pass pass
p.wait() p.wait()
if p.stdout is not None:
p.stdout.close()
self._logged_dead.discard(daemon_name) self._logged_dead.discard(daemon_name)
new_proc = _spawn(spec) new_proc = _spawn(spec)
self.procs[idx] = (spec, new_proc) self.procs[idx] = (spec, new_proc)
+5
View File
@@ -14,6 +14,7 @@ import subprocess
import sys import sys
import time import time
import unittest import unittest
import warnings
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
@@ -135,6 +136,10 @@ class TestSupervisor(unittest.TestCase):
We don't go through `main()` because main installs signal We don't go through `main()` because main installs signal
handlers process-wide, which collides with the test runner.""" 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: def _drive(self, sup: _Supervisor, max_wait_s: float = 6.0) -> int:
deadline = time.monotonic() + max_wait_s deadline = time.monotonic() + max_wait_s
while not sup.tick(): while not sup.tick():