Skip to content

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

Open

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.


Delivery contract from the rmux helper audit

Keep the destructive context-manager defect as its own fix. Review #728 against the chosen compatibility policy before treating it as resolved.

New owned-resource helpers must carry positive creation ownership and preserve borrowed objects. They must not infer ownership from the fact that a Server handle was constructed inside a context manager.

Acceptance for this bug remains narrow: an existing server and its sessions survive a scope the caller did not authorize to destroy them. Do not expand this ticket into the full workspace or lease protocol.

Delivery links: #804, #805. Track this work under #629 in the libtmux project.

Independent PR delivery

Follow the minimal slice and shared compatibility contract in #629. Build and test against master with every other unmerged feature branch absent. Cross-links above identify semantic contracts and future consumers; use a private adapter where needed instead of introducing a merge dependency. Preserve existing facade behavior and the inspected engine-ops interfaces. Leave deferred acceptance criteria open after an MVP lands.

Activity

  1. changed the title [-]Server.__exit__ kills any live server, including one it did not start[/-] [+]`Server.__exit__` kills any live server, including one it did not start[/+] on Jul 29, 2026
  2. added 4 commits that reference this issue on Jul 30, 2026
    7ea0444
    600c845
    dab8173
    3119e54
  3. moved this to In Progress in libtmuxon Oct 31, 2026
  4. tony commented on Nov 1, 2026

    @tony
    MemberAuthor

    The minimal borrowed-server correction is already covered by PR #728; this pass will reuse it rather than open a duplicate.

    I replayed that PR's four-file change onto current master and verified:

    • The five server-context tests pass. Running them before applying the implementation produced three survival failures, so the regression catches the original destruction behavior.
    • Ruff and mypy pass.
    • The full local suite has 18 failures in hooks, popups, and floating panes. Every one reproduces on unchanged master; the ownership tests are green. This is not a green full-suite claim.
    • The repository’s actual just build-docs gate passes. An extra strict Sphinx diagnostic reports unresolved references; unchanged master also reports reference warnings and exceeded that diagnostic’s bound. The strict diagnostic is not the project documentation gate.

    Evidence is scoped to Python 3.15 on Linux/WSL with the local tmux 3.8a build. macOS was not tested and no macOS work is added. PR #728 remains unmerged; keep this implementation issue open.

    Source and regression: ownership change, context tests. Baseline: current master source.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    • Status
      In Progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions