Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ $ uvx --from 'libtmux' --prerelease allow python
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### Fixes

- {class}`~libtmux.Server` now reprs the socket path tmux resolves from
`$TMUX_TMPDIR` instead of a hard-coded `/tmp/tmux-<euid>/default` (#723)

### Documentation

#### Cleaner `from_env` examples (#719)
Expand Down
66 changes: 65 additions & 1 deletion src/libtmux/_internal/env.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Readers for the tmux variables exported into every pane's environment.
"""Readers for the tmux variables libtmux takes its bearings from.

libtmux._internal.env
~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -24,11 +24,16 @@
libtmux therefore reads *only* the socket path out of ``TMUX`` and asks tmux
itself -- targeting ``TMUX_PANE`` -- for the pane's window and session. See
:meth:`libtmux.Pane.from_env`.

A third variable, ``TMUX_TMPDIR``, is read *by* tmux rather than exported by
it: it picks the directory tmux keeps its sockets in. See
:func:`resolve_socket_path`.
"""

from __future__ import annotations

import os
import pathlib
import typing as t

from libtmux import exc
Expand All @@ -39,6 +44,15 @@
TMUX_PANE: t.Final = "TMUX_PANE"
"""Environment variable tmux exports with the pane's id, e.g. ``%3``."""

TMUX_TMPDIR: t.Final = "TMUX_TMPDIR"
"""Environment variable naming the directory tmux keeps its sockets in."""

DEFAULT_SOCKET_DIR: t.Final = "/tmp"
"""Socket directory tmux falls back to when ``$TMUX_TMPDIR`` is unset."""

DEFAULT_SOCKET_NAME: t.Final = "default"
"""Socket name tmux uses when neither ``-L`` nor ``-S`` was given."""


def resolve_env(env: t.Mapping[str, str] | None = None) -> t.Mapping[str, str]:
"""Return *env*, defaulting to the live process environment.
Expand All @@ -65,6 +79,56 @@ def resolve_env(env: t.Mapping[str, str] | None = None) -> t.Mapping[str, str]:
return os.environ if env is None else env


def resolve_socket_path(
socket_name: str | None = None,
env: t.Mapping[str, str] | None = None,
) -> pathlib.Path:
"""Resolve the socket path tmux uses for *socket_name*.

tmux keeps its sockets in ``tmux-<euid>`` under ``$TMUX_TMPDIR``, falling
back to ``/tmp`` when that is unset or empty. ``$TMPDIR`` is deliberately
not consulted -- tmux does not consult it either.

The path is *computed*, not observed: it says where tmux would put the
socket, not that a daemon is listening there. Code holding a live
:class:`~libtmux.Server` should ask tmux instead, with the
``#{socket_path}`` format.

Parameters
----------
socket_name : str, optional
Socket name, as passed to tmux's ``-L``. Defaults to tmux's own
default, ``"default"``.
env : :class:`typing.Mapping`, optional
Environment to read. Defaults to :data:`os.environ`.

Returns
-------
:class:`pathlib.Path`
Path tmux resolves the socket to.

Examples
--------
>>> from libtmux._internal.env import resolve_socket_path
>>> resolve_socket_path(env={})
PosixPath('/tmp/tmux-.../default')

>>> resolve_socket_path("mysocket", env={"TMUX_TMPDIR": "/run/user/1000"})
PosixPath('/run/user/1000/tmux-.../mysocket')

``$TMPDIR`` is not a socket directory, so it changes nothing:

>>> resolve_socket_path(env={"TMPDIR": "/var/folders/xy"})
PosixPath('/tmp/tmux-.../default')
"""
tmpdir = resolve_env(env).get(TMUX_TMPDIR) or DEFAULT_SOCKET_DIR
return (
pathlib.Path(tmpdir)
/ f"tmux-{os.geteuid()}"
/ (socket_name or DEFAULT_SOCKET_NAME)
)


def socket_path_from_env(env: t.Mapping[str, str] | None = None) -> str:
"""Return the tmux socket path recorded in ``$TMUX``.

Expand Down
11 changes: 5 additions & 6 deletions src/libtmux/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from libtmux import exc
from libtmux._internal.control_mode import ControlMode
from libtmux._internal.env import resolve_socket_path
from libtmux.server import Server
from libtmux.test.constants import TEST_SESSION_PREFIX
from libtmux.test.random import get_test_session_name, namer
Expand Down Expand Up @@ -49,12 +50,10 @@ def _reap_test_server(socket_name: str | None) -> None:
if srv.is_alive():
srv.kill()

# ``Server(socket_name=...)`` does not populate ``socket_path`` —
# the Server class only derives the path when neither ``socket_name``
# nor ``socket_path`` was supplied. Recompute the location tmux uses
# so we can unlink the file regardless of daemon state.
tmux_tmpdir = pathlib.Path(os.environ.get("TMUX_TMPDIR", "/tmp"))
socket_path = tmux_tmpdir / f"tmux-{os.geteuid()}" / socket_name
# ``Server(socket_name=...)`` does not populate ``socket_path``, so
# resolve where tmux put the socket to unlink it regardless of daemon
# state.
socket_path = resolve_socket_path(socket_name)
with contextlib.suppress(OSError):
socket_path.unlink(missing_ok=True)

Expand Down
30 changes: 21 additions & 9 deletions src/libtmux/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import warnings

from libtmux import exc
from libtmux._internal.env import socket_path_from_env
from libtmux._internal.env import resolve_socket_path, socket_path_from_env
from libtmux._internal.query_list import QueryList
from libtmux.client import Client
from libtmux.common import get_version, has_gte_version, raise_if_stderr, tmux_cmd
Expand Down Expand Up @@ -2682,17 +2682,29 @@ def __eq__(self, other: object) -> bool:
return False

def __repr__(self) -> str:
"""Representation of :class:`Server` object."""
"""Representation of :class:`Server` object.

A server given neither ``socket_name`` nor ``socket_path`` talks to the
socket tmux resolves from ``$TMUX_TMPDIR``, so that is the path shown.

Examples
--------
>>> from libtmux.server import Server
>>> Server(socket_name="libtmux_repr_demo")
Server(socket_name=libtmux_repr_demo)

>>> Server(socket_path="/run/user/1000/tmux-1000/demo")
Server(socket_path=/run/user/1000/tmux-1000/demo)

>>> monkeypatch.setenv("TMUX_TMPDIR", "/run/user/1000")
>>> Server()
Server(socket_path=/run/user/1000/tmux-.../default)
"""
if self.socket_name is not None:
return (
f"{self.__class__.__name__}"
f"(socket_name={getattr(self, 'socket_name', 'default')})"
)
return f"{self.__class__.__name__}(socket_name={self.socket_name})"
if self.socket_path is not None:
return f"{self.__class__.__name__}(socket_path={self.socket_path})"
return (
f"{self.__class__.__name__}(socket_path=/tmp/tmux-{os.geteuid()}/default)"
)
return f"{self.__class__.__name__}(socket_path={resolve_socket_path()})"

#
# Legacy: Redundant stuff we want to remove
Expand Down
26 changes: 26 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,32 @@ def test_socket_path_not_derived_from_socket_name() -> None:
assert myserver.socket_path is None


def test_repr_socket_path_honors_tmux_tmpdir(
monkeypatch: pytest.MonkeyPatch,
tmp_path: pathlib.Path,
) -> None:
"""A default ``Server()`` reprs the socket tmux resolves, not ``/tmp``.

Regression for #723: the repr hard-coded ``/tmp/tmux-<euid>/default``, so
under any ``$TMUX_TMPDIR`` it named a socket the object was not using.
"""
monkeypatch.setenv("TMUX_TMPDIR", str(tmp_path))

myserver = Server()

socket_path = tmp_path / f"tmux-{os.geteuid()}" / "default"
assert repr(myserver) == f"Server(socket_path={socket_path})"


def test_repr_socket_name(monkeypatch: pytest.MonkeyPatch) -> None:
"""A named socket reprs its name, whatever ``$TMUX_TMPDIR`` says."""
monkeypatch.setenv("TMUX_TMPDIR", "/nonexistent-tmux-tmpdir")

myserver = Server(socket_name="libtmux_test_repr")

assert repr(myserver) == "Server(socket_name=libtmux_test_repr)"


def test_config(server: Server) -> None:
"""``-f`` file for tmux(1) configuration."""
myserver = Server(config_file="test")
Expand Down
Loading