Filed against tmux-python/libtmux v0.62.0. Found while writing tested documentation examples that isolate tmux under a temporary TMUX_TMPDIR.
What happens
with Server(...) as server: kills the server on exit whenever it is alive, unconditionally. There is no kill_on_exit=False, no "only kill what I started", and no way to use the context manager purely for scoping.
if self.is_alive():
self.kill()
A Server is a handle, not a connection, and the same handle addresses a server whether or not this process started it. So the context manager cannot tell "the server I just booted" from "the server the user has been working in all day", and it destroys both.
The sharpest form: with Server() as server: — no arguments, so the default socket — kills the reader's running tmux and every session in it. That is a plausible thing to write after reading the class docstring, and the failure is silent and total.
Recreation
import os, tempfile
os.environ["TMUX_TMPDIR"] = tempfile.mkdtemp(prefix="repro-")
from libtmux.server import Server
Server(socket_name="preexisting").new_session(session_name="important-work")
print("before:", [s.session_name for s in Server(socket_name="preexisting").sessions])
with Server(socket_name="preexisting") as server:
print("inside:", server.is_alive())
print("after :", Server(socket_name="preexisting").is_alive(), "<- the session is gone")
Observed on libtmux v0.62.0 / tmux 3.7b:
before: ['important-work']
inside: True
after : False <- the session is gone
The with block did nothing except enter and exit, and a session it never created is gone. Drop the TMUX_TMPDIR line and the same script destroys the real default server.
Note on the sibling context managers
Session, Window and Pane have the same shape — __exit__ kills the object if it still exists — but the blast radius there is bounded by what the object is, and killing a session you were handed is at least proportionate. A server is different in kind: it is shared, process-wide, and typically not yours.
Documented in docs/topics/context_managers.md, which describes the kill-on-exit behaviour but not the shared-server hazard.
What a fix needs
- Add
kill_on_exit: bool = True to Server.__init__, honoured by __exit__. Keeps the current default, gives callers an out. Cheapest change, no behaviour break.
- Better, if a break is acceptable in a pre-1.0 minor: only kill a server this handle started.
Server already knows whether it booted the daemon — new_session is the call that starts one — so record that and have __exit__ respect it. This makes the destructive path opt-in by construction rather than by remembering a keyword.
- Either way: document the hazard on
Server.__enter__/__exit__ and in docs/topics/context_managers.md, in the same terms as the recreation above, and add a regression test that a pre-existing server survives a with block.
Option 2 with a kill_on_exit=True escape hatch would cover both audiences. If neither is wanted, the minimum is a loud warning in the docstring — the current one says "killing the server if it exists" without saying whose.
Filed against tmux-python/libtmux v0.62.0. Found while writing tested documentation examples that isolate tmux under a temporary
TMUX_TMPDIR.What happens
with Server(...) as server:kills the server on exit whenever it is alive, unconditionally. There is nokill_on_exit=False, no "only kill what I started", and no way to use the context manager purely for scoping.src/libtmux/server.py#L267-L285—__exit__A
Serveris a handle, not a connection, and the same handle addresses a server whether or not this process started it. So the context manager cannot tell "the server I just booted" from "the server the user has been working in all day", and it destroys both.The sharpest form:
with Server() as server:— no arguments, so the default socket — kills the reader's running tmux and every session in it. That is a plausible thing to write after reading the class docstring, and the failure is silent and total.Recreation
Observed on libtmux v0.62.0 / tmux 3.7b:
The
withblock did nothing except enter and exit, and a session it never created is gone. Drop theTMUX_TMPDIRline and the same script destroys the real default server.Note on the sibling context managers
Session,WindowandPanehave the same shape —__exit__kills the object if it still exists — but the blast radius there is bounded by what the object is, and killing a session you were handed is at least proportionate. A server is different in kind: it is shared, process-wide, and typically not yours.src/libtmux/session.py,src/libtmux/window.py,src/libtmux/pane.pyDocumented in
docs/topics/context_managers.md, which describes the kill-on-exit behaviour but not the shared-server hazard.What a fix needs
kill_on_exit: bool = TruetoServer.__init__, honoured by__exit__. Keeps the current default, gives callers an out. Cheapest change, no behaviour break.Serveralready knows whether it booted the daemon —new_sessionis the call that starts one — so record that and have__exit__respect it. This makes the destructive path opt-in by construction rather than by remembering a keyword.Server.__enter__/__exit__and indocs/topics/context_managers.md, in the same terms as the recreation above, and add a regression test that a pre-existing server survives awithblock.Option 2 with a
kill_on_exit=Trueescape hatch would cover both audiences. If neither is wanted, the minimum is a loud warning in the docstring — the current one says "killing the server if it exists" without saying whose.