3434
3535import os
3636import pathlib
37+ import sys
3738import typing as t
3839
3940from libtmux import exc
4041
42+ if t .TYPE_CHECKING :
43+ from libtmux ._internal .types import StrPath
44+
4145TMUX : t .Final = "TMUX"
4246"""Environment variable tmux exports with ``socket_path,server_pid,session_id``."""
4347
5357DEFAULT_SOCKET_NAME : t .Final = "default"
5458"""Socket name tmux uses when neither ``-L`` nor ``-S`` was given."""
5559
60+ # ``sun_path`` in ``struct sockaddr_un`` is a fixed-size char array, and the
61+ # stdlib publishes no constant for its size, so it is spelled out per platform.
62+ # The size is part of each platform's frozen ABI: 104 bytes on the BSD-derived
63+ # kernels (macOS, FreeBSD, OpenBSD, NetBSD), 108 on Linux and elsewhere. One
64+ # byte of it is the NUL terminator. The test suite probes the running kernel to
65+ # keep this honest, which reads better than bisecting for the limit at import
66+ # time.
67+ _SUN_PATH_SIZE : t .Final = (
68+ 104 if sys .platform .startswith (("darwin" , "freebsd" , "openbsd" , "netbsd" )) else 108
69+ )
70+
71+ SOCKET_PATH_MAX_BYTES : t .Final = _SUN_PATH_SIZE - 1
72+ """Bytes a tmux socket path may occupy on this platform."""
73+
5674
5775def resolve_env (env : t .Mapping [str , str ] | None = None ) -> t .Mapping [str , str ]:
5876 """Return *env*, defaulting to the live process environment.
@@ -91,6 +109,13 @@ def resolve_socket_path(
91109 resolved through symlinks, as tmux resolves it before binding, so a
92110 symlinked ``$TMUX_TMPDIR`` yields the path tmux itself reports.
93111
112+ A ``$TMUX_TMPDIR`` tmux cannot resolve falls back the same way. tmux takes
113+ the first of ``$TMUX_TMPDIR`` and ``/tmp`` that resolves, so a path that is
114+ not there -- or a broken symlink -- is never the one it binds, and
115+ measuring it would refuse a server tmux reaches without difficulty. A
116+ directory it resolves but cannot create ``tmux-<euid>`` under is an error
117+ from tmux, not a fallback.
118+
94119 The path is *computed*, not observed: it says where tmux would put the
95120 socket, not that a daemon is listening there. Code holding a live
96121 :class:`~libtmux.Server` should ask tmux instead, with the
@@ -115,22 +140,97 @@ def resolve_socket_path(
115140 >>> resolve_socket_path(env={})
116141 PosixPath('/tmp/tmux-.../default')
117142
118- >>> resolve_socket_path("mysocket", env={"TMUX_TMPDIR": "/run/user/1000 "})
119- PosixPath('/run/user/1000 /tmux-.../mysocket')
143+ >>> resolve_socket_path("mysocket", env={"TMUX_TMPDIR": "/usr "})
144+ PosixPath('/usr /tmux-.../mysocket')
120145
121146 ``$TMPDIR`` is not a socket directory, so it changes nothing:
122147
123148 >>> resolve_socket_path(env={"TMPDIR": "/var/folders/xy"})
124149 PosixPath('/tmp/tmux-.../default')
150+
151+ Nor does a ``$TMUX_TMPDIR`` tmux cannot use, however long it is:
152+
153+ >>> resolve_socket_path(env={"TMUX_TMPDIR": "/nonexistent-" + "d" * 200})
154+ PosixPath('/tmp/tmux-.../default')
125155 """
126156 tmpdir = resolve_env (env ).get (TMUX_TMPDIR ) or DEFAULT_SOCKET_DIR
157+ base = pathlib .Path (tmpdir )
158+ if not base .exists ():
159+ base = pathlib .Path (DEFAULT_SOCKET_DIR )
127160 return (
128- pathlib .Path (tmpdir ).resolve ()
129- / f"tmux-{ os .geteuid ()} "
130- / (socket_name or DEFAULT_SOCKET_NAME )
161+ base .resolve () / f"tmux-{ os .geteuid ()} " / (socket_name or DEFAULT_SOCKET_NAME )
131162 )
132163
133164
165+ def check_socket_path_length (
166+ socket_path : StrPath ,
167+ * ,
168+ socket_name : str | None = None ,
169+ env_var : str | None = None ,
170+ env_value : str | None = None ,
171+ ) -> None :
172+ """Raise if *socket_path* is too long to be a UNIX socket address.
173+
174+ A tmux socket is a UNIX domain socket, so its path has to fit in
175+ :data:`SOCKET_PATH_MAX_BYTES` -- a filesystem that accepts the path says
176+ nothing about whether a socket can be bound at it. Length is counted in
177+ *bytes*, as the kernel counts it, so a non-ASCII path runs out sooner than
178+ its character count suggests.
179+
180+ Parameters
181+ ----------
182+ socket_path : str or :class:`os.PathLike`
183+ Path to measure.
184+ socket_name : str, optional
185+ Socket name *socket_path* was resolved from, when it was resolved
186+ rather than passed in. Recorded on the exception so the message can say
187+ the length was inherited from ``$TMUX_TMPDIR``.
188+ env_var : str, optional
189+ Environment variable the socket directory came from, when one did.
190+ Recorded on the exception so the message can name it.
191+ env_value : str, optional
192+ What that variable held, so the caller can see what to shorten.
193+
194+ Raises
195+ ------
196+ :exc:`~libtmux.exc.SocketPathTooLong`
197+ When *socket_path* exceeds :data:`SOCKET_PATH_MAX_BYTES` bytes.
198+
199+ Examples
200+ --------
201+ >>> from libtmux._internal.env import (
202+ ... check_socket_path_length,
203+ ... SOCKET_PATH_MAX_BYTES,
204+ ... )
205+ >>> check_socket_path_length("/tmp/tmux-1000/default")
206+
207+ >>> try:
208+ ... check_socket_path_length("/tmp/" + "d" * 200 + "/sock")
209+ ... except exc.SocketPathTooLong as e:
210+ ... (e.length, e.limit == SOCKET_PATH_MAX_BYTES)
211+ (210, True)
212+
213+ A name that resolves somewhere too deep reports the name too. The path is
214+ measured as given -- whether tmux would really bind there is settled by
215+ :func:`resolve_socket_path` before this is called:
216+
217+ >>> deep = pathlib.Path("/tmp/" + "d" * 200) / "tmux-1000" / "dev"
218+ >>> try:
219+ ... check_socket_path_length(deep, socket_name="dev")
220+ ... except exc.SocketPathTooLong as e:
221+ ... e.socket_name
222+ 'dev'
223+ """
224+ if len (os .fsencode (socket_path )) > SOCKET_PATH_MAX_BYTES :
225+ raise exc .SocketPathTooLong (
226+ socket_path ,
227+ SOCKET_PATH_MAX_BYTES ,
228+ socket_name = socket_name ,
229+ env_var = env_var ,
230+ env_value = env_value ,
231+ )
232+
233+
134234def socket_path_from_env (env : t .Mapping [str , str ] | None = None ) -> str :
135235 """Return the tmux socket path recorded in ``$TMUX``.
136236
@@ -188,6 +288,56 @@ def socket_path_from_env(env: t.Mapping[str, str] | None = None) -> str:
188288 return parts [0 ]
189289
190290
291+ def resolve_ambient_socket_path (env : t .Mapping [str , str ] | None = None ) -> pathlib .Path :
292+ """Resolve the socket a *bare* tmux invocation talks to, in tmux's own order.
293+
294+ A tmux client given no ``-L`` or ``-S`` prefers ``$TMUX`` -- the socket of
295+ the pane it is running inside -- and only falls back to computing a path
296+ under ``$TMUX_TMPDIR`` when there is no pane. Measured against tmux 3.7b: a
297+ bare client with ``$TMUX`` set connects even when ``$TMUX_TMPDIR`` names a
298+ directory far too deep to bind, because it never looks there.
299+
300+ That order only holds for the bare client. Passing ``-L`` sends tmux to
301+ ``$TMUX_TMPDIR`` regardless of ``$TMUX``, so a named socket resolves through
302+ :func:`resolve_socket_path` instead.
303+
304+ Parameters
305+ ----------
306+ env : :class:`typing.Mapping`, optional
307+ Environment to read. Defaults to :data:`os.environ`.
308+
309+ Returns
310+ -------
311+ :class:`pathlib.Path`
312+ Socket path a bare tmux client would use.
313+
314+ Examples
315+ --------
316+ >>> from libtmux._internal.env import resolve_ambient_socket_path
317+
318+ Inside a pane, ``$TMUX`` names the socket outright:
319+
320+ >>> resolve_ambient_socket_path({"TMUX": "/tmp/tmux-1000/default,8421,0"})
321+ PosixPath('/tmp/tmux-1000/default')
322+
323+ ``$TMUX_TMPDIR`` is not consulted when there is a pane to inherit from:
324+
325+ >>> resolve_ambient_socket_path(
326+ ... {"TMUX": "/tmp/sock,8421,0", "TMUX_TMPDIR": "/nowhere"}
327+ ... )
328+ PosixPath('/tmp/sock')
329+
330+ Outside tmux it falls back to the computed path:
331+
332+ >>> resolve_ambient_socket_path({})
333+ PosixPath('/tmp/tmux-.../default')
334+ """
335+ try :
336+ return pathlib .Path (socket_path_from_env (env ))
337+ except exc .NotInsideTmux :
338+ return resolve_socket_path (env = env )
339+
340+
191341def pane_id_from_env (env : t .Mapping [str , str ] | None = None ) -> str :
192342 """Return the pane id recorded in ``$TMUX_PANE``.
193343
0 commit comments