Skip to content

Commit a1aabde

Browse files
committed
ControlMode(core): Engine hook for hiding control client from attached sessions
why: Keep control-mode client filtering inside the engine instead of server and avoid teardown failures. what: - Add engine hook filter_attached_sessions and implement in ControlModeEngine using list-clients - Server.attached_sessions delegates to engine hook - Keep lints/types/tests green
1 parent 9532ddf commit a1aabde

File tree

3 files changed

+43
-8
lines changed

3 files changed

+43
-8
lines changed

src/libtmux/_internal/engines/base.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
from libtmux.common import tmux_cmd
1111

12+
if t.TYPE_CHECKING:
13+
from libtmux.session import Session
14+
1215

1316
class ExitStatus(enum.Enum):
1417
"""Exit status returned by tmux control mode commands."""
@@ -157,6 +160,14 @@ def iter_notifications(
157160
yield timeout
158161
return
159162

163+
# Optional hooks ---------------------------------------------------
164+
def filter_attached_sessions(
165+
self,
166+
sessions: list[Session],
167+
) -> list[Session]: # pragma: no cover - overridden by control mode
168+
"""Allow engines to hide internal clients from attached session lists."""
169+
return sessions
170+
160171
def get_stats(self) -> EngineStats: # pragma: no cover - default noop
161172
"""Return engine diagnostic stats."""
162173
return EngineStats(

src/libtmux/_internal/engines/control_mode.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
ControlProtocol,
2323
)
2424

25+
if t.TYPE_CHECKING:
26+
from libtmux.session import Session
27+
2528
logger = logging.getLogger(__name__)
2629

2730

@@ -175,6 +178,31 @@ def get_stats(self) -> EngineStats:
175178
"""Return diagnostic statistics for the engine."""
176179
return self._protocol.get_stats(restarts=self._restarts)
177180

181+
def filter_attached_sessions(self, sessions: list[Session]) -> list[Session]:
182+
"""Hide sessions that are only attached via the control-mode client."""
183+
if self.process is None or self.process.pid is None:
184+
return sessions
185+
186+
ctrl_pid = str(self.process.pid)
187+
188+
proc = self.run("list-clients", cmd_args=("-F#{client_pid} #{session_name}",))
189+
pid_map: dict[str, list[str]] = {}
190+
for line in proc.stdout:
191+
parts = line.split()
192+
if len(parts) >= 2:
193+
pid, sess_name = parts[0], parts[1]
194+
pid_map.setdefault(sess_name, []).append(pid)
195+
196+
filtered: list[Session] = []
197+
for sess_obj in sessions:
198+
pids = pid_map.get(sess_obj.session_name or "", [])
199+
# If the only attached client is the control client, hide it.
200+
if len(pids) == 1 and ctrl_pid in pids:
201+
continue
202+
filtered.append(sess_obj)
203+
204+
return filtered
205+
178206
# Internals ---------------------------------------------------------
179207
def _ensure_process(self, server_args: tuple[str | int, ...]) -> None:
180208
if self.process is None:

src/libtmux/server.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -361,14 +361,10 @@ def attached_sessions(self) -> list[Session]:
361361
"""
362362
sessions = list(self.sessions.filter(session_attached__noeq="1"))
363363

364-
# Hide internal control-mode client attachment.
365-
from libtmux._internal.engines.control_mode import ControlModeEngine
366-
367-
if isinstance(self.engine, ControlModeEngine):
368-
internal_names = self._get_internal_session_names()
369-
if self.engine._attach_to:
370-
internal_names.add(self.engine._attach_to)
371-
sessions = [s for s in sessions if s.session_name not in internal_names]
364+
# Let the engine hide its own internal client if it wants to.
365+
filter_fn = getattr(self.engine, "filter_attached_sessions", None)
366+
if callable(filter_fn):
367+
sessions = filter_fn(sessions)
372368

373369
return sessions
374370

0 commit comments

Comments
 (0)