Skip to content

Commit 8504144

Browse files
committed
tmux(fix[errors]): Keep resource errors native
why: The branch wrapped every unrelated launch OSError in LibTmuxException. That changed trunk behavior for E2BIG and similar failures even though only unavailable executables need normalization. what: - Re-raise unrelated launch OSError values unchanged from direct APIs - Catch and log those errors only at lenient list boundaries - Document direct and lenient exception behavior - Cover all three list accessors and supported tmux builds
1 parent c8c8d6e commit 8504144

5 files changed

Lines changed: 65 additions & 24 deletions

File tree

docs/topics/logging.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,15 @@ failures remain exception data, so callers decide whether and where to log
105105
them. Expected probes such as {meth}`Server.is_alive()
106106
<libtmux.Server.is_alive>` also stay quiet.
107107

108-
Executable launch failures use {exc}`~libtmux.exc.TmuxCommandNotFound`.
109-
When the operating system attempted the launch, the exception retains its
110-
message and cause. A failed `PATH` lookup has a factual message without a
111-
synthetic cause.
112-
113-
Three list-shaped accessors intentionally hide
114-
{exc}`~libtmux.exc.LibTmuxException`:
108+
Missing, non-executable, and malformed tmux executables use
109+
{exc}`~libtmux.exc.TmuxCommandNotFound`. When the operating system attempted
110+
the launch, the exception retains its message and cause. A failed `PATH`
111+
lookup has a factual message without a synthetic cause. Other operating-system
112+
launch errors remain native exceptions at direct APIs.
113+
114+
Three list-shaped accessors intentionally hide tmux execution failures,
115+
including {exc}`~libtmux.exc.LibTmuxException` and operating-system launch
116+
errors:
115117

116118
- {attr}`Server.sessions <libtmux.Server.sessions>`;
117119
- {attr}`Server.attached_sessions <libtmux.Server.attached_sessions>`;

src/libtmux/AGENTS.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,14 @@ an `ERROR` because probes use it to answer false.
7171

7272
Normalize executable-launch `ENOENT`, `EACCES`, and `ENOEXEC` errors to
7373
`TmuxCommandNotFound`, preserving the operating-system message and cause.
74-
Keep unrelated `OSError` behavior caller-specific. A failed `PATH` preflight
75-
uses a factual message without inventing a cause.
74+
Keep unrelated `OSError` values native at direct loud APIs. A failed `PATH`
75+
preflight uses a factual message without inventing a cause.
7676

7777
`Server.sessions` and `Server.clients` log once in
78-
`libtmux.server` when they convert `LibTmuxException` to an empty
79-
result. `Server.attached_sessions` inherits that behavior through
80-
`Server.sessions`. The boundary record includes the subcommand,
81-
socket, first 100 stderr lines, and total stderr line count.
78+
`libtmux.server` when they convert `LibTmuxException` or an OS launch failure
79+
to an empty result. `Server.attached_sessions` inherits that behavior through
80+
`Server.sessions`. The boundary record includes the subcommand, socket, first
81+
100 stderr lines, and total stderr line count.
8282

8383
Deprecations and ignored arguments use `warnings.warn`, not a second
8484
log record. Applications can route them through

src/libtmux/common.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,13 @@ class tmux_cmd:
296296
297297
$ tmux new-session -s my session
298298
299+
Raises
300+
------
301+
:exc:`~libtmux.exc.TmuxCommandNotFound`
302+
When the tmux binary cannot be found or executed.
303+
:class:`OSError`
304+
When the operating system rejects the launch for another reason.
305+
299306
Notes
300307
-----
301308
.. versionchanged:: 0.8
@@ -329,7 +336,7 @@ def __init__(self, *args: t.Any, tmux_bin: str | None = None) -> None:
329336
)
330337
except OSError as error:
331338
_raise_if_unusable_tmux(error)
332-
raise exc.LibTmuxException(str(error)) from error
339+
raise
333340

334341
stdout, stderr = self.process.communicate()
335342
returncode = self.process.returncode

