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,11 @@ 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`` that is not a directory falls back the same way. tmux
113+ creates ``<dir>/tmux-<euid>`` and quietly uses ``/tmp`` when it cannot, so
114+ a path under a directory that does not exist is never the one it binds --
115+ measuring it would refuse a server tmux reaches without difficulty.
116+
94117 The path is *computed*, not observed: it says where tmux would put the
95118 socket, not that a daemon is listening there. Code holding a live
96119 :class:`~libtmux.Server` should ask tmux instead, with the
@@ -115,22 +138,97 @@ def resolve_socket_path(
115138 >>> resolve_socket_path(env={})
116139 PosixPath('/tmp/tmux-.../default')
117140
118- >>> resolve_socket_path("mysocket", env={"TMUX_TMPDIR": "/run/user/1000 "})
119- PosixPath('/run/user/1000 /tmux-.../mysocket')
141+ >>> resolve_socket_path("mysocket", env={"TMUX_TMPDIR": "/usr "})
142+ PosixPath('/usr /tmux-.../mysocket')
120143
121144 ``$TMPDIR`` is not a socket directory, so it changes nothing:
122145
123146 >>> resolve_socket_path(env={"TMPDIR": "/var/folders/xy"})
124147 PosixPath('/tmp/tmux-.../default')
148+
149+ Nor does a ``$TMUX_TMPDIR`` tmux cannot use, however long it is:
150+
151+ >>> resolve_socket_path(env={"TMUX_TMPDIR": "/nonexistent-" + "d" * 200})
152+ PosixPath('/tmp/tmux-.../default')
125153 """
126154 tmpdir = resolve_env (env ).get (TMUX_TMPDIR ) or DEFAULT_SOCKET_DIR
155+ base = pathlib .Path (tmpdir )
156+ if not base .is_dir ():
157+ base = pathlib .Path (DEFAULT_SOCKET_DIR )
127158 return (
128- pathlib .Path (tmpdir ).resolve ()
129- / f"tmux-{ os .geteuid ()} "
130- / (socket_name or DEFAULT_SOCKET_NAME )
159+ base .resolve () / f"tmux-{ os .geteuid ()} " / (socket_name or DEFAULT_SOCKET_NAME )
131160 )
132161
133162
163+ def check_socket_path_length (
164+ socket_path : StrPath ,
165+ * ,
166+ socket_name : str | None = None ,
167+ env_var : str | None = None ,
168+ env_value : str | None = None ,
169+ ) -> None :
170+ """Raise if *socket_path* is too long to be a UNIX socket address.
171+
172+ A tmux socket is a UNIX domain socket, so its path has to fit in
173+ :data:`SOCKET_PATH_MAX_BYTES` -- a filesystem that accepts the path says
174+ nothing about whether a socket can be bound at it. Length is counted in
175+ *bytes*, as the kernel counts it, so a non-ASCII path runs out sooner than
176+ its character count suggests.
177+
178+ Parameters
179+ ----------
180+ socket_path : str or :class:`os.PathLike`
181+ Path to measure.
182+ socket_name : str, optional
183+ Socket name *socket_path* was resolved from, when it was resolved
184+ rather than passed in. Recorded on the exception so the message can say
185+ the length was inherited from ``$TMUX_TMPDIR``.
186+ env_var : str, optional
187+ Environment variable the socket directory came from, when one did.
188+ Recorded on the exception so the message can name it.
189+ env_value : str, optional
190+ What that variable held, so the caller can see what to shorten.
191+
192+ Raises
193+ ------
194+ :exc:`~libtmux.exc.SocketPathTooLong`
195+ When *socket_path* exceeds :data:`SOCKET_PATH_MAX_BYTES` bytes.
196+
197+ Examples
198+ --------
199+ >>> from libtmux._internal.env import (
200+ ... check_socket_path_length,
201+ ... SOCKET_PATH_MAX_BYTES,
202+ ... )
203+ >>> check_socket_path_length("/tmp/tmux-1000/default")
204+
205+ >>> try:
206+ ... check_socket_path_length("/tmp/" + "d" * 200 + "/sock")
207+ ... except exc.SocketPathTooLong as e:
208+ ... (e.length, e.limit == SOCKET_PATH_MAX_BYTES)
209+ (210, True)
210+
211+ A name that resolves somewhere too deep reports the name too. The path is
212+ measured as given -- whether tmux would really bind there is settled by
213+ :func:`resolve_socket_path` before this is called:
214+
215+ >>> deep = pathlib.Path("/tmp/" + "d" * 200) / "tmux-1000" / "dev"
216+ >>> try:
217+ ... check_socket_path_length(deep, socket_name="dev")
218+ ... except exc.SocketPathTooLong as e:
219+ ... e.socket_name
220+ 'dev'
221+ """
222+ if len (os .fsencode (socket_path )) > SOCKET_PATH_MAX_BYTES :
223+ raise exc .SocketPathTooLong (
224+ socket_path ,
225+ SOCKET_PATH_MAX_BYTES ,
226+ socket_name = socket_name ,
227+ env_var = env_var ,
228+ env_value = env_value ,
229+ )
230+
231+
134232def socket_path_from_env (env : t .Mapping [str , str ] | None = None ) -> str :
135233 """Return the tmux socket path recorded in ``$TMUX``.
136234
@@ -188,6 +286,56 @@ def socket_path_from_env(env: t.Mapping[str, str] | None = None) -> str:
188286 return parts [0 ]
189287
190288
289+ def resolve_ambient_socket_path (env : t .Mapping [str , str ] | None = None ) -> pathlib .Path :
290+ """Resolve the socket a *bare* tmux invocation talks to, in tmux's own order.
291+
292+ A tmux client given no ``-L`` or ``-S`` prefers ``$TMUX`` -- the socket of
293+ the pane it is running inside -- and only falls back to computing a path
294+ under ``$TMUX_TMPDIR`` when there is no pane. Measured against tmux 3.7b: a
295+ bare client with ``$TMUX`` set connects even when ``$TMUX_TMPDIR`` names a
296+ directory far too deep to bind, because it never looks there.
297+
298+ That order only holds for the bare client. Passing ``-L`` sends tmux to
299+ ``$TMUX_TMPDIR`` regardless of ``$TMUX``, so a named socket resolves through
300+ :func:`resolve_socket_path` instead.
301+
302+ Parameters
303+ ----------
304+ env : :class:`typing.Mapping`, optional
305+ Environment to read. Defaults to :data:`os.environ`.
306+
307+ Returns
308+ -------
309+ :class:`pathlib.Path`
310+ Socket path a bare tmux client would use.
311+
312+ Examples
313+ --------
314+ >>> from libtmux._internal.env import resolve_ambient_socket_path
315+
316+ Inside a pane, ``$TMUX`` names the socket outright:
317+
318+ >>> resolve_ambient_socket_path({"TMUX": "/tmp/tmux-1000/default,8421,0"})
319+ PosixPath('/tmp/tmux-1000/default')
320+
321+ ``$TMUX_TMPDIR`` is not consulted when there is a pane to inherit from:
322+
323+ >>> resolve_ambient_socket_path(
324+ ... {"TMUX": "/tmp/sock,8421,0", "TMUX_TMPDIR": "/nowhere"}
325+ ... )
326+ PosixPath('/tmp/sock')
327+
328+ Outside tmux it falls back to the computed path:
329+
330+ >>> resolve_ambient_socket_path({})
331+ PosixPath('/tmp/tmux-.../default')
332+ """
333+ try :
334+ return pathlib .Path (socket_path_from_env (env ))
335+ except exc .NotInsideTmux :
336+ return resolve_socket_path (env = env )
337+
338+
191339def pane_id_from_env (env : t .Mapping [str , str ] | None = None ) -> str :
192340 """Return the pane id recorded in ``$TMUX_PANE``.
193341
0 commit comments