diff --git a/regress/control-client-exit.sh b/regress/control-client-exit.sh new file mode 100644 index 000000000..ac08db5cc --- /dev/null +++ b/regress/control-client-exit.sh @@ -0,0 +1,111 @@ +#!/bin/sh + +# Once a control client is marked for exit (by detach-client here, or by the +# "too far behind" eviction in control_check_age), the server must stop +# queueing new output and notifications for it. The exit handshake only waits +# for already-queued blocks to drain (control_all_done), so anything queued +# after that point is written to a client that will never read it, and can hold +# the handshake - and the pane buffer it pins - open. This checks that a +# notification generated while the client is exiting never reaches its output. +# +# To see this in seconds rather than waiting out CONTROL_MAXIMUM_AGE (300s) the +# control client's output goes down a fifo that a helper drains only slowly. +# The slow drain keeps a backlog stuck in the server so the exit handshake +# cannot complete; while it is stuck we detach the client (which sets the exit +# flag) and rename the window to a marker. Draining the fifo must then not turn +# up the marker as a %window-renamed line. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest" +$TMUX kill-server 2>/dev/null + +DIR=$(mktemp -d) +FIFO=$DIR/fifo +OUT=$DIR/out +MARKER=exitmarker +READER= + +mkfifo "$FIFO" || exit 1 +: >"$OUT" + +cleanup() { + [ -n "$READER" ] && kill "$READER" 2>/dev/null + $TMUX kill-server 2>/dev/null + exec 8<&- 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +# A detached session whose pane floods printable output forever. +$TMUX -f/dev/null new -d -x 80 -y 24 -s rt 'cat /dev/zero | tr "\000" x' || exit 1 +$TMUX setw -g automatic-rename off || exit 1 + +# Attach a control client: output to the fifo, stdin held open by sleep so it +# stays attached while we drive the session from outside. Open the read end +# after the client has opened the write end. +( sleep 30 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 & +exec 8<"$FIFO" + +n=0 +while [ $n -lt 50 ]; do + $TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . && break + sleep 0.1 + n=$((n + 1)) +done +$TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . || + { echo "control client did not attach"; exit 1; } + +# Drain the control stream slowly (~2.5KB/s). The pane floods far faster, so a +# backlog stays stuck in the server and the fifo stays full, which is what +# keeps the exit handshake from completing below. +( while :; do + dd bs=256 count=1 <&8 >>"$OUT" 2>/dev/null || exit 0 + sleep 0.1 +done ) & +READER=$! + +# Wait until the client is demonstrably backed up: the output only keeps +# growing at the slow-drain rate while the flood is outrunning the reader. +n=0 +while [ $n -lt 100 ]; do + [ "$(wc -c <"$OUT")" -ge 8000 ] && break + sleep 0.1 + n=$((n + 1)) +done +[ "$(wc -c <"$OUT")" -ge 8000 ] || { echo "client output did not back up"; exit 1; } + +# Detach the control client: this sets CLIENT_EXIT immediately. The stuck +# backlog holds the window between CLIENT_EXIT and the exit handshake open. +$TMUX detach-client -s rt + +# While the client is exiting, rename the window to the marker. Vanilla queues +# these %window-renamed notifications for the exiting client; the fix drops them. +i=0 +while [ $i -lt 5 ]; do + $TMUX rename-window -t rt:0 "$MARKER$i" + i=$((i + 1)) +done +sleep 0.5 + +# Drain fast (bounded) so the backlog, and any notification queued behind it, +# reaches OUT. A watchdog keeps this from blocking. +kill "$READER" 2>/dev/null +READER= +( dd bs=4096 count=128 <&8 >>"$OUT" 2>/dev/null ) & +DRAIN=$! +( sleep 3; kill "$DRAIN" 2>/dev/null ) & +WATCHDOG=$! +wait "$DRAIN" 2>/dev/null +kill "$WATCHDOG" 2>/dev/null + +# The marker must never appear as a notification to the exiting client. +if grep -q "%window-renamed .*$MARKER" "$OUT"; then + echo "notification queued to exiting control client:" + grep "%window-renamed .*$MARKER" "$OUT" + exit 1 +fi + +exit 0