Skip to content

Bug: -CC client permanently owns window size, blocking other clients on a shared tmux session (macOS) #278

Description

@o-borovets

Summary

When a rootshell -CC session and any other terminal client are attached to the same tmux session, the other client permanently loses the ability to resize any window. This is not a transient race — it reproduces 100% of the time and persists for as long as rootshell stays attached.

A second, related symptom from the same root cause: hiding the on-screen keyboard resizes only whichever window tmux considers current, so every other rootshell tab on that session keeps a stale size until it happens to become current.

Environment

  • rootshell 1.0.10 (130), macOS 26.6 (25G72)
  • Transport: tssh (tsshd 0.1.9, Homebrew)
  • tmux 3.7b on the remote host, window-size latest (tmux's default since 3.1)
  • Second client: another terminal client, in normal (non-control) mode, attached to the same tmux session

Steps to reproduce

  1. Attach to a tmux session from another terminal client (any non--CC client).
  2. Attach rootshell to the same session: tmux -CC attach -t work.
  3. Resize that other client's window by dragging its edge — a real SIGWINCH / ioctl(TIOCSWINSZ), not just app foreground/background.
  4. Observed: the tmux window does not resize. It stays at whatever size rootshell last reported.
  5. Detach the rootshell client (tmux detach-client -t <tty>) and take no action at all on the other client's side.
  6. Observed: the window instantly snaps to the other client's size.

Step 6 is the key one: nothing about the other client changed between 4 and 6, so rootshell's continued presence — not the resize itself — is what was blocking it.

Measurements from a live session

Other client resized while rootshell was attached to the same session:

other client:  client_width 239x70 → 168x72   (client_activity = newest in the entire server)
tmux window:   80x66 → 80x66                  (unchanged)

Then rootshell detached, nothing else touched:

window:        80x36 → 168x71                 (snapped to the other client's size instantly)

Root cause (traced in tmux 3.7b source)

window-size latest does not continuously pick "whichever client was most recently active," despite what tmux(1) implies. It is a sticky per-window pointer, w->latest, reassigned only at specific events:

  • server-client.c:415, in server_client_set_session(): every attach unconditionally runs s->curw->window->latest = c; — regardless of client type. Every rootshell attach/reconnect reclaims the pointer for the session's current window.

  • cmd-select-window.c:146: select-window reassigns it as well.

  • server-client.c:2242, case MSG_RESIZE: — a genuine terminal resize from a non-control client calls server_client_update_latest() (line 2248) and would reclaim w->latest, but it is guarded two lines earlier:

    case MSG_RESIZE:
            if (datalen != 0)
                    goto bad;
    
            if (c->flags & CLIENT_CONTROL)
                    break;
            server_client_update_latest(c);

    So a control client's own resizes never take this path.

  • resize.c:91, in ignore_client_size(): a control-mode client is excluded from the size calculation entirely until it sets CLIENT_SIZECHANGED / CLIENT_WINDOWSIZECHANGED:

    if ((c->flags & CLIENT_CONTROL) &&
        (~c->flags & CLIENT_SIZECHANGED) &&
        (~c->flags & CLIENT_WINDOWSIZECHANGED))
            return (1);

    Those flags are set by cmd-refresh-client.c (lines 101 and 128) when the client runs refresh-client -C, which rootshell does on attach.

  • resize.c:167, in clients_calculate_size(): once 2+ clients are eligible for a window, only w->latest's size is used:

    if (type == WINDOW_SIZE_LATEST && n > 1 && loop != w->latest) {
            log_debug("%s: %s is not latest", __func__, loop->name);
            continue;
    }

    No comparison of anyone else's activity or recency happens at all.

  • server-client.c:368, server_client_attached_lost(): on detach, for each window where w->latest == c, it picks the remaining viewer with the newest activity_time. This is the only place true activity recency decides anything — which is exactly why detaching rootshell made the window snap to the other client's size with no other action.

Net effect: the other client's resize can win only in the gap between its own SIGWINCH and rootshell's next attach/reconnect/tab-switch, which in practice is never.

Suggested fix

tmux already exposes a per-window sizing path built for precisely this multi-client case, and it bypasses the w->latest contest entirely — cmd-refresh-client.c:90:

if (sscanf(size, "@%u:%ux%u", &w, &x, &y) == 3) {
        ...
        cw = server_client_add_client_window(tc, w);
        cw->sx = x;
        cw->sy = y;
        tc->flags |= CLIENT_WINDOWSIZECHANGED;
        recalculate_sizes_now(1);

i.e. refresh-client -C @<window-id>:<W>x<H> (per window) instead of the whole-client refresh-client -C <W>x<H>. Combined with window-size manual + resize-window -t @<id> -x W -y H, each window's size is set explicitly by ID — and with manual, clients_calculate_size() skips the entire arbitration block, so there is no ownership to win or lose.

This is the mechanism iTerm2's tmux integration uses, which is why iTerm2 does not exhibit this when sharing a session with another client.

Switching rootshell's -CC resize handling to the per-window form would fix both symptoms at once: the shared-session ownership problem, and stale sizes in non-current tabs (each tab would carry its own size instead of competing for the session's single current-window slot).

Related issues

Searched before filing; these are adjacent but distinct:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions