Merge branch 'master' into command_parser

This commit is contained in:
Nicholas Marriott
2026-08-07 16:08:26 +01:00
33 changed files with 919 additions and 210 deletions

View File

@@ -0,0 +1,77 @@
#!/bin/sh
# A control client whose terminal has stopped accepting output (for example
# its ssh connection or terminal emulator died) never drains the output that
# server_client_check_exit waits on before sending MSG_EXIT, and may then
# never close its socket either. This must not prevent the server from
# exiting: after kill-server the server previously stayed alive forever with
# server_exit set, closing every new connection immediately, so every new
# client failed with "server exited unexpectedly" until the server was killed
# by hand. The server now bounds the exit handshake: it discards output it
# can never deliver and drops the client if it still does not close.
#
# The stalled terminal is simulated by a fifo whose read end is held open but
# never read.
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
SERVER=
mkfifo "$FIFO" || exit 1
cleanup() {
[ -n "$SERVER" ] && kill -9 "$SERVER" 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
SERVER=$($TMUX display -pt rt '#{pid}')
# Attach a control client with output down the fifo; stdin held open by sleep
# so it stays attached. The fifo fills and is never read, so a backlog forms
# in the server that can never be delivered.
( sleep 60 ) | $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; }
# Let the pane flood fill the fifo so the client's output is stuck.
sleep 2
# Ask the server to exit. This sets CLIENT_EXIT on the stalled client, whose
# output can never drain.
$TMUX kill-server 2>/dev/null
# The server must exit within the exit timeout (10 seconds) plus slack,
# rather than staying in limbo forever rejecting new clients.
n=0
while [ $n -lt 100 ]; do
if ! kill -0 "$SERVER" 2>/dev/null; then
SERVER=
exit 0
fi
sleep 0.2
n=$((n + 1))
done
echo "server did not exit after kill-server; new clients see:"
$TMUX ls 2>&1
exit 1

158
regress/control-notify-guard.sh Executable file
View File

@@ -0,0 +1,158 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMPDIR=$(mktemp -d)
IN="$TMPDIR/in"
OUT="$TMPDIR/out"
BADCFG="$TMPDIR/bad.conf"
PID=
cleanup()
{
exec 3>&- 2>/dev/null
[ -n "$PID" ] && kill "$PID" 2>/dev/null
$TMUX kill-server 2>/dev/null
rm -rf "$TMPDIR"
}
trap cleanup EXIT
fail()
{
echo "$1" >&2
[ -s "$OUT" ] && cat "$OUT" >&2
exit 1
}
wait_for()
{
pattern=$1
timeout=${2:-60}
i=0
while [ "$i" -lt "$timeout" ]; do
grep -F -- "$pattern" "$OUT" >/dev/null 2>&1 && return 0
sleep 0.1
i=$((i + 1))
done
fail "missing: $pattern"
}
wait_for_count()
{
pattern=$1
expected=$2
timeout=${3:-60}
i=0
while [ "$i" -lt "$timeout" ]; do
count=$(grep -F -c -- "$pattern" "$OUT" 2>/dev/null)
[ "$count" -ge "$expected" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "missing count $expected for: $pattern"
}
send()
{
printf '%s\n' "$1" >&3 || fail "failed to send: $1"
}
# A notification is a standalone protocol message. It must never be part of
# the response between the real %begin and matching %end or %error for a
# command. Ignore guard-looking command output while a real guard is open.
check_guards()
{
awk '
function notification(line) {
return line ~ /^%(sessions-changed|session-changed |unlinked-window-add )/ ||
line ~ /^%(pause |continue |config-error )/
}
!open && $0 ~ /^%begin [0-9]+ [0-9]+ [0-9]+$/ {
open = 1
time = $2
number = $3
flags = $4
next
}
open && ($0 == "%end " time " " number " " flags ||
$0 == "%error " time " " number " " flags) {
open = 0
next
}
notification($0) {
seen[$1]++
if (open) {
print "notification inside guard: " $0 > "/dev/stderr"
bad = 1
}
}
END {
if (open) {
print "unterminated command guard" > "/dev/stderr"
bad = 1
}
if (seen["%sessions-changed"] < 2 ||
seen["%session-changed"] < 1 ||
seen["%unlinked-window-add"] < 2 ||
seen["%pause"] < 1 || seen["%continue"] < 1 ||
seen["%config-error"] < 1) {
print "missing expected notification" > "/dev/stderr"
bad = 1
}
exit bad
}' "$OUT" || fail "bad control protocol notification ordering"
}
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -s guard -x 80 -y 24 || exit 1
pane=$($TMUX display-message -p -t guard:0.0 '#{pane_id}') || exit 1
mkfifo "$IN" || exit 1
: >"$OUT"
$TMUX -C attach-session -t guard <"$IN" >"$OUT" 2>&1 &
PID=$!
exec 3>"$IN"
# Attaching generates a session notification synchronously from inside the
# attach command. It must follow the closing guard.
wait_for '%session-changed '
# Guard-looking output is arbitrary command output, not protocol state. Make
# new-session both generate notifications and print an unmatched fake %begin;
# it must not leave those notifications permanently deferred.
send "set-option -g @fake-begin '%begin 1 2 3'"
send "new-session -d -P -F '#{@fake-begin}' -s fake-begin"
wait_for_count '%sessions-changed' 1
# Likewise, fake %end output from a command that generates multiple
# notifications must not flush them before the command actually ends.
send "set-option -g @fake-end '%end 1 2 3'"
send "new-session -d -P -F '#{@fake-end}' -s fake-end"
wait_for_count '%sessions-changed' 2
# Exercise notification paths outside control-notify.c.
send "refresh-client -A '$pane:pause'"
wait_for "%pause $pane"
send "refresh-client -A '$pane:continue'"
wait_for "%continue $pane"
printf '%s\n' 'not-a-command' >"$BADCFG"
send "source-file '$BADCFG'"
wait_for '%config-error '
# A final command guarantees all earlier command guards have completed before
# the transcript is checked.
send "display-message -p 'guard-check-done'"
wait_for 'guard-check-done'
check_guards
exit 0

View File

@@ -114,5 +114,30 @@ $TMUX send-keys -X next-space-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "500xyz" ] || exit 1
# Test that vi cursor movement does not stop on the padding cell of a wide
# character at the end of a line.
$TMUX kill-server 2>/dev/null
sleep 1
$TMUX new -d -x20 -y5 \
"printf 'abc中\nxyz\n'; exec cat" || exit 1
$TMUX set-window-option -g mode-keys vi
$TMUX copy-mode
$TMUX send-keys -X history-top
$TMUX send-keys -X start-of-line
$TMUX send-keys -X cursor-right
$TMUX send-keys -X cursor-right
$TMUX send-keys -X cursor-right
[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "3,0" ] ||
exit 1
$TMUX send-keys -X cursor-right
[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "0,1" ] ||
exit 1
$TMUX send-keys -X cursor-left
[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "3,0" ] ||
exit 1
$TMUX send-keys -X cursor-left
[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "2,0" ] ||
exit 1
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -192,13 +192,13 @@ check_fail "invalid window name: $(printf 'a\377b')" \
break-pane -d -n "$(printf 'a\377b')" -s "$p1" -t P:
# join-pane can move a pane from one window to another without destroying
# the source window if other panes remain. (On this branch move-pane is
# reserved for floating panes, covered by floating-pane-geometry.sh.)
# the source window if other panes remain. move-pane does the same when no
# floating-pane movement flags are given.
check_ok new-window -d -t P:5 -n other
check_ok join-pane -d -s "$p1" -t P:5.0
check_fmt 'P:5' '#{window_panes}' '2'
check_fmt 'P:0' '#{window_panes}' '3'
check_ok join-pane -d -v -s "$p1" -t "$p2"
check_ok move-pane -d -v -s "$p1" -t "$p2"
check_fmt 'P:0' '#{window_panes}' '4'
check_fmt 'P:5' '#{window_panes}' '1'

