Skip to content

Server's context manager spares a server it did not start - #728

Open
tony wants to merge 1 commit into
masterfrom
fix/724-server-kill-on-exit
Open

Server's context manager spares a server it did not start#728
tony wants to merge 1 commit into
masterfrom
fix/724-server-kill-on-exit

Conversation

@tony

@tony tony commented Jul 29, 2026

Copy link
Copy Markdown
Member

Breaking change. Read the Before / After below before merging.

Summary

  • Fix with Server() as server: destroying a tmux daemon it had nothing to do with. __exit__ was if self.is_alive(): self.kill(), unconditionally — so a block that did no work at all took out the reader's default server and every session in it.
  • Change the default: __enter__ records whether the daemon was already running, and __exit__ kills only one the block started.
  • Add kill_on_exit to Server.__init__ and as an attribute, so the policy can be stated explicitly rather than inferred.
  • Document the hazard on Server, __enter__, __exit__, and in docs/topics/context_managers.md, with running examples.

Why the old default could not be right

A Server is a handle on a socket, not a connection. The same handle addresses a daemon whether or not this process booted it, so the context manager had no way to tell "the server I just started" from "the server the user has been working in all day" — and it destroyed both. The sharpest form is with Server() as server: with no arguments, which targets the default socket. That is a plausible thing to write after reading the class docstring, and the failure is silent and total.

Before / After

Server(socket_name="my-work").new_session(session_name="important")

with Server(socket_name="my-work") as server:
    pass

Server(socket_name="my-work").is_alive()   # False — the session is gone
Server(socket_name="my-work").new_session(session_name="important")

with Server(socket_name="my-work") as server:
    pass

Server(socket_name="my-work").is_alive()   # True — it was not this block's server

To get the old behaviour back, say so:

with Server(socket_name="my-work", kill_on_exit=True) as server:
    pass

kill_on_exit

Value Leaving the block
None (default) kills the daemon only if the block started it
True always kills a live daemon — the pre-0.63 behaviour
False never kills

Design decisions

Three states rather than two. A plain kill_on_exit: bool = True would have been purely additive and left the dangerous default in place; a plain bool = False would have silently stopped cleaning up servers that tests and scripts expect to be cleaned up. The started-by-the-block rule is what callers actually mean, and the two explicit values remain available for callers who mean something else. Worth a reviewer's opinion — an enum would also work.

"Alive at __enter__" as the proxy for "started by this block." It costs one list-sessions per block and is observable rather than tracked through new_session. The gap: if a daemon dies and something else reboots it mid-block, exit will kill a server this block did not start. Judged acceptable against the cost of threading ownership through every call that can boot a daemon.

_alive_on_enter defaults to True. So a bare __exit__() call that never went through __enter__ is non-destructive rather than destructive.

Session, Window and Pane are untouched. They have the same __exit__ shape, but the blast radius there is bounded by what the object is, and killing a session you were handed is proportionate. A server is shared and process-wide.

Test plan

  • test_server_context_manager_spares_pre_existing_server — a pre-existing server survives an empty with block; fails on master
  • test_server_context_manager_kill_on_exit_true — the opt-in restores the unconditional kill
  • test_server_context_manager_kill_on_exit_false — never kills
  • Doctests on Server, __enter__ and __exit__ covering all three states, plus the reworked examples in docs/topics/context_managers.md — all collected and run
  • Full suite clean, including libtmux.pytest_plugin, which leans on server teardown
  • uv run ruff check . / uv run ruff format . / uv run mypy clean
  • just build-docs clean

Note for the merger

CHANGES will conflict with the sibling PRs for #723, #725 and #726, which all add to the same block — keep both sides. This one lands under ### Breaking changes; the others are ### Fixes.

why: `with Server() as server:` killed any live daemon on exit, so a
block that did nothing at all destroyed the reader's default tmux
server and every session in it. A Server is a handle on a socket, not
a connection, so the handle could not tell a daemon it booted from one
that had been running all day. Fixes #724.

what:
- `Server.__enter__` records whether the daemon was already alive; by
  default `__exit__` kills only a daemon the block started
- Add `kill_on_exit` to `Server.__init__`: `True` restores the
  unconditional kill, `False` never kills, `None` (default) is the
  started-by-the-block rule
- Document the hazard on `Server`, `__enter__`, `__exit__` and in
  docs/topics/context_managers.md, with running examples
- Cover pre-existing survival and both `kill_on_exit` values in
  tests/test_server.py
- Note the behaviour change under `### Breaking changes` in CHANGES
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.77778% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.43%. Comparing base (be7c8c1) to head (846fb7c).

Files with missing lines Patch % Lines
src/libtmux/server.py 77.77% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #728      +/-   ##
==========================================
- Coverage   52.45%   52.43%   -0.02%     
==========================================
  Files          26       26              
  Lines        3729     3738       +9     
  Branches      747      749       +2     
==========================================
+ Hits         1956     1960       +4     
- Misses       1469     1474       +5     
  Partials      304      304              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant