Skip to content

Commit b4736f7

Browse files
committed
ControlModeEngine(feat[testability]): Allow injectable process factory
why: Enable deterministic restart/timeout testing and future transport customization. what: - Add optional process_factory hook to control-mode engine for test fakes - Make close resilient to stub processes lacking terminate/kill - Count command timeout toward restart tally for diagnostics
1 parent 3ed5e34 commit b4736f7

File tree

1 file changed

+17
-5
lines changed

1 file changed

+17
-5
lines changed

src/libtmux/_internal/engines/control_mode.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __init__(
4646
notification_queue_size: int = 4096,
4747
internal_session_name: str | None = None,
4848
attach_to: str | None = None,
49+
process_factory: t.Callable[[list[str]], subprocess.Popen[str]] | None = None,
4950
) -> None:
5051
"""Initialize control mode engine.
5152
@@ -68,6 +69,10 @@ def __init__(
6869
.. warning::
6970
Attaching to user sessions can cause notification spam from
7071
pane output. Use for advanced scenarios only.
72+
process_factory : Callable[[list[str]], subprocess.Popen], optional
73+
Test hook to override how the tmux control-mode process is created.
74+
When provided, it receives the argv list and must return an object
75+
compatible with ``subprocess.Popen`` (stdin/stdout/stderr streams).
7176
"""
7277
self.process: subprocess.Popen[str] | None = None
7378
self._lock = threading.Lock()
@@ -83,6 +88,7 @@ def __init__(
8388
self._restarts = 0
8489
self._internal_session_name = internal_session_name or "libtmux_control_mode"
8590
self._attach_to = attach_to
91+
self._process_factory = process_factory
8692

8793
# Lifecycle ---------------------------------------------------------
8894
def close(self) -> None:
@@ -92,11 +98,15 @@ def close(self) -> None:
9298
return
9399

94100
try:
95-
proc.terminate()
96-
proc.wait(timeout=1)
101+
if hasattr(proc, "terminate"):
102+
proc.terminate() # type: ignore[call-arg]
103+
if hasattr(proc, "wait"):
104+
proc.wait(timeout=1) # type: ignore[call-arg]
97105
except subprocess.TimeoutExpired:
98-
proc.kill()
99-
proc.wait()
106+
if hasattr(proc, "kill"):
107+
proc.kill() # type: ignore[call-arg]
108+
if hasattr(proc, "wait"):
109+
proc.wait() # type: ignore[call-arg]
100110
finally:
101111
self.process = None
102112
self._server_args = None
@@ -147,6 +157,7 @@ def run_result(
147157
# Wait outside the lock so multiple callers can run concurrently
148158
if not ctx.wait(timeout=effective_timeout):
149159
self.close()
160+
self._restarts += 1
150161
msg = "tmux control mode command timed out"
151162
raise exc.ControlModeTimeout(msg)
152163

@@ -350,7 +361,8 @@ def _start_process(self, server_args: tuple[str | int, ...]) -> None:
350361
]
351362

352363
logger.debug("Starting Control Mode process: %s", cmd)
353-
self.process = subprocess.Popen(
364+
popen_factory = self._process_factory or subprocess.Popen
365+
self.process = popen_factory( # type: ignore[arg-type]
354366
cmd,
355367
stdin=subprocess.PIPE,
356368
stdout=subprocess.PIPE,

0 commit comments

Comments
 (0)