View File

@@ -35,9 +35,9 @@ check_alive()
# A pipe-pane -I child may write after the pane process has exited. With
# remain-on-exit, the pane stays around but its bufferevent has been freed.
check_ok new-session -d -s pipe -x 80 -y 24 'sleep 0.2'
check_ok new-session -d -s pipe -x 80 -y 24 'sleep 1'
check_ok set-option -t pipe:0 remain-on-exit on
check_ok pipe-pane -t pipe:0.0 -I 'sleep 0.6; printf x'
check_ok pipe-pane -t pipe:0.0 -I 'sleep 2; printf x'
i=0
while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do
@@ -46,7 +46,7 @@ while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do
sleep 0.1
done
sleep 0.7
sleep 2
check_alive
$TMUX kill-server 2>/dev/null

49
regress/server-socket-error.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/sh
# Server socket creation failures must produce a nonzero client exit status.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
# Root bypasses directory mode bits, so it cannot trigger this failure.
[ "$(id -u)" -eq 0 ] && exit 0
TMP=$(mktemp -d) || exit 1
SOCKET="$TMP/ro/socket"
cleanup()
{
chmod 700 "$TMP/ro" 2>/dev/null
rm -rf "$TMP"
}
fail()
{
echo "$1" >&2
cleanup
exit 1
}
mkdir "$TMP/ro" || exit 1
chmod 500 "$TMP/ro" || exit 1
trap cleanup 0 1 15
check_failure()
{
output=$($TEST_TMUX -S "$SOCKET" -f/dev/null "$@" 2>&1)
retval=$?
[ "$retval" -ne 0 ] || fail "tmux $* exited zero: $output"
case "$output" in
"error creating $SOCKET ("*) ;;
*) fail "tmux $* produced unexpected error: $output" ;;
esac
}
check_failure start-server
check_failure new-session -d
exit 0

43
regress/tab-cell-background.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/sh
# When a write breaks up a wide character or a tab, the columns it covered must
# keep the background colour, both in the grid and on the terminal. The second
# server is attached to the first so that what is actually sent is checked, not
# just what tmux has stored.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP" 0 1 15
# 1. Write inside a tab. 2. Overwrite the right half of a wide character.
# 3. Two tabs of different colours, the first partly deleted so that padding
# from both ends up next to each other, then written over.
# The sleep is so that the client is attached before anything is written and
# the changes are sent as they happen rather than as one redraw.
$TMUX2 -f/dev/null new -d -x24 -y3 "
sleep 2
printf '\033[44m\033[K\t\033[4GX'
printf '\033[2;1H\033[44m\033[K\344\270\226\033[2GX'
printf '\033[3;1H\033[42m\033[K\t\033[43m\033[K\t\033[0m\033[4G\033[8P\rx'
cat" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX -f/dev/null new -x24 -y4 -d "$TMUX2 attach" || exit 1
sleep 4
$TMUX capturep -pe -S0 -E2 >$TMP || exit 1
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
printf '\033[44m X\n X\n\033[49mx\033[42m \033[43m \033[49m\n' |
cmp - $TMP || exit 1
exit 0