|
34 | 34 |
|
35 | 35 | import os |
36 | 36 | import pathlib |
| 37 | +import sys |
37 | 38 | import typing as t |
38 | 39 |
|
39 | 40 | from libtmux import exc |
40 | 41 |
|
| 42 | +if t.TYPE_CHECKING: |
| 43 | + from libtmux._internal.types import StrPath |
| 44 | + |
41 | 45 | TMUX: t.Final = "TMUX" |
42 | 46 | """Environment variable tmux exports with ``socket_path,server_pid,session_id``.""" |
43 | 47 |
|
|
53 | 57 | DEFAULT_SOCKET_NAME: t.Final = "default" |
54 | 58 | """Socket name tmux uses when neither ``-L`` nor ``-S`` was given.""" |
55 | 59 |
|
| 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 | + |
56 | 74 |
|
57 | 75 | def resolve_env(env: t.Mapping[str, str] | None = None) -> t.Mapping[str, str]: |
58 | 76 | """Return *env*, defaulting to the live process environment. |
@@ -131,6 +149,73 @@ def resolve_socket_path( |
131 | 149 | ) |
132 | 150 |
|
133 | 151 |
|
| 152 | +def check_socket_path_length( |
| 153 | + socket_path: StrPath, |
| 154 | + *, |
| 155 | + socket_name: str | None = None, |
| 156 | + env_var: str | None = None, |
| 157 | + env_value: str | None = None, |
| 158 | +) -> None: |
| 159 | + """Raise if *socket_path* is too long to be a UNIX socket address. |
| 160 | +
|
| 161 | + A tmux socket is a UNIX domain socket, so its path has to fit in |
| 162 | + :data:`SOCKET_PATH_MAX_BYTES` -- a filesystem that accepts the path says |
| 163 | + nothing about whether a socket can be bound at it. Length is counted in |
| 164 | + *bytes*, as the kernel counts it, so a non-ASCII path runs out sooner than |
| 165 | + its character count suggests. |
| 166 | +
|
| 167 | + Parameters |
| 168 | + ---------- |
| 169 | + socket_path : str or :class:`os.PathLike` |
| 170 | + Path to measure. |
| 171 | + socket_name : str, optional |
| 172 | + Socket name *socket_path* was resolved from, when it was resolved |
| 173 | + rather than passed in. Recorded on the exception so the message can say |
| 174 | + the length was inherited from ``$TMUX_TMPDIR``. |
| 175 | + env_var : str, optional |
| 176 | + Environment variable the socket directory came from, when one did. |
| 177 | + Recorded on the exception so the message can name it. |
| 178 | + env_value : str, optional |
| 179 | + What that variable held, so the caller can see what to shorten. |
| 180 | +
|
| 181 | + Raises |
| 182 | + ------ |
| 183 | + :exc:`~libtmux.exc.SocketPathTooLong` |
| 184 | + When *socket_path* exceeds :data:`SOCKET_PATH_MAX_BYTES` bytes. |
| 185 | +
|
| 186 | + Examples |
| 187 | + -------- |
| 188 | + >>> from libtmux._internal.env import ( |
| 189 | + ... check_socket_path_length, |
| 190 | + ... SOCKET_PATH_MAX_BYTES, |
| 191 | + ... ) |
| 192 | + >>> check_socket_path_length("/tmp/tmux-1000/default") |
| 193 | +
|
| 194 | + >>> try: |
| 195 | + ... check_socket_path_length("/tmp/" + "d" * 200 + "/sock") |
| 196 | + ... except exc.SocketPathTooLong as e: |
| 197 | + ... (e.length, e.limit == SOCKET_PATH_MAX_BYTES) |
| 198 | + (210, True) |
| 199 | +
|
| 200 | + A name that resolves somewhere too deep reports the name too: |
| 201 | +
|
| 202 | + >>> deep = resolve_socket_path("dev", env={"TMUX_TMPDIR": "/tmp/" + "d" * 200}) |
| 203 | + >>> try: |
| 204 | + ... check_socket_path_length(deep, socket_name="dev") |
| 205 | + ... except exc.SocketPathTooLong as e: |
| 206 | + ... e.socket_name |
| 207 | + 'dev' |
| 208 | + """ |
| 209 | + if len(os.fsencode(socket_path)) > SOCKET_PATH_MAX_BYTES: |
| 210 | + raise exc.SocketPathTooLong( |
| 211 | + socket_path, |
| 212 | + SOCKET_PATH_MAX_BYTES, |
| 213 | + socket_name=socket_name, |
| 214 | + env_var=env_var, |
| 215 | + env_value=env_value, |
| 216 | + ) |
| 217 | + |
| 218 | + |
134 | 219 | def socket_path_from_env(env: t.Mapping[str, str] | None = None) -> str: |
135 | 220 | """Return the tmux socket path recorded in ``$TMUX``. |
136 | 221 |
|
@@ -188,6 +273,56 @@ def socket_path_from_env(env: t.Mapping[str, str] | None = None) -> str: |
188 | 273 | return parts[0] |
189 | 274 |
|
190 | 275 |
|
| 276 | +def resolve_ambient_socket_path(env: t.Mapping[str, str] | None = None) -> pathlib.Path: |
| 277 | + """Resolve the socket a *bare* tmux invocation talks to, in tmux's own order. |
| 278 | +
|
| 279 | + A tmux client given no ``-L`` or ``-S`` prefers ``$TMUX`` -- the socket of |
| 280 | + the pane it is running inside -- and only falls back to computing a path |
| 281 | + under ``$TMUX_TMPDIR`` when there is no pane. Measured against tmux 3.7b: a |
| 282 | + bare client with ``$TMUX`` set connects even when ``$TMUX_TMPDIR`` names a |
| 283 | + directory far too deep to bind, because it never looks there. |
| 284 | +
|
| 285 | + That order only holds for the bare client. Passing ``-L`` sends tmux to |
| 286 | + ``$TMUX_TMPDIR`` regardless of ``$TMUX``, so a named socket resolves through |
| 287 | + :func:`resolve_socket_path` instead. |
| 288 | +
|
| 289 | + Parameters |
| 290 | + ---------- |
| 291 | + env : :class:`typing.Mapping`, optional |
| 292 | + Environment to read. Defaults to :data:`os.environ`. |
| 293 | +
|
| 294 | + Returns |
| 295 | + ------- |
| 296 | + :class:`pathlib.Path` |
| 297 | + Socket path a bare tmux client would use. |
| 298 | +
|
| 299 | + Examples |
| 300 | + -------- |
| 301 | + >>> from libtmux._internal.env import resolve_ambient_socket_path |
| 302 | +
|
| 303 | + Inside a pane, ``$TMUX`` names the socket outright: |
| 304 | +
|
| 305 | + >>> resolve_ambient_socket_path({"TMUX": "/tmp/tmux-1000/default,8421,0"}) |
| 306 | + PosixPath('/tmp/tmux-1000/default') |
| 307 | +
|
| 308 | + ``$TMUX_TMPDIR`` is not consulted when there is a pane to inherit from: |
| 309 | +
|
| 310 | + >>> resolve_ambient_socket_path( |
| 311 | + ... {"TMUX": "/tmp/sock,8421,0", "TMUX_TMPDIR": "/nowhere"} |
| 312 | + ... ) |
| 313 | + PosixPath('/tmp/sock') |
| 314 | +
|
| 315 | + Outside tmux it falls back to the computed path: |
| 316 | +
|
| 317 | + >>> resolve_ambient_socket_path({}) |
| 318 | + PosixPath('/tmp/tmux-.../default') |
| 319 | + """ |
| 320 | + try: |
| 321 | + return pathlib.Path(socket_path_from_env(env)) |
| 322 | + except exc.NotInsideTmux: |
| 323 | + return resolve_socket_path(env=env) |
| 324 | + |
| 325 | + |
191 | 326 | def pane_id_from_env(env: t.Mapping[str, str] | None = None) -> str: |
192 | 327 | """Return the pane id recorded in ``$TMUX_PANE``. |
193 | 328 |
|
|
0 commit comments