Skip to content

Commit 744e5a7

Browse files
committed
ControlModeEngine(fix[shutdown]): avoid hang on shutdown
why: prevent control-mode threads from blocking interpreter exit if cleanup stalls. what: - Close control pipes before joining reader/stderr threads - Run IO threads as daemon and update shutdown comment
1 parent 687b77e commit 744e5a7

1 file changed

Lines changed: 12 additions & 11 deletions

File tree

src/libtmux/_internal/engines/control_mode.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ def close(self) -> None:
159159
"""Terminate the tmux control mode process and clean up threads.
160160
161161
Terminates the subprocess and waits for reader/stderr threads to
162-
finish. Non-daemon threads ensure clean shutdown without races.
162+
finish. Threads are daemonized to avoid hanging interpreter shutdown
163+
if cleanup is interrupted, but close() still joins for a clean exit.
163164
"""
164165
proc = self.process
165166
if proc is None:
@@ -178,7 +179,13 @@ def close(self) -> None:
178179
self._shutdown_timeout,
179180
)
180181
finally:
181-
# Join threads to ensure clean shutdown (non-daemon threads).
182+
# Close pipes first to unblock reader/stderr threads.
183+
for stream in (proc.stdin, proc.stdout, proc.stderr):
184+
if isinstance(stream, io.IOBase):
185+
with contextlib.suppress(Exception):
186+
stream.close()
187+
188+
# Join threads to ensure clean shutdown.
182189
# Skip join if called from within the thread itself (e.g., during GC).
183190
current = threading.current_thread()
184191
if (
@@ -204,12 +211,6 @@ def close(self) -> None:
204211
self._shutdown_timeout,
205212
)
206213

207-
# Close pipes to avoid unraisable BrokenPipe errors on GC.
208-
for stream in (proc.stdin, proc.stdout, proc.stderr):
209-
if isinstance(stream, io.IOBase):
210-
with contextlib.suppress(Exception):
211-
stream.close()
212-
213214
self.process = None
214215
self._server_args = None
215216
self._protocol.mark_dead("engine closed")
@@ -686,19 +687,19 @@ def _start_process(self, server_args: tuple[str | int, ...]) -> None:
686687
self._protocol.register_command(bootstrap_ctx)
687688

688689
# Start IO threads after registration to avoid early protocol errors.
689-
# Non-daemon threads ensure clean shutdown via join() in close().
690+
# Daemon threads prevent interpreter hang if cleanup is interrupted.
690691
if self._start_threads:
691692
self._reader_thread = threading.Thread(
692693
target=self._reader,
693694
args=(self.process,),
694-
daemon=False,
695+
daemon=True,
695696
)
696697
self._reader_thread.start()
697698

698699
self._stderr_thread = threading.Thread(
699700
target=self._drain_stderr,
700701
args=(self.process,),
701-
daemon=False,
702+
daemon=True,
702703
)
703704
self._stderr_thread.start()
704705

0 commit comments

Comments
 (0)