Skip to content

Commit 1a76783

Browse files
committed
ControlModeEngine(threading): Non-daemon threads with explicit join
Change reader and stderr threads from daemon=True to daemon=False, with explicit join() in close() method. This provides: - Clean shutdown: threads complete gracefully instead of being orphaned - No race conditions between close() and thread termination - Deterministic lifecycle for debugging and testing The 3 ScriptedProcess/ProcessFactory tests still fail - they require Part 5 (ScriptedStdout queue-based iterator) to fix the tuple stdout consumption race. The threading fix is still valuable for production correctness.
1 parent 944879d commit 1a76783

File tree

1 file changed

+14
-3
lines changed

1 file changed

+14
-3
lines changed

src/libtmux/_internal/engines/control_mode.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,11 @@ def __init__(
149149

150150
# Lifecycle ---------------------------------------------------------
151151
def close(self) -> None:
152-
"""Terminate the tmux control mode process and clean up threads."""
152+
"""Terminate the tmux control mode process and clean up threads.
153+
154+
Terminates the subprocess and waits for reader/stderr threads to
155+
finish. Non-daemon threads ensure clean shutdown without races.
156+
"""
153157
proc = self.process
154158
if proc is None:
155159
return
@@ -165,6 +169,12 @@ def close(self) -> None:
165169
self._server_args = None
166170
self._protocol.mark_dead("engine closed")
167171

172+
# Join threads to ensure clean shutdown (non-daemon threads)
173+
if self._reader_thread is not None and self._reader_thread.is_alive():
174+
self._reader_thread.join(timeout=2)
175+
if self._stderr_thread is not None and self._stderr_thread.is_alive():
176+
self._stderr_thread.join(timeout=2)
177+
168178
def __del__(self) -> None: # pragma: no cover - best effort cleanup
169179
"""Ensure subprocess is terminated on GC."""
170180
self.close()
@@ -481,18 +491,19 @@ def _start_process(self, server_args: tuple[str | int, ...]) -> None:
481491
self._protocol.register_command(bootstrap_ctx)
482492

483493
# Start IO threads after registration to avoid early protocol errors.
494+
# Non-daemon threads ensure clean shutdown via join() in close().
484495
if self._start_threads:
485496
self._reader_thread = threading.Thread(
486497
target=self._reader,
487498
args=(self.process,),
488-
daemon=True,
499+
daemon=False,
489500
)
490501
self._reader_thread.start()
491502

492503
self._stderr_thread = threading.Thread(
493504
target=self._drain_stderr,
494505
args=(self.process,),
495-
daemon=True,
506+
daemon=False,
496507
)
497508
self._stderr_thread.start()
498509

0 commit comments

Comments
 (0)