Skip to content

Commit 9c7d244

Browse files
committed
ControlModeEngine(feat[testability]): Configurable retries and thread hook
why: Investigate restart/timeout semantics with fakes; avoid mypy casts and allow protocol-fed fakes. what: - Add max_retries and start_threads hooks to control-mode engine ctor - Retry loop now respects max_retries, increments restarts on timeout - Allow skipping reader/stderr threads for test fakes - Update timeout tests to use start_threads=False
1 parent 0b28544 commit 9c7d244

File tree

2 files changed

+26
-15
lines changed

2 files changed

+26
-15
lines changed

src/libtmux/_internal/engines/control_mode.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ def __init__(
8080
internal_session_name: str | None = None,
8181
attach_to: str | None = None,
8282
process_factory: _ProcessFactory | None = None,
83+
max_retries: int = 1,
84+
start_threads: bool = True,
8385
) -> None:
8486
"""Initialize control mode engine.
8587
@@ -106,6 +108,12 @@ def __init__(
106108
Test hook to override how the tmux control-mode process is created.
107109
When provided, it receives the argv list and must return an object
108110
compatible with ``subprocess.Popen`` (stdin/stdout/stderr streams).
111+
max_retries : int, optional
112+
Number of times to retry a command after a BrokenPipeError while
113+
writing to the control-mode process. Default: 1.
114+
start_threads : bool, optional
115+
Internal/testing hook to skip spawning reader/stderr threads when
116+
using a fake process that feeds the protocol directly. Default: True.
109117
"""
110118
self.process: _ControlProcess | None = None
111119
self._lock = threading.Lock()
@@ -122,6 +130,8 @@ def __init__(
122130
self._internal_session_name = internal_session_name or "libtmux_control_mode"
123131
self._attach_to = attach_to
124132
self._process_factory = process_factory
133+
self._max_retries = max(0, max_retries)
134+
self._start_threads = start_threads
125135

126136
# Lifecycle ---------------------------------------------------------
127137
def close(self) -> None:
@@ -178,7 +188,7 @@ def run_result(
178188
try:
179189
self._write_line(command_line, server_args=incoming_server_args)
180190
except exc.ControlModeConnectionError:
181-
if attempts >= 2:
191+
if attempts > self._max_retries:
182192
raise
183193
# retry the full cycle with a fresh process/context
184194
continue
@@ -409,19 +419,20 @@ def _start_process(self, server_args: tuple[str | int, ...]) -> None:
409419
self._protocol.register_command(bootstrap_ctx)
410420

411421
# Start IO threads after registration to avoid early protocol errors.
412-
self._reader_thread = threading.Thread(
413-
target=self._reader,
414-
args=(self.process,),
415-
daemon=True,
416-
)
417-
self._reader_thread.start()
422+
if self._start_threads:
423+
self._reader_thread = threading.Thread(
424+
target=self._reader,
425+
args=(self.process,),
426+
daemon=True,
427+
)
428+
self._reader_thread.start()
418429

419-
self._stderr_thread = threading.Thread(
420-
target=self._drain_stderr,
421-
args=(self.process,),
422-
daemon=True,
423-
)
424-
self._stderr_thread.start()
430+
self._stderr_thread = threading.Thread(
431+
target=self._drain_stderr,
432+
args=(self.process,),
433+
daemon=True,
434+
)
435+
self._stderr_thread.start()
425436

426437
if not bootstrap_ctx.wait(timeout=self.command_timeout):
427438
self.close()

tests/test_control_mode_engine.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def wait(self, timeout: float | None = None) -> int | None: # pragma: no cover
9999
def poll(self) -> int | None: # pragma: no cover - simple stub
100100
return 0
101101

102-
engine = ControlModeEngine(command_timeout=0.01)
102+
engine = ControlModeEngine(command_timeout=0.01, start_threads=False)
103103

104104
fake_process: _ControlProcess = FakeProcess()
105105

@@ -139,7 +139,7 @@ def wait(self, timeout: float | None = None) -> int | None:
139139
def poll(self) -> int | None:
140140
return 0
141141

142-
engine = ControlModeEngine(command_timeout=5.0)
142+
engine = ControlModeEngine(command_timeout=5.0, start_threads=False)
143143

144144
def fake_start(server_args: t.Sequence[str | int] | None) -> None:
145145
engine.tmux_bin = "tmux"

0 commit comments

Comments
 (0)