Skip to content

Commit 7c4e635

Browse files
committed
Server(refactor[sessions]): Use engine internal filters
why: keep engine transparency without reaching into control-mode internals. what: - hide management sessions via engine.internal_session_names - route attached_sessions through engine.exclude_internal_sessions hook - preserve existing server arg handling and attach behaviour
1 parent a8d81cd commit 7c4e635

File tree

1 file changed

+215
-28
lines changed

1 file changed

+215
-28
lines changed

src/libtmux/server.py

Lines changed: 215 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import warnings
1717

1818
from libtmux import exc, formats
19+
from libtmux._internal.engines.subprocess_engine import SubprocessEngine
1920
from libtmux._internal.query_list import QueryList
2021
from libtmux.common import tmux_cmd
2122
from libtmux.constants import OptionScope
@@ -40,6 +41,7 @@
4041

4142
from typing_extensions import Self
4243

44+
from libtmux._internal.engines.base import Engine
4345
from libtmux._internal.types import StrPath
4446

4547
DashLiteral: TypeAlias = t.Literal["-"]
@@ -78,17 +80,17 @@ class Server(
7880
>>> server
7981
Server(socket_name=libtmux_test...)
8082
81-
>>> server.sessions
82-
[Session($1 ...)]
83+
>>> server.sessions # doctest: +ELLIPSIS
84+
[Session($... ...)]
8385
84-
>>> server.sessions[0].windows
85-
[Window(@1 1:..., Session($1 ...))]
86+
>>> server.sessions[0].windows # doctest: +ELLIPSIS
87+
[Window(@... ..., Session($... ...))]
8688
87-
>>> server.sessions[0].active_window
88-
Window(@1 1:..., Session($1 ...))
89+
>>> server.sessions[0].active_window # doctest: +ELLIPSIS
90+
Window(@... ..., Session($... ...))
8991
90-
>>> server.sessions[0].active_pane
91-
Pane(%1 Window(@1 1:..., Session($1 ...)))
92+
>>> server.sessions[0].active_pane # doctest: +ELLIPSIS
93+
Pane(%... Window(@... ..., Session($... ...)))
9294
9395
The server can be used as a context manager to ensure proper cleanup:
9496
@@ -137,12 +139,17 @@ def __init__(
137139
colors: int | None = None,
138140
on_init: t.Callable[[Server], None] | None = None,
139141
socket_name_factory: t.Callable[[], str] | None = None,
142+
engine: Engine | None = None,
140143
**kwargs: t.Any,
141144
) -> None:
142145
EnvironmentMixin.__init__(self, "-g")
143146
self._windows: list[WindowDict] = []
144147
self._panes: list[PaneDict] = []
145148

149+
if engine is None:
150+
engine = SubprocessEngine()
151+
self.engine = engine
152+
146153
if socket_path is not None:
147154
self.socket_path = socket_path
148155
elif socket_name is not None:
@@ -205,6 +212,12 @@ def is_alive(self) -> bool:
205212
>>> tmux = Server(socket_name="no_exist")
206213
>>> assert not tmux.is_alive()
207214
"""
215+
# Avoid spinning up control-mode just to probe.
216+
from libtmux._internal.engines.control_mode import ControlModeEngine
217+
218+
if isinstance(self.engine, ControlModeEngine):
219+
return self._probe_server() == 0
220+
208221
try:
209222
res = self.cmd("list-sessions")
210223
except Exception:
@@ -221,23 +234,57 @@ def raise_if_dead(self) -> None:
221234
... print(type(e))
222235
<class 'subprocess.CalledProcessError'>
223236
"""
237+
from libtmux._internal.engines.control_mode import ControlModeEngine
238+
239+
if isinstance(self.engine, ControlModeEngine):
240+
rc = self._probe_server()
241+
if rc != 0:
242+
tmux_bin_probe = shutil.which("tmux") or "tmux"
243+
raise subprocess.CalledProcessError(
244+
returncode=rc,
245+
cmd=[tmux_bin_probe, *self._build_server_args(), "list-sessions"],
246+
)
247+
return
248+
224249
tmux_bin = shutil.which("tmux")
225250
if tmux_bin is None:
226251
raise exc.TmuxCommandNotFound
227252

228-
cmd_args: list[str] = ["list-sessions"]
253+
server_args = self._build_server_args()
254+
proc = self.engine.run("list-sessions", server_args=server_args)
255+
if proc.returncode is not None and proc.returncode != 0:
256+
raise subprocess.CalledProcessError(
257+
returncode=proc.returncode,
258+
cmd=[tmux_bin, *server_args, "list-sessions"],
259+
)
260+
261+
#
262+
# Command
263+
#
264+
def _build_server_args(self) -> list[str]:
265+
"""Return tmux server args based on socket/config settings."""
266+
server_args: list[str] = []
229267
if self.socket_name:
230-
cmd_args.insert(0, f"-L{self.socket_name}")
268+
server_args.append(f"-L{self.socket_name}")
231269
if self.socket_path:
232-
cmd_args.insert(0, f"-S{self.socket_path}")
270+
server_args.append(f"-S{self.socket_path}")
233271
if self.config_file:
234-
cmd_args.insert(0, f"-f{self.config_file}")
272+
server_args.append(f"-f{self.config_file}")
273+
return server_args
235274

236-
subprocess.check_call([tmux_bin, *cmd_args])
275+
def _probe_server(self) -> int:
276+
"""Check server liveness without bootstrapping control mode."""
277+
tmux_bin = shutil.which("tmux")
278+
if tmux_bin is None:
279+
raise exc.TmuxCommandNotFound
280+
281+
result = subprocess.run(
282+
[tmux_bin, *self._build_server_args(), "list-sessions"],
283+
check=False,
284+
capture_output=True,
285+
)
286+
return result.returncode
237287

238-
#
239-
# Command
240-
#
241288
def cmd(
242289
self,
243290
cmd: str,
@@ -291,25 +338,24 @@ def cmd(
291338
292339
Renamed from ``.tmux`` to ``.cmd``.
293340
"""
294-
svr_args: list[str | int] = [cmd]
295-
cmd_args: list[str | int] = []
341+
server_args: list[str | int] = []
296342
if self.socket_name:
297-
svr_args.insert(0, f"-L{self.socket_name}")
343+
server_args.append(f"-L{self.socket_name}")
298344
if self.socket_path:
299-
svr_args.insert(0, f"-S{self.socket_path}")
345+
server_args.append(f"-S{self.socket_path}")
300346
if self.config_file:
301-
svr_args.insert(0, f"-f{self.config_file}")
347+
server_args.append(f"-f{self.config_file}")
302348
if self.colors:
303349
if self.colors == 256:
304-
svr_args.insert(0, "-2")
350+
server_args.append("-2")
305351
elif self.colors == 88:
306-
svr_args.insert(0, "-8")
352+
server_args.append("-8")
307353
else:
308354
raise exc.UnknownColorOption
309355

310-
cmd_args = ["-t", str(target), *args] if target is not None else [*args]
356+
cmd_args = ["-t", str(target), *args] if target is not None else list(args)
311357

312-
return tmux_cmd(*svr_args, *cmd_args)
358+
return self.engine.run(cmd, cmd_args=cmd_args, server_args=server_args)
313359

314360
@property
315361
def attached_sessions(self) -> list[Session]:
@@ -324,10 +370,28 @@ def attached_sessions(self) -> list[Session]:
324370
-------
325371
list of :class:`Session`
326372
"""
327-
return self.sessions.filter(session_attached__noeq="1")
373+
sessions = list(self.sessions.filter(session_attached__noeq="1"))
374+
375+
# Let the engine hide its own internal client if it wants to.
376+
filter_fn = getattr(self.engine, "exclude_internal_sessions", None)
377+
if callable(filter_fn):
378+
server_args = tuple(self._build_server_args())
379+
try:
380+
sessions = filter_fn(
381+
sessions,
382+
server_args=server_args,
383+
)
384+
except TypeError:
385+
# Subprocess engine does not accept server_args; ignore.
386+
sessions = filter_fn(sessions)
387+
388+
return sessions
328389

329390
def has_session(self, target_session: str, exact: bool = True) -> bool:
330-
"""Return True if session exists.
391+
"""Return True if session exists (excluding internal engine sessions).
392+
393+
Internal sessions used by engines for connection management are
394+
excluded to maintain engine transparency.
331395
332396
Parameters
333397
----------
@@ -347,6 +411,11 @@ def has_session(self, target_session: str, exact: bool = True) -> bool:
347411
"""
348412
session_check_name(target_session)
349413

414+
# Never report internal engine sessions as existing
415+
internal_names = self._get_internal_session_names()
416+
if target_session in internal_names:
417+
return False
418+
350419
if exact:
351420
target_session = f"={target_session}"
352421

@@ -412,6 +481,15 @@ def switch_client(self, target_session: str) -> None:
412481
"""
413482
session_check_name(target_session)
414483

484+
server_args = tuple(self._build_server_args())
485+
486+
# If the engine knows there are no "real" clients, mirror tmux's
487+
# `no current client` error before dispatching.
488+
can_switch = getattr(self.engine, "can_switch_client", None)
489+
if callable(can_switch) and not can_switch(server_args=server_args):
490+
msg = "no current client"
491+
raise exc.LibTmuxException(msg)
492+
415493
proc = self.cmd("switch-client", target=target_session)
416494

417495
if proc.stderr:
@@ -435,6 +513,78 @@ def attach_session(self, target_session: str | None = None) -> None:
435513
if proc.stderr:
436514
raise exc.LibTmuxException(proc.stderr)
437515

516+
def connect(self, session_name: str) -> Session:
517+
"""Connect to a session, creating if it doesn't exist.
518+
519+
Returns an existing session if found, otherwise creates a new detached session.
520+
521+
Parameters
522+
----------
523+
session_name : str
524+
Name of the session to connect to.
525+
526+
Returns
527+
-------
528+
:class:`Session`
529+
The connected or newly created session.
530+
531+
Raises
532+
------
533+
:exc:`exc.BadSessionName`
534+
If the session name is invalid (contains '.' or ':').
535+
:exc:`exc.LibTmuxException`
536+
If tmux returns an error.
537+
538+
Examples
539+
--------
540+
>>> session = server.connect('my_session')
541+
>>> session.name
542+
'my_session'
543+
544+
Calling again returns the same session:
545+
546+
>>> session2 = server.connect('my_session')
547+
>>> session2.session_id == session.session_id
548+
True
549+
"""
550+
session_check_name(session_name)
551+
552+
# Check if session already exists
553+
if self.has_session(session_name):
554+
session = self.sessions.get(session_name=session_name)
555+
if session is None:
556+
msg = "Session lookup failed after has_session passed"
557+
raise exc.LibTmuxException(msg)
558+
return session
559+
560+
# Session doesn't exist, create it
561+
# Save and clear TMUX env var (same as new_session)
562+
env = os.environ.get("TMUX")
563+
if env:
564+
del os.environ["TMUX"]
565+
566+
proc = self.cmd(
567+
"new-session",
568+
"-d",
569+
f"-s{session_name}",
570+
"-P",
571+
"-F#{session_id}",
572+
)
573+
574+
if proc.stderr:
575+
raise exc.LibTmuxException(proc.stderr)
576+
577+
session_id = proc.stdout[0]
578+
579+
# Restore TMUX env var
580+
if env:
581+
os.environ["TMUX"] = env
582+
583+
return Session.from_session_id(
584+
server=self,
585+
session_id=session_id,
586+
)
587+
438588
def new_session(
439589
self,
440590
session_name: str | None = None,
@@ -596,14 +746,51 @@ def new_session(
596746
#
597747
# Relations
598748
#
749+
def _get_internal_session_names(self) -> set[str]:
750+
"""Get session names used internally by the engine for management."""
751+
internal_names: set[str] = set(
752+
getattr(self.engine, "internal_session_names", set()),
753+
)
754+
try:
755+
return set(internal_names)
756+
except Exception: # pragma: no cover - defensive
757+
return set()
758+
599759
@property
600760
def sessions(self) -> QueryList[Session]:
601-
"""Sessions contained in server.
761+
"""Sessions contained in server (excluding internal engine sessions).
762+
763+
Internal sessions are used by engines for connection management
764+
(e.g., control mode maintains a persistent connection session).
765+
These are automatically filtered to maintain engine transparency.
766+
767+
For advanced debugging, use the internal :meth:`._sessions_all()` method.
602768
603769
Can be accessed via
604770
:meth:`.sessions.get() <libtmux._internal.query_list.QueryList.get()>` and
605771
:meth:`.sessions.filter() <libtmux._internal.query_list.QueryList.filter()>`
606772
"""
773+
all_sessions = self._sessions_all()
774+
775+
# Filter out internal engine sessions
776+
internal_names = self._get_internal_session_names()
777+
filtered_sessions = [
778+
s for s in all_sessions if s.session_name not in internal_names
779+
]
780+
781+
return QueryList(filtered_sessions)
782+
783+
def _sessions_all(self) -> QueryList[Session]:
784+
"""Return all sessions including internal engine sessions.
785+
786+
Used internally for engine management and advanced debugging.
787+
Most users should use the :attr:`.sessions` property instead.
788+
789+
Returns
790+
-------
791+
QueryList[Session]
792+
All sessions including internal ones used by engines.
793+
"""
607794
sessions: list[Session] = []
608795

609796
try:

0 commit comments

Comments
 (0)