Merge pull request #5358 from bmaurer/regress-control-client-exit

Add a regression test for output queued to exiting control clients
This commit is contained in:
Nicholas Marriott
2026-07-10 16:39:04 +01:00
committed by GitHub

View File

@@ -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