Skip to content

Server.__exit__ kills any live server, including one it did not start #724

Description

@tony

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

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions