Server's context manager spares a server it did not start - #728
Open
tony wants to merge 1 commit into
Open
Conversation
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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
with Server() as server:destroying a tmux daemon it had nothing to do with.__exit__wasif 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.__enter__records whether the daemon was already running, and__exit__kills only one the block started.kill_on_exittoServer.__init__and as an attribute, so the policy can be stated explicitly rather than inferred.Server,__enter__,__exit__, and indocs/topics/context_managers.md, with running examples.Why the old default could not be right
A
Serveris 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 iswith 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
To get the old behaviour back, say so:
kill_on_exitNone(default)TrueFalseDesign decisions
Three states rather than two. A plain
kill_on_exit: bool = Truewould have been purely additive and left the dangerous default in place; a plainbool = Falsewould 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 onelist-sessionsper block and is observable rather than tracked throughnew_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_enterdefaults toTrue. So a bare__exit__()call that never went through__enter__is non-destructive rather than destructive.Session,WindowandPaneare 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 emptywithblock; fails onmastertest_server_context_manager_kill_on_exit_true— the opt-in restores the unconditional killtest_server_context_manager_kill_on_exit_false— never killsServer,__enter__and__exit__covering all three states, plus the reworked examples indocs/topics/context_managers.md— all collected and runlibtmux.pytest_plugin, which leans on server teardownuv run ruff check ./uv run ruff format ./uv run mypycleanjust build-docscleanNote for the merger
CHANGESwill 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.