test/e2e_01_boot.sh
Ref: Size: 64.8 KiB History
# shellcheck shell=sh
# e2e_01_boot.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in
# the order they stand in; see the lib's header for what this file may
# assume and what it must register.
# Second daemon, used only by the M7 abort scenario.
SOCK2="${TMPDIR:-/tmp}/muxd-e2e-abort-$$.sock"
defer_sock "$SOCK2"
# Third daemon, for the restart scenario: it gets killed and started again on
# the same path, so it cannot share the long-lived one.
SOCK3="${TMPDIR:-/tmp}/muxd-e2e-restart-$$.sock"
defer_sock "$SOCK3"
# Fourth daemon, for the M8 --quic scenario: it is the only one holding a UDP
# port, so it gets its own path rather than sharing the long-lived one.
SOCK4="${TMPDIR:-/tmp}/muxd-e2e-quic-$$.sock"
defer_sock "$SOCK4" "$SOCK4.nokey" "$SOCK4.second"
QKEY="${TMPDIR:-/tmp}/mux-e2e-key-$$"
defer_rm "$QKEY" "$QKEY.wrong" "$QKEY.bad"
# A port out of the way of the ephemeral range, made per-run so two suites can
# overlap. Collisions surface as a loud bind failure, never as a silent pass.
QPORT=$(( 21000 + ($$ % 4000) ))
# M10 key-resolution daemons. Two, not one: the first scenario's client
# detaches and its daemon is killed, and the second must not be sharing
# either. Ports in bands of their own so a concurrent suite cannot collide.
SOCK9="${TMPDIR:-/tmp}/muxd-e2e-envkey-$$.sock"
defer_sock "$SOCK9"
SOCK10="${TMPDIR:-/tmp}/muxd-e2e-flagwins-$$.sock"
defer_sock "$SOCK10"
QPORT2=$(( 26000 + ($$ % 4000) ))
QPORT3=$(( 31000 + ($$ % 4000) ))
# M10 `mux d start -d`. These daemons are spawned DETACHED, so the suite never
# holds their pids as shell jobs — it reads them off the up-line and kills
# by that tracked pid, never by name.
SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock"
defer_sock "$SOCK8"
SOCK8T="${TMPDIR:-/tmp}/muxd-e2e-trunc-$$.sock"
defer_sock "$SOCK8T"
SOCK11="${TMPDIR:-/tmp}/muxd-e2e-goal-$$.sock"
defer_sock "$SOCK11"
QPORT4=$(( 36000 + ($$ % 4000) ))
# --- M10: --version answers "did the scp land" without a daemon anywhere.
# No convergence: no daemon and no client, so there is no grid on either side.
"$MUX" d --version | grep -q '^mux 0\.' || { echo "e2e FAIL: mux d --version"; exit 1; }
"$MUX" --version | grep -q '^mux 0\.' || { echo "e2e FAIL: mux --version"; exit 1; }
# Both must report the SAME version: the greps above pass just as happily for
# a binary that re-hardcoded a literal instead of reading build.zig's constant,
# which is exactly the drift --version exists to rule out.
[ "$("$MUX" d --version | cut -d' ' -f2)" = "$("$MUX" --version | cut -d' ' -f2)" ] || {
echo "e2e FAIL: version drift between binaries"; exit 1; }
ok "--version on both binaries"
# --- M10: keygen writes 0600, prints the path, refuses a second run.
# No convergence: keygen touches the filesystem, never a session.
KEYOUT=$("$MUX" d keygen)
[ "$KEYOUT" = "$XDG_CONFIG_HOME/mux/key" ] || {
echo "e2e FAIL: keygen printed '$KEYOUT'"; exit 1; }
PERMS=$(file_mode "$KEYOUT")
[ "$PERMS" = "600" ] || { echo "e2e FAIL: keygen perms $PERMS, want 600"; exit 1; }
DPERMS=$(file_mode "$XDG_CONFIG_HOME/mux")
[ "$DPERMS" = "700" ] || { echo "e2e FAIL: key dir perms $DPERMS, want 700"; exit 1; }
SUM1=$(sha256_of "$KEYOUT")
if "$MUX" d keygen > /dev/null 2>&1; then
echo "e2e FAIL: second keygen did not refuse"; exit 1
fi
SUM2=$(sha256_of "$KEYOUT")
[ "$SUM1" = "$SUM2" ] || { echo "e2e FAIL: refused keygen still changed the key"; exit 1; }
ok "keygen creates once, 0600 in a 0700 dir, refuses twice"
start_daemon "$SOCK" "$OUT.d1.d" "socket never appeared" --shell /bin/sh
D1PID=$DPID
# Client with piped stdio: types a command, waits for its output, detaches
# with the Ctrl-\ chord (prefix, then prefix again for detach).
pipe_mux "$OUT" "" timeout 30 "$MUX" --sock "$SOCK"
pipe_send 'printf "e2e-%%s\\n" works\n'
# 1. The client's rendered output must contain the command's result.
await_out "$OUT" "e2e-works" "client render missing output"
pipe_detach
# 2. The daemon kept the session; its grid must match.
"$MUX" d dump --sock "$SOCK" | grep -q "e2e-works" || {
echo "e2e FAIL: daemon grid missing output"; exit 1;
}
# 3. Detach left the daemon running.
kill -0 "$D1PID" || { echo "e2e FAIL: daemon died on detach"; exit 1; }
# 4. The M11 claim itself: the screen the client painted equals the
# screen the daemon holds — same engine, same formatter, both formats.
assert_converged "$OUT" "$SOCK" "base attach"
# Preloaded stdin belongs to the terminal command loop, not the dial's
# cancellation signal. Both local transports must attach/create before the
# queued detach is handled; merely changing an early-abort exit code to zero
# would still leave the requested session absent.
EARLYSOCK="$SOCK.early-detach"
EARLYSTATE="$OUT.early.state"
defer_sock "$EARLYSOCK"
defer_rm "$EARLYSTATE"
mkdir -p "$EARLYSTATE"
start_daemon "$EARLYSOCK" "$OUT.early.d" "early-detach daemon never bound" --shell /bin/sh
XDG_STATE_HOME="$EARLYSTATE" python3 - "$MUX" "$EARLYSOCK" <<'PY'
import os
import subprocess
import sys
mux, sock = sys.argv[1:]
for name, target in (("early-socket", ["--sock", sock]),
("early-stdio", ["--via", mux + " d proxy --sock " + sock])):
reader, writer = os.pipe()
os.write(writer, bytes([28]) + b"d")
os.close(writer)
try:
result = subprocess.run([mux, *target, "--session", name], stdin=reader,
capture_output=True, timeout=10)
finally:
os.close(reader)
if result.returncode != 0:
raise AssertionError((name, result.returncode, result.stderr.decode()))
subprocess.run([mux, "a", "status", "--sock", sock, "--session", name,
"--timeout", "2000"], stdout=subprocess.DEVNULL,
check=True, timeout=5)
PY
ok "preloaded detach on socket and stdio clients attaches first and preserves the new session"
# The control: a doctored stream must NOT converge. A convergence check
# that cannot fail proves nothing (the wan.sh rule, M9).
cp "$OUT" "$OUT.doctored"
printf '\033[12;1Hconvergence-control-glyphs' >> "$OUT.doctored"
if converged_quiet "$OUT.doctored" "$SOCK"; then
echo "e2e FAIL: convergence control did not fire on a doctored stream"; exit 1
fi
rm_swept "$OUT.doctored" "$OUT.doctored.render" "$OUT.doctored.dump" \
"$OUT.doctored.render.n" "$OUT.doctored.dump.n" "$OUT.doctored.diff" \
"$OUT.doctored.rvt" "$OUT.doctored.dvt" \
"$OUT.doctored.rvt.n" "$OUT.doctored.dvt.n"
ok "convergence control fires on a doctored stream"
# --- M11: a styled specimen, so the byte-exact leg has something to compare.
# The campaign's row 4 broke the delta paint's leading SGR reset and BOTH
# suites passed — not because the styled comparison is weak but because no
# scenario in the corpus had ever emitted a colour. A suppressed reset resets
# nothing when nothing upstream is styled, so the instrument was handed no
# specimen. This scenario is the specimen: two delta rows carrying a live
# attribute, followed by a plain prompt row that must come back unstyled.
#
# The escapes are doubled because they are written by the SESSION's shell,
# not by this one: what goes down the pipe is the literal text
# `printf "\033[1;31mstyled-%s\033[0m\n" red bold`.
pipe_mux "$OUT.st" "" timeout 30 "$MUX" --sock "$SOCK"
pipe_send 'printf "\\033[1;31mstyled-%%s\\033[0m\\n" red bold\n'
await_out "$OUT.st" "styled-bold" "the second styled row never reached the client"
pipe_detach
grep -q "styled-red" "$OUT.st" || {
echo "e2e FAIL: styled output never reached the client"; cat "$OUT.st"; exit 1; }
# The plain leg would pass on a bled attribute — same glyphs, wrong colours.
# The --vt leg inside assert_converged is the one that speaks here.
assert_converged "$OUT.st" "$SOCK" "styled content"
rm_swept "$OUT.st"
ok "styled content survives the paint path"
# --- M3: kill a client mid-run; daemon survives; reattach lands correctly.
# No convergence on $OUT.kill: kill -9 truncates the stream mid-paint, possibly
# mid-escape-sequence, so a half-drawn capture is the expected shape here.
{ printf 'seq 1 60\n'; sleep 2; } | "$MUX" --sock "$SOCK" > "$OUT.kill" &
CPID=$!
defer_kill "$CPID"
sleep 1
hardkill "$CPID"
sleep 1
kill -0 "$D1PID" || { echo "e2e FAIL: daemon died after client kill -9"; exit 1; }
{ sleep 1; printf '\034\034'; } | timeout 30 "$MUX" --sock "$SOCK" > "$OUT.re"
grep -q "60" "$OUT.re" || {
echo "e2e FAIL: reattach after kill missing state"; cat "$OUT.re"; exit 1;
}
assert_converged "$OUT.re" "$SOCK" "reattach after kill"
rm_swept "$OUT.kill" "$OUT.re"
# --- M5: two clients on one session. Output typed in A reaches both; then A
# detaches and B must still have a live input path (its own marker echoes back
# after A is gone), so one client's detach does not disturb the other.
{ sleep 0.5; printf 'printf "m5-%%s\\n" both\n'; sleep 2.5; printf '\034\034'; } | \
"$MUX" --sock "$SOCK" > "$OUT.a" &
APID=$!
defer_kill "$APID"
{ sleep 4; printf 'printf "m5-%%s\\n" after-a-left\n'; sleep 2.5; printf '\034\034'; } | \
"$MUX" --sock "$SOCK" > "$OUT.b" &
BPID=$!
defer_kill "$BPID"
wait "$APID" "$BPID"
# The marker text never appears in the echoed command line ("m5-%s" plus a
# separate argument), so a hit proves the shell ran it and the result came back.
grep -q "m5-both" "$OUT.a" || { echo "e2e FAIL: client A missing shared output"; exit 1; }
grep -q "m5-both" "$OUT.b" || { echo "e2e FAIL: client B missing shared output"; exit 1; }
grep -q "m5-after-a-left" "$OUT.b" || {
echo "e2e FAIL: client B lost its input path after A detached"; exit 1;
}
kill -0 "$D1PID" || { echo "e2e FAIL: daemon died in two-client scenario"; exit 1; }
# B's capture, not A's: A detached before B's marker landed, so A's stream ends
# on a grid the daemon has since moved past. B saw both, and its convergence
# covers the same grid.
assert_converged "$OUT.b" "$SOCK" "two clients"
rm_swept "$OUT.a" "$OUT.b"
# --- M6: the same protocol over an arbitrary byte pipe. `mux d proxy` is a
# frame-agnostic stdio<->socket pump; if the session works through it, the
# transport really is a swap. `--via` splits the string on whitespace into
# argv and execs it, so $MUX and $SOCK must contain no spaces — they are the
# build tree's artifact path and this suite's socket, which do not.
#
# XDG_RUNTIME_DIR is pointed at nothing so the test cannot pass by environment
# luck: if --via ever silently fell back to the default socket path, that path
# would resolve to a directory that does not exist and the session would fail
# instead of quietly attaching over the local socket.
pipe_mux "$OUT.via" "" env XDG_RUNTIME_DIR=/nonexistent-mux-e2e "$MUX" --via "$MUX d proxy --sock $SOCK"
pipe_send 'printf "m6-%%s\\n" via-pipe\n'
await_out "$OUT.via" "m6-via-pipe" "--via transport"
pipe_detach
kill -0 "$D1PID" || { echo "e2e FAIL: daemon died in --via scenario"; exit 1; }
assert_converged "$OUT.via" "$SOCK" "via transport"
rm_swept "$OUT.via"
# Same transport, on a TERMINAL, for what the wall must NOT do with it: a
# `--via` run records no layout. `hosts.zig` refuses to write a `--via`
# line, so no hosts table can ever hold a row for one, while a tile of that
# transport still spells itself `--via CMD#NAME` — the only spelling it
# has. Saved, that leaf is one the next start's loader finds no host for,
# and it refuses the WHOLE file: one throwaway `--via` run would cost the
# user every pane they had authored. A pty and a state dir of its own are
# both required to see it — the layout path is opened only when stdin is a
# terminal, so the piped run above cannot show this either way.
VIASTATE="${TMPDIR:-/tmp}/mux-e2e-vias-$$"
defer_rm "$VIASTATE"
mkdir -p "$VIASTATE"
set +e
env XDG_STATE_HOME="$VIASTATE" XDG_RUNTIME_DIR=/nonexistent-mux-e2e SHELL=/bin/sh \
timeout 60 "$PTYCLIENT" --cols 80 --rows 24 \
--out "$OUT.viapty" --err "$OUT.viapty.err" -- \
"$MUX" --via "$MUX d proxy --sock $SOCK" \
> "$OUT.viapty.log" 2>&1 <<'VIAEOF'
settle 700 20000
send printf 'm6-%s\n' via-tty\n
expect m6-via-tty 20000
settle 500 15000
send \x1cd
waitexit 10000
VIAEOF
RC=$?
set -e
rc0 "--via on a terminal exited $RC:" "$OUT.viapty.log" "$OUT.viapty.err"
grep -q 'layout not saved: a --via wall is not recorded' "$OUT.viapty" || {
echo "e2e FAIL: a --via wall on a terminal did not say it records nothing:"
cat "$OUT.viapty"; exit 1; }
[ ! -e "$VIASTATE/mux/layout" ] || {
echo "e2e FAIL: a --via run wrote a layout; the next start's loader refuses"
echo " the whole file over its leaf, taking every other pane with it:"
cat "$VIASTATE/mux/layout"; exit 1; }
rm_swept "$OUT.viapty" "$OUT.viapty.err" "$OUT.viapty.log"
# --- M10: a --via command that dies before the first frame stops claiming
# a connection existed. ssh's own stderr still passes through untouched.
# No convergence: no session was ever established, so there is no grid to match.
# A program, not a shell line: `--via` execs its words, so "die at once"
# is a script and not a `sh -c` the client would have had to parse.
VIADEAD="${TMPDIR:-/tmp}/mux-e2e-viadead-$$.sh"
defer_rm "$VIADEAD"
printf '#!/bin/sh\nexit 127\n' > "$VIADEAD"
chmod +x "$VIADEAD"
set +e
"$MUX" --via "$VIADEAD" > "$OUT.via" 2>&1
VRC=$?
set -e
[ "$VRC" = "1" ] || { echo "e2e FAIL: dead --via exit $VRC, want 1"; exit 1; }
grep -q "transport command failed before a session started" "$OUT.via" || {
echo "e2e FAIL: --via death message:"; cat "$OUT.via"; exit 1; }
grep -q "connection to the daemon lost" "$OUT.via" && {
echo "e2e FAIL: the old lie is still printed"; cat "$OUT.via"; exit 1; }
rm_swept "$OUT.via"
ok "--via failure says what happened"
# --- M7: a transport that dies before any session must exit, not retry. The
# reconnect loop resumes sessions; it must not turn a bad --via command into
# an unkillable client. With piped stdin there is no Ctrl-\ to rescue it, so
# a regression here hangs forever: exit 124 below is the timeout, and it is
# the failure this test exists to catch.
# No convergence: the transport died first, so no session and no grid.
set +e
printf '#!/bin/sh\nexit 7\n' > "$VIADEAD"
timeout 10 "$MUX" --via "$VIADEAD" < /dev/null > "$OUT.dead" 2>&1
RC=$?
set -e
[ "$RC" -eq 1 ] || {
echo "e2e FAIL: dead first transport exited $RC (want 1; 124 means it hung retrying)"
cat "$OUT.dead"; exit 1;
}
# M10 reworded this one: a --via program that exits 7 is the same shape as the scenario
# above — a transport command that died before carrying a frame — so it now
# gets the honest message. What this scenario is FOR is the exit code above
# (1, never 124); the diagnostic is asserted so the exit is not a silent one.
grep -q "transport command failed before a session started" "$OUT.dead" || {
echo "e2e FAIL: dead first transport lost its diagnostic; got:"; cat "$OUT.dead"; exit 1;
}
# The can-fail control for the M13 reword: a grep for the new wording passes
# just as well if BOTH lines are printed, so the old one must be absent from
# the very capture that just satisfied it. The dropped parenthetical guessed
# a cause the transport's own stderr had already named.
grep -q "is mux installed on the host" "$OUT.dead" && {
echo "e2e FAIL: old lostMsg wording still emitted alongside the new pin"
cat "$OUT.dead"; exit 1; }
# Free ride on this capture, for a property with nothing to do with the exit
# code: this client had NON-TTY stdin and died before any frame, so it is
# the one place in the suite that proves mux does not push a title onto a
# terminal it never took over. An unmatched push is a title stack that only
# grows, and nothing later would pop it — the teardown never runs.
# -F is load-bearing, not decoration: without it `[22;0t` is a malformed
# bracket expression, grep EXITS 2 rather than 1, and `grep && { fail }`
# reads an error as "not found" — an assertion that can never fire. Caught
# by running this needle by hand against a capture known to contain it.
grep -qaF "$(printf '\033[22;0t')" "$OUT.dead" && {
echo "e2e FAIL: a client that entered nothing still pushed the title stack"
cat -v "$OUT.dead"; exit 1; }
# A socket path past sun_path's 107 usable bytes is refused by name, and
# the refusal belongs to the one binder: `mux d` reads its own flags,
# refuses at parse and exits 1 before any fork. The client no longer
# compares the length at all — it asks for a daemon and relays what the
# daemon says — so this is also the pin on that relay: the DAEMON's words,
# on the client's stderr, fast, with no `starting` line in front of them.
# The failure both replaced was a 2s poll ending in "daemon did not
# answer", a timeout story about a path doomed at parse.
LONGSOCK="/tmp/$(printf 'a%.0s' $(seq 1 110)).sock"
"$MUX" d dump --sock "$LONGSOCK" > "$OUT.long" 2>&1 && { echo "e2e FAIL: mux d accepted long sock"; exit 1; }
grep -q "mux d: socket path too long" "$OUT.long" || { echo "e2e FAIL:"; cat "$OUT.long"; exit 1; }
LONGT0=$(date +%s)
"$MUX" --sock "$LONGSOCK" > "$OUT.longc" 2>&1 && { echo "e2e FAIL: mux accepted long sock"; exit 1; }
LONGEL=$(( $(date +%s) - LONGT0 ))
grep -q "mux d: socket path too long" "$OUT.longc" || {
echo "e2e FAIL: the client did not relay the daemon's refusal:"; cat "$OUT.longc"; exit 1; }
grep -q "daemon did not answer" "$OUT.longc" && { echo "e2e FAIL: still the timeout story"; cat "$OUT.longc"; exit 1; }
# No `starting` line: the daemon refuses at parse, so nothing was ever
# forked and nothing should have announced that it was about to be. This
# is what a re-comparison in the client used to buy, and what the owner's
# own refusal buys now.
grep -q "starting" "$OUT.longc" && {
echo "e2e FAIL: a doomed path still announced a start:"; cat "$OUT.longc"; exit 1; }
[ "$LONGEL" -le 1 ] || {
echo "e2e FAIL: a doomed --sock took ${LONGEL}s to be refused (want <=1)"; exit 1; }
"$MUX" d --version --sock "$LONGSOCK" >/dev/null 2>&1 || { echo "e2e FAIL: --version refused over sock length"; exit 1; }
rm_swept "$OUT.dead" "$OUT.long" "$OUT.longc"
# --- M7: aborting a reconnect exits cleanly. The client establishes a real
# session (so reconnect is allowed), its daemon is then killed under it, and
# Ctrl-\ arrives while it is retrying. Exit 0 and the message; a double-close
# of the transport would abort here instead (SIGABRT = 134).
# No convergence: the client exits while reconnecting and its daemon is dead,
# so there is nothing left to dump against.
start_daemon "$SOCK2" "$OUT.d2.d" "second socket never appeared" --shell /bin/sh
D2PID=$DPID
set +e
{ printf 'echo m7-abort-live\n'; sleep 3; printf '\034'; sleep 2; } | \
timeout 20 "$MUX" --sock "$SOCK2" > "$OUT.abort" 2>&1 &
CLIPID=$!
defer_kill "$CLIPID"
# Let the session establish, then take the daemon away mid-session.
sleep 2
hardkill "$D2PID"
D2PID=""
wait "$CLIPID"
RC=$?
set -e
[ "$RC" -eq 0 ] || {
echo "e2e FAIL: abort during reconnect exited $RC (want 0; 134 = panic, 124 = hung)"
cat "$OUT.abort"; exit 1;
}
grep -q "detached while reconnecting" "$OUT.abort" || {
echo "e2e FAIL: abort during reconnect lost its message; got:"; cat "$OUT.abort"; exit 1;
}
rm_swept "$OUT.abort" "$SOCK2"
# --- M7 Scenario A: kill the transport mid-session; the client must resume
# by DELTA. The proxy is the transport, so killing it stands in for an ssh
# drop while the daemon and its session carry on untouched.
#
# The counter is the real assertion, not the markers: a snapshot-served
# resume renders "m7-after" perfectly well, so only `snapshots` holding
# still across the tear proves the resume was a delta — which is the whole
# milestone claim.
pipe_mux "$OUT.m7" "$OUT.m7.err" env XDG_RUNTIME_DIR=/nonexistent-mux-e2e timeout 40 \
"$MUX" --via "$MUX d proxy --sock $SOCK"
pipe_send 'printf "m7-%%s\\n" before\n'
# Tear only once the session is provably live and idle: the marker is the
# shell's own output, so seeing it means attached and drained.
wait_for "$OUT.m7" "m7-before" 20 || {
echo "e2e FAIL: m7 pre-tear output never arrived"
cat "$OUT.m7" "$OUT.m7.err" 2>/dev/null; exit 1;
}
SNAPS_BEFORE=$("$MUX" d stats --sock "$SOCK" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p')
PAINTS_BEFORE=$(repaints "$OUT.m7")
PPID_PROXY=$(proxy_pid "$SOCK")
[ -n "$PPID_PROXY" ] || { echo "e2e FAIL: could not find the proxy to kill"; exit 1; }
kill -9 "$PPID_PROXY"
# The tear must have hit the transport only; the daemon is what we are
# proving survives, so say so out loud rather than inferring it later.
kill -0 "$D1PID" || { echo "e2e FAIL: the tear killed the daemon, not the proxy"; exit 1; }
await_repaint "$OUT.m7" "$PAINTS_BEFORE" "m7 client never resumed after the transport was killed"
pipe_send 'printf "m7-%%s\\n" after\n'
await_out "$OUT.m7" "m7-after" "m7 client did not resume after the transport was killed"
pipe_detach "m7 client"
SNAPS_AFTER=$("$MUX" d stats --sock "$SOCK" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p')
[ -n "$SNAPS_BEFORE" ] && [ -n "$SNAPS_AFTER" ] || {
echo "e2e FAIL: could not read the snapshots counter"; exit 1;
}
[ "$SNAPS_BEFORE" = "$SNAPS_AFTER" ] || {
echo "e2e FAIL: reconnect was served a snapshot ($SNAPS_BEFORE -> $SNAPS_AFTER), not a delta"
exit 1;
}
assert_converged "$OUT.m7" "$SOCK" "delta resume"
rm_swept "$OUT.m7" "$OUT.m7.err"
# --- M7 Scenario B: kill the DAEMON under an attached client and start a new
# one on the same path. The seq the client holds belongs to a session that no
# longer exists, so the epoch fence must refuse it and serve a snapshot of the
# fresh session instead. Needs its own daemon, since this one gets killed.
start_daemon "$SOCK3" "$OUT.d3a.d" "restart-scenario socket never appeared" --shell /bin/sh
D3PID=$DPID
pipe_mux "$OUT.m7b" "$OUT.m7b.err" timeout 40 "$MUX" --sock "$SOCK3"
pipe_send 'printf "m7b-%%s\\n" pre-restart\n'
wait_for "$OUT.m7b" "m7b-pre-restart" 20 || {
echo "e2e FAIL: m7b pre-restart output never arrived"
cat "$OUT.m7b" "$OUT.m7b.err" 2>/dev/null; exit 1;
}
# SIGKILL leaves the socket file behind; the new daemon's stale-socket
# recovery (ECONNREFUSED + S_ISSOCK -> unlink) is what lets it rebind here.
PAINTS_BEFORE=$(repaints "$OUT.m7b")
D3DEAD=$D3PID
hardkill "$D3PID"
sleep 0.5
# The SIGKILL also strands the dead daemon's agent directory beside the
# socket — nothing of it runs to remove one — and the successor on the same
# socket directory is what reaps it (xdg.reapDeadPid). Asserted on the
# directory itself, by the dead pid in its name, before and after.
AGENTLEFT=$(find "$(dirname "$SOCK3")" -maxdepth 1 -name "mux-agent-$D3DEAD-*" | wc -l)
[ "$AGENTLEFT" -eq 1 ] || {
echo "e2e FAIL: a SIGKILLed daemon left $AGENTLEFT agent directories for pid $D3DEAD (want 1)"; exit 1; }
start_daemon "$SOCK3" "$OUT.d3b.d" "daemon did not rebind the stale socket" --shell /bin/sh
D3PID=$DPID
# The reap is not a boot step: it happens inside the successor's own
# `makeDir`, which runs when its FIRST session is born — here, when the
# stranded client above redials. A bound socket says nothing about that,
# so wait for the successor's own directory to appear and read the
# predecessor's only then. Asserting straight off start_daemon read a
# directory that was removed microseconds later, which is a race Linux
# won every time and a Mac lost (2026-09-04).
_i=0
until [ -n "$(find "$(dirname "$SOCK3")" -maxdepth 1 -name "mux-agent-$D3PID-*")" ]; do
_i=$((_i + 1)); [ "$_i" -lt $(( 100 * TIME_SCALE )) ] || {
echo "e2e FAIL: the restarted daemon never made an agent directory of its own,"
echo " so nothing here has reaped yet and the check below would lie:"
find "$(dirname "$SOCK3")" -maxdepth 1 -name 'mux-agent-*'; exit 1; }
sleep 0.1
done
AGENTLEFT=$(find "$(dirname "$SOCK3")" -maxdepth 1 -name "mux-agent-$D3DEAD-*" | wc -l)
[ "$AGENTLEFT" -eq 0 ] || {
echo "e2e FAIL: the restarted daemon left its SIGKILLed predecessor's agent directory:"
find "$(dirname "$SOCK3")" -maxdepth 1 -name "mux-agent-$D3DEAD-*"; exit 1; }
ok "a restarted daemon reaps the agent directory its SIGKILLed predecessor stranded"
await_repaint "$OUT.m7b" "$PAINTS_BEFORE" "m7b client never resumed into the restarted daemon"
pipe_send 'printf "m7b-%%s\\n" post-restart\n'
await_out "$OUT.m7b" "m7b-post-restart" "m7b client did not resume into the restarted daemon"
pipe_detach "m7b client"
# The fresh session really is fresh: a new daemon means a new shell, so the
# pre-restart marker cannot be in its grid. If it were, we would be looking
# at a client that reconnected to something with the old session's history —
# i.e. the epoch fence letting a stale seq through.
if "$MUX" d dump --sock "$SOCK3" | grep -q "m7b-pre-restart"; then
echo "e2e FAIL: restarted daemon's grid still holds the pre-restart marker"
exit 1
fi
# Markers are blind to the kind of resume — a broken epoch fence handing back
# a garbage delta would render "m7b-post-restart" just as well. Only the
# counter sees it: the reconnecting client is this daemon's sole client, so a
# snapshot in its stats is proof it was resynced from scratch rather than
# resumed off a seq that belongs to a session that no longer exists.
SNAPS_NEW=$("$MUX" d stats --sock "$SOCK3" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p')
[ -n "$SNAPS_NEW" ] || { echo "e2e FAIL: could not read the restarted daemon's counters"; exit 1; }
[ "$SNAPS_NEW" -ge 1 ] || {
echo "e2e FAIL: restarted daemon served no snapshot ($SNAPS_NEW); the stale seq was honoured"
exit 1;
}
assert_converged "$OUT.m7b" "$SOCK3" "epoch resync"
rm_swept "$OUT.m7b" "$OUT.m7b.err"
# --- M8: the --quic flags. Every refusal must cost nothing — no session
# socket, no shell, no stack trace — and the accepted case must leave a
# daemon that is both listening on UDP and still an ordinary daemon.
#
# The client half of this (attaching over quic://) is Task 3; what is proven
# here is the daemon's side of the command line.
# A refusal that leaves a socket behind has already started a session, which
# is the failure this ordering exists to prevent.
# No convergence anywhere in refuse(): no daemon survives one, by construction.
# Takes the config home to run under as an explicit second argument, rather
# than relying on a `XDG_CONFIG_HOME=... refuse ...` prefix: POSIX says an
# assignment prefixing a FUNCTION call may outlive it, so on a dash /bin/sh
# that form could leak into every later scenario. Naming it per call site
# also makes each refusal say which key world it is refusing in, which is
# the thing that silently changes once a default key path exists.
refuse() {
_want="$1"; _cfg="$2"; shift 2
set +e
# Timed out rather than trusted to exit: every case here is a refusal, so
# a regression that ACCEPTS one would otherwise run a daemon forever and
# hang the suite instead of failing it. 124 is a distinguishable answer.
env XDG_CONFIG_HOME="$_cfg" timeout 10 "$MUX" d start --sock "$SOCK4" \
--shell /bin/sh "$@" > "$OUT.q" 2>&1
_rc=$?
set -e
[ "$_rc" -eq "$_want" ] || {
echo "e2e FAIL: mux d start $* exited $_rc (want $_want)"; cat "$OUT.q"; exit 1;
}
[ ! -e "$SOCK4" ] || {
echo "e2e FAIL: mux d start $* was refused but left $SOCK4 behind"; exit 1;
}
# One line of complaint plus the usage block, and not a stack trace: a
# Zig panic runs to dozens of lines and names a source file, which is
# what this is guarding against.
#
# Two bounds, because each misses what the other catches. The relative
# one is measured from the no-args usage path, which is NOT one of the
# paths it bounds: if usage printing ever ballooned, the bound would
# balloon with it and quietly stop catching a panic. The absolute one
# cannot drift, and 16 sits above any plausible complaint-plus-usage
# (the block is 15 lines since `upgrade HOST` joined it) and below the
# ~20 lines of the smallest Debug panic.
_usage_lines=$("$MUX" d 2>&1 | wc -l)
_lines=$(wc -l < "$OUT.q")
{ [ "$_lines" -le $((_usage_lines + 2)) ] && [ "$_lines" -le 16 ]; } || {
echo "e2e FAIL: mux d start $* answered with more than a message:"; cat "$OUT.q"; exit 1;
}
}
head -c 32 /dev/urandom > "$QKEY"
chmod 600 "$QKEY"
cp "$QKEY" "$QKEY.bad"
chmod 644 "$QKEY.bad"
# Both or neither, and a usage mistake exits 2 like every other one.
# The no-key refusal must not find the suite's own keygen'd key, so it runs
# against a config home that has never had one written to it.
NOKEY_CFG="${TMPDIR:-/tmp}/mux-e2e-nokey-$$"
defer_rm "$NOKEY_CFG"
refuse 2 "$NOKEY_CFG" --quic "127.0.0.1:$QPORT"
refuse 2 "$XDG_CONFIG_HOME" --key "$QKEY"
refuse 2 "$XDG_CONFIG_HOME" --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 0
refuse 2 "$XDG_CONFIG_HOME" --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms soon
# Refusals that are about the world rather than the spelling exit 1.
refuse 1 "$XDG_CONFIG_HOME" --quic "127.0.0.1:" --key "$QKEY"
refuse 1 "$XDG_CONFIG_HOME" --quic "localhost:$QPORT" --key "$QKEY"
refuse 1 "$XDG_CONFIG_HOME" --quic "127.0.0.1:$QPORT" --key "$QKEY.bad"
refuse 1 "$XDG_CONFIG_HOME" --quic "127.0.0.1:$QPORT" --key "$QKEY.missing"
# The key-missing message must name the way out, not just the absence.
env XDG_CONFIG_HOME="$NOKEY_CFG" "$MUX" d start --sock "$SOCK4.nokey" \
--quic "127.0.0.1:$QPORT" 2> "$OUT.nokey" || true
grep -q "mux d keygen" "$OUT.nokey" || {
echo "e2e FAIL: key-missing message does not name keygen"; cat "$OUT.nokey"; exit 1; }
rm -rf "$NOKEY_CFG"
ok "no key anywhere is refused, and says how to make one"
# The accepted case.
start_daemon "$SOCK4" "$OUT.d4.d" "--quic daemon never bound its session socket" --shell /bin/sh \
--quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 3000
D4PID=$DPID
# The UDP port is actually held. Asked through the lib's udp_local_bound
# rather than ss or lsof: the kernel's own table is always there and needs
# no privileges, and the spelling of the question is the oracle's business.
# 127.0.0.1 is 0100007F in the little-endian hex that table uses.
QHEX=$(printf '0100007F:%04X' "$QPORT")
udp_local_bound "$QHEX" || {
echo "e2e FAIL: no UDP socket bound at 127.0.0.1:$QPORT ($QHEX)"
udp_table | grep -i "0100007F" || true
exit 1
}
# A second daemon must NOT be able to take a share of that port. UDP with
# SO_REUSEADDR would let it bind alongside the first and the kernel would
# hand each datagram to one of them — two sessions splitting one port, with
# no error anywhere. This is the QUIC edition of the stale-socket story, and
# it needs two processes to test, which is why it lives here.
set +e
# Same reasoning as refuse(): if the second daemon ever succeeds it runs
# until killed, so the failure has to be a timeout rather than a hang.
timeout 10 "$MUX" d start --sock "$SOCK4.second" --shell /bin/sh \
--quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 15000 > "$OUT.q" 2>&1
RC=$?
set -e
[ "$RC" -eq 1 ] || {
echo "e2e FAIL: a second daemon took udp $QPORT (exit $RC, want 1; 124 means it bound and ran)"
cat "$OUT.q"; exit 1;
}
grep -q "already listening" "$OUT.q" || {
echo "e2e FAIL: second daemon refused, but not with the already-listening message:"
cat "$OUT.q"; exit 1;
}
[ ! -e "$SOCK4.second" ] || {
echo "e2e FAIL: refused second daemon left $SOCK4.second behind"; exit 1;
}
# ...and the daemon is still an ordinary daemon: the session runs and the
# unix-socket path is unaffected by the listener sharing its poll loop.
pipe_mux "$OUT.q" "" "$MUX" --sock "$SOCK4"
pipe_send 'printf "quic-%%s\\n" flags-ok\n'
await_out "$OUT.q" "quic-flags-ok" "quic-flags-ok never reached the client"
pipe_detach
grep -q "quic-flags-ok" "$OUT.q" || {
echo "e2e FAIL: --quic daemon did not serve an ordinary socket client"; cat "$OUT.q"; exit 1;
}
"$MUX" d dump --sock "$SOCK4" | grep -q "quic-flags-ok" || {
echo "e2e FAIL: --quic daemon's grid missing output"; exit 1;
}
kill -0 "$D4PID" || { echo "e2e FAIL: --quic daemon died"; exit 1; }
assert_converged "$OUT.q" "$SOCK4" "quic daemon serves sockets"
# --- M8 Task 3: the client speaks quic://. Same daemon, new transport.
# 1. Attach, run something, detach. The whole point of the milestone in one
# scenario: the wire protocol did not change, so this must behave exactly
# as the socket client does.
pipe_mux "$OUT.qc" "$OUT.qc.err" timeout 30 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 15000
pipe_send 'printf "quic-%%s\\n" attach-ok\n'
await_out "$OUT.qc" "quic-attach-ok" "quic-attach-ok never reached the client"
pipe_detach "quic:// client"
grep -q "quic-attach-ok" "$OUT.qc" || {
echo "e2e FAIL: quic:// client render missing output"
cat "$OUT.qc" "$OUT.qc.err" 2>/dev/null; exit 1;
}
"$MUX" d dump --sock "$SOCK4" | grep -q "quic-attach-ok" || {
echo "e2e FAIL: daemon grid missing the quic:// client's output"; exit 1;
}
assert_converged "$OUT.qc" "$SOCK4" "quic attach"
# 1b. Reattach. A detach leaves the session running, so coming back must
# find it — and must be served as a fresh attach (its own snapshot),
# because a client that detached deliberately holds nothing to resume
# from. The counter is what tells that apart from a delta.
SNAPS_RA=$("$MUX" d stats --sock "$SOCK4" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p')
pipe_mux "$OUT.qc" "$OUT.qc.err" timeout 30 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 15000
pipe_send 'printf "quic-%%s\\n" reattached\n'
await_out "$OUT.qc" "quic-reattached" "quic-reattached never reached the client"
pipe_detach "quic:// reattach"
# The earlier marker is still on the grid this client was handed, which is
# the session having survived the detach rather than a new shell.
grep -q "quic-attach-ok" "$OUT.qc" || {
echo "e2e FAIL: quic:// reattach did not land in the existing session"
cat "$OUT.qc" "$OUT.qc.err" 2>/dev/null; exit 1;
}
grep -q "quic-reattached" "$OUT.qc" || {
echo "e2e FAIL: quic:// reattach could not run a command"
cat "$OUT.qc" "$OUT.qc.err" 2>/dev/null; exit 1;
}
SNAPS_RA2=$("$MUX" d stats --sock "$SOCK4" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p')
[ "$((SNAPS_RA2 - SNAPS_RA))" -eq 1 ] || {
echo "e2e FAIL: reattach served $((SNAPS_RA2 - SNAPS_RA)) snapshots (want exactly 1)"
exit 1;
}
# ...and the slot it held is free again now that it has gone.
CLIENTS=$("$MUX" d stats --sock "$SOCK4" | sed -n 's/.*clients=\([0-9]*\).*/\1/p')
[ "$CLIENTS" = "0" ] || {
echo "e2e FAIL: $CLIENTS client slots still held after a clean detach (want 0)"; exit 1;
}
assert_converged "$OUT.qc" "$SOCK4" "quic reattach"
# 2. The key is checked, and a wrong one is refused loudly rather than
# retried forever. Nothing was ever established, so the reconnect loop
# must not engage — that is the never-established gate, over QUIC.
# No convergence: a refused client painted no grid.
head -c 32 /dev/urandom > "$QKEY.wrong"
chmod 600 "$QKEY.wrong"
set +e
: | timeout 30 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY.wrong" \
--quic-idle-ms 1500 > "$OUT.qc" 2>&1
RC=$?
set -e
[ "$RC" -eq 1 ] || {
echo "e2e FAIL: wrong-key quic client exited $RC (want 1; 124 means it retried)"
cat "$OUT.qc"; exit 1;
}
grep -q "did not answer" "$OUT.qc" || {
echo "e2e FAIL: wrong-key quic client said nothing useful:"; cat "$OUT.qc"; exit 1;
}
# 3. The abort key works DURING a handshake, not just after it. waitReady
# runs inside Transport.open, after drainStdinForQuit has returned, so
# watching only the socket left nothing looking for Ctrl-\ for as long as
# the handshake bound allows — and during a reconnect the terminal is in
# raw mode, where Ctrl-\ is the only way out.
#
# The target is the LIVE daemon dialled with the wrong key, and it has
# to be something silent like that: an unreachable port used to hold the
# handshake open for the whole bound, but since M15 a refused port is
# answered by an ICMP unreachable that kills the dial in ~2ms — far too
# fast to fit an abort inside, and this scenario would then be timing a
# failure rather than an abort. A listener that cannot authenticate us
# never answers (mutual PSK, same mechanism as case 2 above), so the
# dial runs its full budget and the window is real.
#
# No --quic-idle-ms: since M15 the dial is bounded by the client's
# 2000ms attach budget, which no flag on this command line moves, and
# spelling an idle timeout here would suggest otherwise.
# No convergence: the client aborts before it ever attaches, so it paints nothing.
# A fifo rather than a pipeline, so what is timed is the CLIENT's exit and
# not how long the writer happened to hang around afterwards.
QFIFO="${TMPDIR:-/tmp}/mux-e2e-abort-fifo-$$"
defer_rm "$QFIFO"
mkfifo "$QFIFO"
( sleep 0.3; printf '\034'; sleep 20 ) > "$QFIFO" &
QWPID=$!
defer_kill "$QWPID"
QT0=$(now_ms)
set +e
timeout 30 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY.wrong" \
< "$QFIFO" > "$OUT.qa" 2>&1
RC=$?
set -e
QT1=$(now_ms)
softkill "$QWPID" || true
rm -f "$QFIFO"
QMS=$(( QT1 - QT0 ))
[ "$RC" -eq 0 ] || {
echo "e2e FAIL: aborted quic handshake exited $RC (want 0; 124 means Ctrl-\ went unheard)"
cat "$OUT.qa"; exit 1;
}
# The abort lands 300ms in and the budget ends at 2000ms, so this bound is
# what tells "answered the user" from "ran the budget out and reported a
# failure that happened to exit late" — the exit code catches the second
# too, but only this catches an abort that was merely slow. Measured at
# ~350ms; 1500 leaves room for a loaded machine and still clears 2000.
[ "$QMS" -lt 1500 ] || {
echo "e2e FAIL: abort during handshake took ${QMS}ms (want well inside the 2000ms budget)"
exit 1;
}
# 4. Reconnect over QUIC, with the resume kind asserted rather than assumed.
# The tear is a SIGSTOP held past the client's idle timeout: the daemon
# stops answering, the client declares the transport dead and reconnects.
# Deterministic, needs no privileges, and unlike kill -9 it leaves the
# session alive so the resume can be a DELTA rather than a fresh snapshot.
SNAPS_BEFORE=$("$MUX" d stats --sock "$SOCK4" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p')
[ -n "$SNAPS_BEFORE" ] || { echo "e2e FAIL: could not read snapshots before the tear"; exit 1; }
pipe_mux "$OUT.qr" "$OUT.qr.err" timeout 40 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" \
--quic-idle-ms 1500
pipe_send 'printf "quic-%%s\\n" pre-tear\n'
await_out "$OUT.qr" "quic-pre-tear" "quic reconnect client never got its pre-tear marker"
# Twice the 1500ms idle timeout: long enough that the client cannot mistake
# it for a slow moment, short enough not to pad the suite.
kill -STOP "$D4PID"
sleep 3
kill -CONT "$D4PID"
# The session is alive on the far side of the tear only if a NEW round trip
# completes: the pre-tear marker is already in the capture and proves
# nothing about the reconnect.
pipe_send 'printf "quic-%%s\\n" post-tear\n'
await_out "$OUT.qr" "quic-post-tear" "quic client lost its session across the tear"
pipe_detach "quic client"
# The counter is the only witness to HOW the resume was served: a snapshot
# renders identically to a delta, so markers cannot tell them apart. One
# more snapshot is the fresh attach at the start of this scenario; a second
# would mean the reconnect was resynced from scratch instead of resumed.
SNAPS_AFTER=$("$MUX" d stats --sock "$SOCK4" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p')
[ -n "$SNAPS_AFTER" ] || { echo "e2e FAIL: could not read snapshots after the tear"; exit 1; }
[ "$((SNAPS_AFTER - SNAPS_BEFORE))" -eq 1 ] || {
echo "e2e FAIL: quic reconnect served $((SNAPS_AFTER - SNAPS_BEFORE)) snapshots (want 1:"
echo " the attach only; the resume itself must be delta-served)"
exit 1;
}
assert_converged "$OUT.qr" "$SOCK4" "quic delta resume"
# 5. The daemon is killed outright and started again on the SAME paths — unix
# socket and UDP port both. Two things are under test. The client must
# resume into a session that no longer exists, which can only be a
# snapshot under a new epoch (the stale-seq fence): a delta here would
# mean the daemon honoured a seq belonging to content that is gone. And
# the restarted daemon must be able to rebind the UDP port at all — there
# is no SO_REUSEADDR any more, so anything lingering from the killed
# process would show up as a bind failure rather than as silent sharing.
pipe_mux "$OUT.qk" "$OUT.qk.err" timeout 40 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" \
--quic-idle-ms 1500
pipe_send 'printf "quic-%%s\\n" pre-restart\n'
wait_for "$OUT.qk" "quic-pre-restart" 20 || {
echo "e2e FAIL: quic restart client never got its pre-restart marker"
cat "$OUT.qk" "$OUT.qk.err" 2>/dev/null; exit 1;
}
PAINTS_BEFORE=$(repaints "$OUT.qk")
hardkill "$D4PID"
D4PID=""
sleep 0.5
start_daemon "$SOCK4" "$OUT.q" "restarted --quic daemon never rebound its session socket" --shell /bin/sh \
--quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 15000
D4PID=$DPID
# Nearly vacuous, and kept only because a MISSING file would still be worth
# saying out loud: kill -9 leaves the old socket file behind, so this
# passes on the dead daemon's leavings. What proves the unix socket serves
# again is further down, where `mux d dump` and `mux d stats` answer on it;
# what proves the daemon serves SESSIONS again is the post-restart marker
# the resumed client gets back.
# The UDP port really came back, and to THIS daemon. Without SO_REUSEADDR a
# bind that collided would have failed loudly instead.
#
# Polled rather than sampled once, and matched on the LOCAL address only
# ($2), because both halves of the old one-shot `grep " $QHEX "` were
# accidents. The socket wait above cannot gate it: kill -9 leaves the
# socket FILE behind, so that loop exits before the new daemon has done
# anything. And a bare grep also matched the reconnecting CLIENT's socket,
# whose rem_address is this port — which is what made an unbound instant
# read look bound. That client no longer holds a socket at this moment:
# since M15 a refused dial dies in ~1 RTT and the socket goes with it, so
# the gap between reconnect attempts is now genuinely empty. Both fixed
# here rather than one, since either alone still passes on an accident.
#
# One expression, used by the wait and the verdict: two spellings of the
# same match are two matches waiting to drift apart. It is the lib's
# udp_local_bound now, so the local-address-only rule this block bought is
# also what the M8 bind check above asks, and neither can drift again.
i=0
# 50 x 0.1s, the same 5s every daemon wait in this file allows — and scaled
# by TIME_SCALE like every other budget, because under test/coverage.sh the
# traced daemon takes longer to reach its bind and a bare 5s reads the delay
# as "it never rebound".
while ! udp_local_bound "$QHEX" && [ "$i" -lt $(( 50 * TIME_SCALE )) ]; do
sleep 0.1; i=$((i+1))
done
udp_local_bound "$QHEX" || {
echo "e2e FAIL: restarted daemon did not rebind udp 127.0.0.1:$QPORT ($QHEX) in $(( 5 * TIME_SCALE ))s"
echo " daemon pid $D4PID: $(ps -o stat=,comm= -p "$D4PID" 2>/dev/null || echo gone)"
echo " kernel UDP table lines mentioning the port:"
udp_table | grep -i "$QHEX" || echo " (none)"
cat "$OUT.q"; exit 1;
}
kill -0 "$D4PID" || { echo "e2e FAIL: restarted --quic daemon died"; cat "$OUT.q"; exit 1; }
await_repaint "$OUT.qk" "$PAINTS_BEFORE" "quic client never resumed into the restarted daemon"
pipe_send 'printf "quic-%%s\\n" post-restart\n'
await_out "$OUT.qk" "quic-post-restart" "quic client did not resume into the restarted daemon"
pipe_detach "quic client"
# A new daemon means a new shell, so the old marker cannot be in its grid.
# If it were, we would be looking at a client that resumed off a seq
# belonging to a session that no longer exists.
if "$MUX" d dump --sock "$SOCK4" | grep -q "quic-pre-restart"; then
echo "e2e FAIL: restarted daemon's grid still holds the pre-restart marker"
exit 1
fi
# And the counter, because markers are blind to how a resume was served: the
# reconnecting client is this daemon's only client, so a snapshot in its
# stats is proof it was resynced from scratch rather than handed a delta off
# a stale seq.
SNAPS_NQ=$("$MUX" d stats --sock "$SOCK4" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p')
[ -n "$SNAPS_NQ" ] || { echo "e2e FAIL: could not read the restarted daemon's counters"; exit 1; }
[ "$SNAPS_NQ" -ge 1 ] || {
echo "e2e FAIL: restarted daemon served no snapshot ($SNAPS_NQ); the stale seq was honoured"
exit 1;
}
assert_converged "$OUT.qk" "$SOCK4" "quic epoch resync"
softkill "$D4PID" || true
D4PID=""
# --- M10: the daemon's own key resolution, over a real handshake.
#
# 1. MUX_KEY_FILE with no --key anywhere. The daemon's env support had no
# automated coverage at all: envKey() reads the real environment, so
# only a daemon started with it set can prove the path works.
env MUX_KEY_FILE="$QKEY" "$MUX" d start --sock "$SOCK9" --shell /bin/sh \
--quic "127.0.0.1:$QPORT2" --quic-idle-ms 3000 > "$OUT.d9.d" 2>&1 &
D9PID=$!
defer_kill "$D9PID"
wait_sock "$SOCK9" "$OUT.d9.d" "MUX_KEY_FILE daemon never bound its session socket"
pipe_mux "$OUT.env1" "$OUT.env1.err" timeout 30 "$MUX" "quic://127.0.0.1:$QPORT2" --key "$QKEY" --quic-idle-ms 15000
pipe_send 'printf "envkey-%%s\\n" ok\n'
await_out "$OUT.env1" "envkey-ok" "envkey-ok never reached the client"
pipe_detach "quic attach to a MUX_KEY_FILE daemon"
grep -q "envkey-ok" "$OUT.env1" || {
echo "e2e FAIL: MUX_KEY_FILE daemon served no session"
cat "$OUT.env1" "$OUT.env1.err" 2>/dev/null; exit 1; }
assert_converged "$OUT.env1" "$SOCK9" "env key"
softkill "$D9PID" || true
D9PID=""
# 2. --key AND MUX_KEY_FILE, naming different keys. The env names one the
# client does not have, so an attach that succeeds proves the FLAG won.
# This is the shape that catches the two being swapped at the call site
# in run() — pickKey's unit test pins the ordering inside the function
# and is blind to the order they are handed to it.
env MUX_KEY_FILE="$QKEY.wrong" "$MUX" d start --sock "$SOCK10" --shell /bin/sh \
--quic "127.0.0.1:$QPORT3" --key "$QKEY" --quic-idle-ms 3000 > "$OUT.d10.d" 2>&1 &
D10PID=$!
defer_kill "$D10PID"
wait_sock "$SOCK10" "$OUT.d10.d" "--key-beats-env daemon never bound its session socket"
pipe_mux "$OUT.env2" "$OUT.env2.err" timeout 30 "$MUX" "quic://127.0.0.1:$QPORT3" --key "$QKEY" --quic-idle-ms 15000
pipe_send 'printf "flagwins-%%s\\n" ok\n'
await_out "$OUT.env2" "flagwins-ok" "flagwins-ok never reached the client"
pipe_detach "the --key attach (which must beat MUX_KEY_FILE)"
grep -q "flagwins-ok" "$OUT.env2" || {
echo "e2e FAIL: --key-beats-env daemon served no session"
cat "$OUT.env2" "$OUT.env2.err" 2>/dev/null; exit 1; }
assert_converged "$OUT.env2" "$SOCK10" "flag beats env"
softkill "$D10PID" || true
D10PID=""
ok "daemon honours MUX_KEY_FILE, and --key beats it"
rm_swept "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY" "$QKEY.bad" "$QKEY.wrong" \
"$OUT.qc.err" "$OUT.qr.err" "$OUT.qk.err"
# --- M10: mux d start -d — detached spawn, no-op rerun, race, pinned lines.
#
# Every scenario below asserts a MARKER through the session, never just $?.
# A client that fails to authenticate exits 0 by way of "aborted before
# attaching", so an exit code cannot distinguish a working session from a
# key or transport regression — only bytes coming back out of the shell can.
"$MUX" d start -d --sock "$SOCK8" 2> "$OUT.start"
grep -q '^mux d: starting' "$OUT.start" || {
echo "e2e FAIL: start printed no starting line"; cat "$OUT.start"; exit 1; }
grep -q '^up (' "$OUT.start" || {
echo "e2e FAIL: start printed no up line"; cat "$OUT.start"; exit 1; }
# Known gap, accepted: between the spawn above and this capture the daemon
# is running with no pid the trap can reach, so a failure in the two
# assertions in that window leaks it. Narrow, only on an already-failing
# run, and closing it would mean parsing the pid before asserting the lines
# that prove the pid is there.
SPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start")
defer_kill "$SPID"
[ -n "$SPID" ] || { echo "e2e FAIL: up line carries no pid"; exit 1; }
kill -0 "$SPID" || { echo "e2e FAIL: started daemon not alive"; exit 1; }
# `mux d start -d` is the OTHER production spawn, and it wears the name too.
# It shares `spawn.selfExe` with the client's auto-start (pinned the same
# way in 03_side), so this holds today by construction — which is exactly
# why it is asserted here: a later split of the two callers would leave
# `d start` free to hand execve the /proc link and name its daemon `exe`.
SCOMM=$(pid_comm "$SPID")
[ "$SCOMM" = "mux" ] || {
echo "e2e FAIL: the daemon mux d start -d brought up has comm '$SCOMM', want 'mux'"
exit 1; }
# Non-tty stderr: exactly two lines, no dots.
# -eq and not =, here and below: BSD wc pads its count with spaces.
[ "$(wc -l < "$OUT.start")" -eq 2 ] || {
echo "e2e FAIL: non-tty start not exactly two lines:"; cat "$OUT.start"; exit 1; }
# The daemon it started serves a session (marker in, marker in dump).
pipe_mux "$OUT.s8" "$OUT.s8.err" "$MUX" --sock "$SOCK8"
pipe_send 'printf "start-%%s\\n" works\n'
await_out "$OUT.s8" "start-works" "start-works never reached the client"
pipe_detach
"$MUX" d dump --sock "$SOCK8" | grep -q "start-works" || {
echo "e2e FAIL: auto-started daemon lost the marker"
cat "$OUT.s8" "$OUT.s8.err" 2>/dev/null; exit 1; }
assert_converged "$OUT.s8" "$SOCK8" "started daemon"
# The log the failure path names really is there, and is no more readable
# than the key is: it carries whatever the daemon says about its own start.
MUXLOG="$XDG_STATE_HOME/mux/muxd.log"
[ -f "$MUXLOG" ] || { echo "e2e FAIL: no daemon log at $MUXLOG"; exit 1; }
LPERMS=$(file_mode "$MUXLOG")
[ "$LPERMS" = "600" ] || { echo "e2e FAIL: daemon log perms $LPERMS, want 600"; exit 1; }
# Rerun: silent no-op beyond the already-running line, exit 0, same daemon.
"$MUX" d start -d --sock "$SOCK8" 2> "$OUT.start2"
grep -q "already running on $SOCK8 (stop it first with \`mux d stop --sock $SOCK8\`" "$OUT.start2" || {
echo "e2e FAIL: rerun did not say already running"; cat "$OUT.start2"; exit 1; }
"$MUX" d dump --sock "$SOCK8" | grep -q "start-works" || {
echo "e2e FAIL: rerun replaced the daemon (marker gone)"; exit 1; }
# APPENDED per spawn, never truncated. One xdg log serves every socket on
# the box, and $SOCK8's daemon is still writing to this file: a spawn on
# the SECOND path that zeroed it would delete a live daemon's crash out
# from under the operator who went looking for it. Seeded with a line the
# first daemon could have written, then a real spawn on a fresh path must
# leave that line where it is and grow the file.
SEED="a live daemon was writing here"
echo "$SEED" >> "$MUXLOG"
SEEDED=$(file_size "$MUXLOG")
"$MUX" d start -d --sock "$SOCK8T" 2> "$OUT.start"
TPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start")
defer_kill "$TPID"
[ -n "$TPID" ] || { echo "e2e FAIL: append spawn reported no pid"; exit 1; }
GREW=$(file_size "$MUXLOG")
[ "$GREW" -ge "$SEEDED" ] || {
echo "e2e FAIL: log shrank on spawn ($SEEDED -> $GREW) — a second daemon zeroed the first's"
exit 1; }
# The bytes themselves, not just the size: a mode that wrote over the front
# and happened to be longer would pass a size check alone.
grep -qxF "$SEED" "$MUXLOG" || {
echo "e2e FAIL: the spawn overwrote what was already in $MUXLOG"; exit 1; }
softkill "$TPID" || true
TPID=""
# --- the socket path deleted under a live daemon: a trail, and a way back --
#
# 2026-09-04, on a live box: a daemon's socket file vanished from the
# runtime dir. The daemon kept its sessions and kept listening on the
# unlinked inode, nothing could reach it by name, and the next `mux` to
# dial the path started a SECOND daemon on it. The day's log said nothing
# about any of it. Two fixes, both graded here against a real detached
# daemon and the real log file: the daemon writes one line per socket
# event, and it takes a deleted path back within a second — so the second
# daemon never gets its chance.
grep -q "mux d: socket $SOCK8: claimed (nothing there)" "$MUXLOG" || {
echo "e2e FAIL: the log has no claim line for $SOCK8"; tail -20 "$MUXLOG"; exit 1; }
grep -q "mux d: socket $SOCK8: bound dev=[0-9]* ino=[0-9]*" "$MUXLOG" || {
echo "e2e FAIL: the log has no bind line for $SOCK8"; tail -20 "$MUXLOG"; exit 1; }
# `|` as the delimiter: the socket path is full of `/`.
INO_BEFORE=$(sed -n "s|.*socket $SOCK8: bound dev=[0-9]* ino=\([0-9]*\).*|\1|p" "$MUXLOG" | tail -1)
[ -n "$INO_BEFORE" ] || { echo "e2e FAIL: could not read the bound inode off the log"; exit 1; }
rm "$SOCK8"
# The path is dead to dials for now — this is the state the box was in for
# hours — and the daemon is still there: the OS, not the daemon, says so.
kill -0 "$SPID" || { echo "e2e FAIL: the daemon died with its socket file"; exit 1; }
wait_until 30 "the daemon never took $SOCK8 back after its file was deleted" \
'"$MUX" d stats --sock "$SOCK8" > /dev/null 2>&1' \
'tail -20 "$MUXLOG"'
grep -q "mux d: socket $SOCK8: no longer names our listener (was dev=[0-9]* ino=$INO_BEFORE, now missing)" "$MUXLOG" || {
echo "e2e FAIL: the log does not record the loss of $SOCK8"; tail -20 "$MUXLOG"; exit 1; }
grep -q "mux d: socket $SOCK8: re-bound dev=[0-9]* ino=[0-9]*" "$MUXLOG" || {
echo "e2e FAIL: the log does not record the re-bind of $SOCK8"; tail -20 "$MUXLOG"; exit 1; }
# The same daemon, not a replacement: the marker typed into its shell
# before the deletion is still on its grid, and the pid never changed.
"$MUX" d dump --sock "$SOCK8" | grep -q "start-works" || {
echo "e2e FAIL: the re-bound path reaches a daemon without the marker"; exit 1; }
kill -0 "$SPID" || { echo "e2e FAIL: the daemon that re-bound is not pid $SPID"; exit 1; }
ok "a deleted socket path is logged and taken back within a tick (04b3019d, 145807a2)"
# Race: two concurrent starts, both exit 0, still one session (the marker
# survives — a second daemon on the path would have started a fresh shell).
softkill "$SPID" && wait_gone "$SOCK8"
SPID=""
"$MUX" d start -d --sock "$SOCK8" 2> "$OUT.ra" & RA=$!; defer_kill "$RA"
"$MUX" d start -d --sock "$SOCK8" 2> "$OUT.rb" & RB=$!; defer_kill "$RB"
set +e
wait "$RA"; RCA=$?
wait "$RB"; RCB=$?
set -e
{ [ "$RCA" = "0" ] && [ "$RCB" = "0" ]; } || {
echo "e2e FAIL: race: exits $RCA/$RCB"; cat "$OUT.ra" "$OUT.rb"; exit 1; }
pipe_mux "$OUT.race" /dev/null "$MUX" --sock "$SOCK8"
pipe_send 'printf "race-%%s\\n" one\n'
await_out "$OUT.race" "race-one" "race-one never reached the client"
pipe_detach
"$MUX" d dump --sock "$SOCK8" | grep -q "race-one" || {
echo "e2e FAIL: race: session unusable"; exit 1; }
SPID=$(cat "$OUT.ra" "$OUT.rb" | sed -n 's/.* pid=\([0-9]*\).*/\1/p' | while read -r p; do
kill -0 "$p" 2>/dev/null && echo "$p"; done | head -1)
defer_kill "$SPID"
# What actually makes two concurrent starts safe is that the LOSER exits
# instead of unlinking the winner's socket and binding over it. The race
# above cannot see that — both starts report success either way, and the
# marker comes back from whichever daemon owns the path — so it is pinned
# head-on: a second `mux d start` against a live socket must refuse.
set +e
timeout 10 "$MUX" d start --sock "$SOCK8" --shell /bin/sh > "$OUT.rb" 2>&1
RCS=$?
set -e
[ "$RCS" -eq 1 ] || {
echo "e2e FAIL: second mux d start on a live socket exited $RCS (want 1; 124 means it took the path)"
cat "$OUT.rb"; exit 1; }
grep -q "already running" "$OUT.rb" || {
echo "e2e FAIL: second mux d start refused, but not as already-running"; cat "$OUT.rb"; exit 1; }
# ...and the session the winner was serving is undisturbed.
"$MUX" d dump --sock "$SOCK8" | grep -q "race-one" || {
echo "e2e FAIL: the refused daemon disturbed the live session"; exit 1; }
# The race client's own capture, checked here rather than above: the refused
# `mux d start` between the two touches no session, so the grid has not moved.
assert_converged "$OUT.race" "$SOCK8" "start race"
softkill "$SPID" || true
SPID=""
ok "mux d start -d — spawn, no-op rerun, log append, race"
# --- `mux d endpoint --start` ensures the daemon ON THE SOCKET IT PROBES.
#
# The verb a cold `mux HOST` runs over ssh: it ensures a daemon and then
# announces that daemon's QUIC coordinates on the same stdout, which is what
# makes the attach one ssh login instead of three. What it forwards to the
# ensure is the whole of this scenario, and `--sock` is the only flag that
# may be: forwarding nothing starts the daemon on the runtime dir's DEFAULT
# socket, probes the named one, finds nothing and exits 1 — a daemon on a
# path nobody asked for, and no announce to say it happened.
#
# A runtime dir of this leg's own is what makes that default socket
# observable at all: on the suite's shared one it is indistinguishable from
# some other leg's daemon. Short, because a unix path is capped at 107
# bytes and this one carries a socket name.
ESRUN="${TMPDIR:-/tmp}/mux-e2e-esr-$$"
defer_rm "$ESRUN"
ESSTATE="${TMPDIR:-/tmp}/mux-e2e-ess-$$"
defer_rm "$ESSTATE"
mkdir -p "$ESRUN" "$ESSTATE"
ESOWN="$ESRUN/own.sock"
ESDEF="$ESRUN/muxd.sock"
defer_sock "$ESOWN" "$ESDEF"
# stdin from /dev/null: this verb becomes `mux d proxy` once it has
# announced, and a pump with nothing left to read ends by itself. The
# announce is the first bytes of that stdout either way, which is the
# contract the client depends on.
set +e
env XDG_RUNTIME_DIR="$ESRUN" XDG_STATE_HOME="$ESSTATE" SHELL=/bin/sh timeout 30 \
"$MUX" d endpoint --sock "$ESOWN" --start < /dev/null > "$OUT.esa" 2> "$OUT.esa.err"
ESRC=$?
set -e
[ "$ESRC" -eq 0 ] || {
echo "e2e FAIL: mux d endpoint --sock ... --start exited $ESRC (want 0)"
cat "$OUT.esa.err"; exit 1; }
# (a) The named socket is bound, and bound by a `mux` — asked of the OS by
# INODE, not by a name, which is what pid_holds_unix_sock does. A `mux`
# running anywhere would satisfy a name check while binding something else
# entirely; the kernel's listening inode for this path, found among that
# pid's own open fds, cannot.
ESPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.esa.err" | head -1)
defer_kill "$ESPID"
[ -n "$ESPID" ] || {
echo "e2e FAIL: endpoint --start printed no up-line pid"; cat "$OUT.esa.err"; exit 1; }
[ -S "$ESOWN" ] || {
echo "e2e FAIL: endpoint --start bound nothing at the socket it was given"
ls -la "$ESRUN"; exit 1; }
pid_holds_unix_sock "$ESPID" "$ESOWN" || {
echo "e2e FAIL: pid $ESPID does not hold the socket at $ESOWN — the daemon"
echo " that answered is not the daemon --start reported starting,"
echo " or the kernel lists no listening socket at that path at all"
exit 1; }
ESCOMM=$(pid_comm "$ESPID")
[ "$ESCOMM" = "mux" ] || {
echo "e2e FAIL: the socket at $ESOWN is served by comm '$ESCOMM', want 'mux'"; exit 1; }
# (b) ...and the announce came back on stdout, first line, in the grammar
# `handoff.parseAnnounce` reads. Either arm is a pass: this box may or may
# not have a key to announce, and "which" is another scenario's business.
ESLINE=$(head -1 "$OUT.esa")
printf '%s\n' "$ESLINE" | grep -qE '^endpoint (none|[1-9][0-9]* [0-9a-f]{64})$' || {
echo "e2e FAIL: endpoint --start announced '$ESLINE', which is not an announce"
cat -v "$OUT.esa" | head -3; exit 1; }
# (c) ...and NOTHING appeared on this runtime dir's default socket. This is
# the assertion the forwarded `--sock` exists for: without it the daemon
# lands here instead, and every check above fails with it.
[ ! -e "$ESDEF" ] || {
echo "e2e FAIL: endpoint --start bound the DEFAULT socket $ESDEF as well as"
echo " the one it was given — the ensure did not forward --sock"
ls -la "$ESRUN"; exit 1; }
ESSOCKS=$(ls -1 "$ESRUN"/*.sock 2>/dev/null | wc -l)
[ "$ESSOCKS" -eq 1 ] || {
echo "e2e FAIL: endpoint --start left $ESSOCKS sockets under $ESRUN, want 1"
ls -la "$ESRUN"; exit 1; }
assert_stopped "$ESOWN" "$ESPID" "endpoint --start daemon" "$OUT.esstop"
ESPID=""
ok "mux d endpoint --start ensures a daemon on the socket it announces, and on no other"
# --- M10: a start whose daemon dies young REPORTS it. This is the first-run
# mistake the failure line exists for — `ssh HOST 'mux d start -d --quic 0.0.0.0'`
# before the key was ever scp'd — so it must be a message, not a panic. The
# child exits on the missing key, the poll loop reaps it, and polling
# continues to the deadline; a second waitpid there gets ECHILD, which the
# stdlib answers with `unreachable`, i.e. exit 134 and a stack trace.
# No convergence: the daemon under test is the one that died, so nothing to dump.
DEADCFG="${TMPDIR:-/tmp}/mux-e2e-deadchild-$$"
defer_rm "$DEADCFG"
set +e
env XDG_CONFIG_HOME="$DEADCFG" timeout 30 "$MUX" d start -d --sock "$SOCK8T" \
--quic "127.0.0.1:1" --key /nonexistent > "$OUT.dead" 2>&1
DRC=$?
set -e
[ "$DRC" -eq 1 ] || {
echo "e2e FAIL: start with a doomed child exited $DRC (want 1; 134 is the waitpid panic)"
cat "$OUT.dead"; exit 1; }
grep -q "did not answer" "$OUT.dead" || {
echo "e2e FAIL: doomed start printed no deadline line"; cat "$OUT.dead"; exit 1; }
grep -q "log: .*muxd\.log" "$OUT.dead" || {
echo "e2e FAIL: deadline line does not name the log"; cat "$OUT.dead"; exit 1; }
# The failure says "daemon did not answer", never a mode word:
# the prefix is already the program's name, and the same line serves the
# mux-side caller when auto-start lands.
grep -q "daemon did not answer" "$OUT.dead" || {
echo "e2e FAIL: deadline line does not read 'daemon did not answer'"
cat "$OUT.dead"; exit 1; }
# Non-tty: exactly two lines, same as the success path. The newline that
# terminates the dot line is tty-only, so nothing blank creeps in here.
[ "$(wc -l < "$OUT.dead")" -eq 2 ] || {
echo "e2e FAIL: non-tty failure not exactly two lines:"; cat "$OUT.dead"; exit 1; }
rm -rf "$DEADCFG"
ok "a start whose daemon dies young says so, with the log path"
# --- M10: the goal commands, minus ssh: keygen'd default key on both ends,
# explicit loopback port (4433 on the suite machine is somebody's daemon).
# No --key on either side — the key is the one `mux d keygen` wrote at the
# hermetic default path near the top of this suite.
"$MUX" d start -d --sock "$SOCK11" --quic "127.0.0.1:$QPORT4" 2> "$OUT.goal"
GPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.goal")
defer_kill "$GPID"
[ -n "$GPID" ] || { echo "e2e FAIL: goal start reported no pid"; cat "$OUT.goal"; exit 1; }
pipe_mux "$OUT.g9" "$OUT.g9.err" timeout 30 "$MUX" "quic://127.0.0.1:$QPORT4"
pipe_send 'printf "goal-%%s\\n" quic\n'
await_out "$OUT.g9" "goal-quic" "goal-quic never reached the client"
pipe_detach
"$MUX" d dump --sock "$SOCK11" | grep -q "goal-quic" || {
echo "e2e FAIL: no-key-flag QUIC attach did not reach the session"
cat "$OUT.g9" "$OUT.g9.err" 2>/dev/null; exit 1; }
assert_converged "$OUT.g9" "$SOCK11" "goal commands"
softkill "$GPID" || true
GPID=""
ok "keygen + start --quic + mux quic:// with no --key anywhere"
# --- one stalled peer cannot stop the daemon --------------------------------
# A connection that writes the first byte of a frame and then holds still.
# Before this was fixed the daemon blocked in read() on that fd and every
# other connection — stats included — waited behind it (2026-08-27, found
# on a live wall). The witness is `mux d stats` answering within its timeout
# WHILE the half frame is still outstanding; the peer is only released after.
SOCKHF="${TMPDIR:-/tmp}/muxd-e2e-halfframe-$$.sock"
start_daemon "$SOCKHF" "$OUT.hf.d" "half-frame daemon never bound" --shell /bin/sh
DHFPID=$DPID
HFMARK="$OUT.hf.sent"; defer_rm "$HFMARK"
# The daemon's OWN fd table, sampled before the peer exists: a unix
# connect() succeeds into the listen backlog, so the peer's marker below
# says the byte was written and NOT that the daemon has it. What the leg
# needs is an ACCEPTED half frame — an unaccepted one parks nothing — and
# the accept is one more fd appearing in the daemon's table. Ask the OS
# about the OS.
HFDPID=$(real_pid "$DHFPID")
HFFD0=$(pid_fd_count "$HFDPID")
[ "$HFFD0" -gt 0 ] || {
echo "e2e FAIL: half-frame: the OS will not list pid $HFDPID's open fds,"
echo " so the accept below could not be witnessed"; exit 1; }
python3 - "$SOCKHF" "$HFMARK" <<'EOF' &
import socket, sys, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(sys.argv[1])
s.sendall(b'\x06') # stats_req's kind byte, and nothing after it
open(sys.argv[2], 'w').write('sent\n') # written; the accept is asserted below
time.sleep(60)
EOF
HFPID=$!
defer_kill "$HFPID"
# The marker, not a sleep: the leg's whole precondition is that the half
# frame is ALREADY outstanding when stats connects. A timed settle that
# loses the race on a slow box asks a pre-fix daemon a question it can
# answer, and the leg goes green having tested nothing.
wait_for "$HFMARK" sent 5 || {
echo "e2e FAIL: half-frame: peer never connected"; tail -30 "$OUT.hf.d"; exit 1; }
# ...and then the accept, which is the half of the precondition the marker
# cannot speak for. Nothing else connects in this window, so one more fd is
# this peer's.
_hfi=0
while [ "$(pid_fd_count "$HFDPID")" -le "$HFFD0" ]; do
_hfi=$((_hfi + 1))
[ "$_hfi" -lt $(( 100 * TIME_SCALE )) ] || {
echo "e2e FAIL: half-frame: the daemon never accepted the peer, so the"
echo " stats below would not have been asked behind one"
tail -30 "$OUT.hf.d"; exit 1; }
sleep 0.05
done
set +e
timeout $((5 * TIME_SCALE)) "$MUX" d stats --sock "$SOCKHF" > "$OUT.hf.st" 2>&1
HFRC=$?
set -e
[ "$HFRC" -eq 0 ] || {
echo "e2e FAIL: half-frame: stats exited $HFRC with a one-byte peer outstanding"
echo " (124 = the daemon is blocked in read() behind that peer)"
cat "$OUT.hf.st"; tail -30 "$OUT.hf.d"; exit 1; }
grep -q 'sessions=' "$OUT.hf.st" || {
echo "e2e FAIL: half-frame: stats answered but not with a stats line:"; cat "$OUT.hf.st"; exit 1; }
softkill "$HFPID" || true
HFPID=""
softkill "$DHFPID" || true
DHFPID=""
ok "one peer holding half a frame does not stop mux d stats"