Skip to content

Commit e712f3e

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 5c15ae4 commit e712f3e

1 file changed

Lines changed: 215 additions & 28 deletions

File tree

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]:
@@ -325,10 +371,28 @@ def attached_sessions(self) -> list[Session]:
325371
list[:class:`Session`]
326372
Sessions that are attached.
327373
"""
328-
return self.sessions.filter(session_attached__noeq="1")
374+
sessions = list(self.sessions.filter(session_attached__noeq="1"))
375+
376+
# Let the engine hide its own internal client if it wants to.
377+
filter_fn = getattr(self.engine, "exclude_internal_sessions", None)
378+
if callable(filter_fn):
379+
server_args = tuple(self._build_server_args())
380+
try:
381+
sessions = filter_fn(
382+
sessions,
383+
server_args=server_args,
384+
)
385+
except TypeError:
386+
# Subprocess engine does not accept server_args; ignore.
387+
sessions = filter_fn(sessions)
388+
389+
return sessions
329390

330391
def has_session(self, target_session: str, exact: bool = True) -> bool:
331-
"""Return True if session exists.
392+
"""Return True if session exists (excluding internal engine sessions).
393+
394+
Internal sessions used by engines for connection management are
395+
excluded to maintain engine transparency.
332396
333397
Parameters
334398
----------
@@ -348,6 +412,11 @@ def has_session(self, target_session: str, exact: bool = True) -> bool:
348412
"""
349413
session_check_name(target_session)
350414

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

@@ -413,6 +482,15 @@ def switch_client(self, target_session: str) -> None:
413482
"""
414483
session_check_name(target_session)
415484

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

418496
if proc.stderr:
@@ -436,6 +514,78 @@ def attach_session(self, target_session: str | None = None) -> None:
436514
if proc.stderr:
437515
raise exc.LibTmuxException(proc.stderr)
438516

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

610797
try:

0 commit comments

Comments
 (0)