src/libtmux/server.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def _fetch_or_empty(
9292
def _log_swallowed_list_error(
9393
server: Server,
9494
list_cmd: str,
95-
error: exc.LibTmuxException,
95+
error: Exception,
9696
) -> None:
9797
"""Record a list failure at the boundary that converts it to empty."""
9898
stderr = str(error).splitlines()
@@ -339,6 +339,8 @@ def raise_if_dead(self) -> None:
339339
:class:`subprocess.CalledProcessError`
340340
When the tmux server is not running (non-zero exit from
341341
``list-sessions``).
342+
:class:`OSError`
343+
When the operating system rejects the launch for another reason.
342344
343345
>>> tmux = Server(socket_name="no_exist")
344346
>>> try:
@@ -2473,7 +2475,7 @@ def sessions(self) -> QueryList[Session]:
24732475
Session(server=self, **obj)
24742476
for obj in fetch_objs(server=self, list_cmd="list-sessions")
24752477
]
2476-
except exc.LibTmuxException as error:
2478+
except (exc.LibTmuxException, OSError) as error:
24772479
_log_swallowed_list_error(self, "list-sessions", error)
24782480
return QueryList([])
24792481
return QueryList(sessions)
@@ -2547,7 +2549,7 @@ def clients(self) -> QueryList[Client]:
25472549
Client(server=self, **obj)
25482550
for obj in fetch_objs(server=self, list_cmd="list-clients")
25492551
]
2550-
except exc.LibTmuxException as error:
2552+
except (exc.LibTmuxException, OSError) as error:
25512553
_log_swallowed_list_error(self, "list-clients", error)
25522554
return QueryList([])
25532555
return QueryList(clients)

tests/test_logging.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -482,19 +482,49 @@ def test_lenient_accessors_log_unusable_tmux_diagnostics(
482482
assert str(tmux_path) in diagnostic
483483

484484

485-
def test_unrelated_tmux_launch_failure_preserves_diagnostics() -> None:
486-
"""A launch resource failure is not mislabeled as a missing executable."""
485+
def test_tmux_cmd_unrelated_launch_failure_stays_os_error() -> None:
486+
"""A direct launch resource failure retains trunk's native exception."""
487487
from libtmux import exc
488488
from libtmux.common import tmux_cmd
489489

490-
with pytest.raises(exc.LibTmuxException) as exc_info:
490+
with pytest.raises(OSError) as exc_info:
491491
tmux_cmd("set-buffer", "x" * os.sysconf("SC_ARG_MAX"))
492492

493493
assert not isinstance(exc_info.value, exc.TmuxCommandNotFound)
494-
cause = exc_info.value.__cause__
495-
assert isinstance(cause, OSError)
496-
assert cause.errno == errno.E2BIG
497-
assert str(exc_info.value) == str(cause)
494+
assert exc_info.value.errno == errno.E2BIG
495+
496+
497+
@pytest.mark.parametrize(
498+
("accessor", "list_cmd"),
499+
[
500+
("sessions", "list-sessions"),
501+
("attached_sessions", "list-sessions"),
502+
("clients", "list-clients"),
503+
],
504+
)
505+
def test_lenient_accessors_log_unrelated_launch_failure(
506+
accessor: str,
507+
list_cmd: str,
508+
monkeypatch: pytest.MonkeyPatch,
509+
caplog: pytest.LogCaptureFixture,
510+
) -> None:
511+
"""Lenient list boundaries still swallow a raw launch resource error."""
512+
from libtmux.server import Server
513+
514+
def raise_e2big(*args: object, **kwargs: object) -> None:
515+
raise OSError(errno.E2BIG, os.strerror(errno.E2BIG), "/configured/tmux")
516+
517+
monkeypatch.setattr("libtmux.common.subprocess.Popen", raise_e2big)
518+
server = Server(tmux_bin="/configured/tmux")
519+
with caplog.at_level(logging.ERROR, logger="libtmux.server"):
520+
assert list(getattr(server, accessor)) == []
521+
522+
records = [record for record in caplog.records if record.levelno == logging.ERROR]
523+
assert len(records) == 1
524+
record = t.cast(t.Any, records[0])
525+
assert record.tmux_subcommand == list_cmd
526+
assert record.tmux_stderr_len == 1
527+
assert os.strerror(errno.E2BIG) in record.tmux_stderr[0]
498528

499529

500530
def test_raise_if_dead_unrelated_launch_failure_stays_os_error() -> None:

0 commit comments

Comments
 (0)