a6ad1dca
test: the upgrade keeps the shell, its pid, its title, its marks, its agent
a73x 2026-08-26 15:17
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -77,6 +77,19 @@ for _agtool in ssh-agent ssh-add ssh-keygen; do | |||
| 77 | exit 1; } | 77 | exit 1; } |
| 78 | done | 78 | done |
| 79 | 79 | ||
| 80 | # And the fifth, required for the fourth's reason: the upgrade legs assert | ||
| 81 | # that `muxa` still answers under the `marks` mechanism after the exec, and | ||
| 82 | # marks exist only in a shell mux injects OSC 133 into (shellint's | ||
| 83 | # `bash_init`). Run against /bin/sh those legs would still go green having | ||
| 84 | # proved nothing about the one piece of session state the manifest carries | ||
| 85 | # that nothing else can rebuild. | ||
| 86 | [ -x /bin/bash ] || { | ||
| 87 | echo "e2e FAIL: this suite needs /bin/bash (the upgrade legs check that OSC 133" | ||
| 88 | echo " marks survive the exec, and mux injects them into bash and zsh" | ||
| 89 | echo " only); install bash, or lose the only check that an upgraded" | ||
| 90 | echo " daemon can still report a command's real exit code" | ||
| 91 | exit 1; } | ||
| 92 | |||
| 80 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" | 93 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" |
| 81 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" | 94 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" |
| 82 | # M10: hermetic XDG homes. Key-default scenarios must see OUR key or none, | 95 | # M10: hermetic XDG homes. Key-default scenarios must see OUR key or none, |
| @@ -429,6 +442,40 @@ D67PID="" | |||
| 429 | W5PID="" | 442 | W5PID="" |
| 430 | D68PID="" | 443 | D68PID="" |
| 431 | W6PID="" | 444 | W6PID="" |
| 445 | # `muxd upgrade` — four daemons, because each one is a different daemon | ||
| 446 | # LIFECYCLE and no two can share a process: the same-binary leg's daemon | ||
| 447 | # ends up running a second image, the rollback leg's is born with the abort | ||
| 448 | # armed in its environment, the agent leg's holds an `-A` client, and the | ||
| 449 | # QUIC leg's owns a UDP socket. They run one after another, so the last of | ||
| 450 | # them is the only leg here that needs a port. | ||
| 451 | SOCK69="${TMPDIR:-/tmp}/muxd-e2e-upgrade-$$.sock" | ||
| 452 | SOCK70="${TMPDIR:-/tmp}/muxd-e2e-uproll-$$.sock" | ||
| 453 | SOCK71="${TMPDIR:-/tmp}/muxd-e2e-upagent-$$.sock" | ||
| 454 | SOCK72="${TMPDIR:-/tmp}/muxd-e2e-upquic-$$.sock" | ||
| 455 | # The next 4000-wide band DOWN from the hydrate leg's 6000: every base from | ||
| 456 | # 11000 up is taken, and the 61000+ tail above the ephemeral range is spoken | ||
| 457 | # for by the two browser ports. A collision here reads as this leg's daemon | ||
| 458 | # failing to bind, the same verdict every other band gives. | ||
| 459 | UPQPORT=$(( 2000 + ($$ % 4000) )) | ||
| 460 | UPKEY="${TMPDIR:-/tmp}/mux-e2e-upkey-$$" | ||
| 461 | # A HOME of its own for the session shell. The title assertion below is | ||
| 462 | # about a title the SESSION set, and a distribution's rc file repaints the | ||
| 463 | # window title from PROMPT_COMMAND on every prompt (shellint.bash_init says | ||
| 464 | # so of Arch's) — read out of the developer's own dotfiles, this leg would | ||
| 465 | # assert on whatever their prompt happens to spell. | ||
| 466 | UPHOME="${TMPDIR:-/tmp}/mux-e2e-uphome-$$" | ||
| 467 | # The candidate binary the refusal leg offers: a copy, so its exec bit can | ||
| 468 | # be taken away without touching the one every other scenario runs. | ||
| 469 | UPBIN="${TMPDIR:-/tmp}/mux-e2e-upcand-$$" | ||
| 470 | UPAGENT="${TMPDIR:-/tmp}/mux-e2e-upagent-$$.sock" | ||
| 471 | UPAGKEY="${TMPDIR:-/tmp}/mux-e2e-upagkey-$$" | ||
| 472 | D69PID="" | ||
| 473 | D70PID="" | ||
| 474 | D71PID="" | ||
| 475 | D72PID="" | ||
| 476 | UPAGPID="" | ||
| 477 | UPPCPID="" | ||
| 478 | UPCPID="" | ||
| 432 | D54PID="" | 479 | D54PID="" |
| 433 | D55PID="" | 480 | D55PID="" |
| 434 | D56PID="" | 481 | D56PID="" |
| @@ -1388,6 +1435,17 @@ cleanup() { | |||
| 1388 | [ -n "${D64PID:-}" ] && kill "$D64PID" 2>/dev/null || true | 1435 | [ -n "${D64PID:-}" ] && kill "$D64PID" 2>/dev/null || true |
| 1389 | [ -n "${D65PID:-}" ] && kill "$D65PID" 2>/dev/null || true | 1436 | [ -n "${D65PID:-}" ] && kill "$D65PID" 2>/dev/null || true |
| 1390 | [ -n "${D66PID:-}" ] && kill "$D66PID" 2>/dev/null || true | 1437 | [ -n "${D66PID:-}" ] && kill "$D66PID" 2>/dev/null || true |
| 1438 | # The upgrade legs' daemons. CONT first for AGENT48PID's reason turned | ||
| 1439 | # on a daemon: the non-executable-candidate refusal holds this one under | ||
| 1440 | # SIGSTOP while it takes the exec bit off the candidate, and a run that | ||
| 1441 | # dies inside that window leaves a process no SIGTERM can reach. | ||
| 1442 | [ -n "${D69PID:-}" ] && kill -CONT "$D69PID" 2>/dev/null || true | ||
| 1443 | [ -n "${D69PID:-}" ] && kill "$D69PID" 2>/dev/null || true | ||
| 1444 | [ -n "${D70PID:-}" ] && kill "$D70PID" 2>/dev/null || true | ||
| 1445 | [ -n "${D71PID:-}" ] && kill "$D71PID" 2>/dev/null || true | ||
| 1446 | [ -n "${D72PID:-}" ] && kill "$D72PID" 2>/dev/null || true | ||
| 1447 | [ -n "${UPPCPID:-}" ] && kill "$UPPCPID" 2>/dev/null || true | ||
| 1448 | [ -n "${UPCPID:-}" ] && kill "$UPCPID" 2>/dev/null || true | ||
| 1391 | # The ssh-agents the forwarding legs start. Not mux processes and so not | 1449 | # The ssh-agents the forwarding legs start. Not mux processes and so not |
| 1392 | # the leak sweep's business, but they are daemons this file forked: left | 1450 | # the leak sweep's business, but they are daemons this file forked: left |
| 1393 | # alive they outlive the suite holding a private key, which is the one | 1451 | # alive they outlive the suite holding a private key, which is the one |
| @@ -1398,6 +1456,7 @@ cleanup() { | |||
| 1398 | [ -n "$AGENT48PID" ] && kill "$AGENT48PID" 2>/dev/null || true | 1456 | [ -n "$AGENT48PID" ] && kill "$AGENT48PID" 2>/dev/null || true |
| 1399 | [ -n "$AGENT49APID" ] && kill "$AGENT49APID" 2>/dev/null || true | 1457 | [ -n "$AGENT49APID" ] && kill "$AGENT49APID" 2>/dev/null || true |
| 1400 | [ -n "$AGENT49BPID" ] && kill "$AGENT49BPID" 2>/dev/null || true | 1458 | [ -n "$AGENT49BPID" ] && kill "$AGENT49BPID" 2>/dev/null || true |
| 1459 | [ -n "${UPAGPID:-}" ] && kill "$UPAGPID" 2>/dev/null || true | ||
| 1401 | # The stops still precede the socket rm below, like SOCK14-17 above: | 1460 | # The stops still precede the socket rm below, like SOCK14-17 above: |
| 1402 | # unlinking a socket first would leave a live daemon nothing could reach | 1461 | # unlinking a socket first would leave a live daemon nothing could reach |
| 1403 | # by path. | 1462 | # by path. |
| @@ -1444,6 +1503,10 @@ cleanup() { | |||
| 1444 | [ -S "$SOCK66" ] && "$MUXD" stop --sock "$SOCK66" 2>/dev/null || true | 1503 | [ -S "$SOCK66" ] && "$MUXD" stop --sock "$SOCK66" 2>/dev/null || true |
| 1445 | [ -S "$SOCK67" ] && "$MUXD" stop --sock "$SOCK67" 2>/dev/null || true | 1504 | [ -S "$SOCK67" ] && "$MUXD" stop --sock "$SOCK67" 2>/dev/null || true |
| 1446 | [ -S "$SOCK68" ] && "$MUXD" stop --sock "$SOCK68" 2>/dev/null || true | 1505 | [ -S "$SOCK68" ] && "$MUXD" stop --sock "$SOCK68" 2>/dev/null || true |
| 1506 | [ -S "$SOCK69" ] && "$MUXD" stop --sock "$SOCK69" 2>/dev/null || true | ||
| 1507 | [ -S "$SOCK70" ] && "$MUXD" stop --sock "$SOCK70" 2>/dev/null || true | ||
| 1508 | [ -S "$SOCK71" ] && "$MUXD" stop --sock "$SOCK71" 2>/dev/null || true | ||
| 1509 | [ -S "$SOCK72" ] && "$MUXD" stop --sock "$SOCK72" 2>/dev/null || true | ||
| 1447 | 1510 | ||
| 1448 | # ---- the leak sweep (hygiene kit, 6a) ---- | 1511 | # ---- the leak sweep (hygiene kit, 6a) ---- |
| 1449 | # Here rather than at the bottom of the file, which `set -e` reaches only | 1512 | # Here rather than at the bottom of the file, which `set -e` reaches only |
| @@ -1461,7 +1524,8 @@ cleanup() { | |||
| 1461 | "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" "$D43PID" "$D54PID" \ | 1524 | "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" "$D43PID" "$D54PID" \ |
| 1462 | "$D55PID" "$D56PID" "$D57PID" "$D58PID" "$D59PID" \ | 1525 | "$D55PID" "$D56PID" "$D57PID" "$D58PID" "$D59PID" \ |
| 1463 | "$D60PID" "$D61PID" "$D62PID" "$D63PID" "$D64PID" "$D65PID" \ | 1526 | "$D60PID" "$D61PID" "$D62PID" "$D63PID" "$D64PID" "$D65PID" \ |
| 1464 | "$D66PID" "$D67PID" "$D68PID" | 1527 | "$D66PID" "$D67PID" "$D68PID" "$D69PID" "$D70PID" \ |
| 1528 | "$D71PID" "$D72PID" | ||
| 1465 | _leak=0 | 1529 | _leak=0 |
| 1466 | leak_sweep "$_rc" || _leak=1 | 1530 | leak_sweep "$_rc" || _leak=1 |
| 1467 | 1531 | ||
| @@ -1569,7 +1633,21 @@ cleanup() { | |||
| 1569 | "$OUT.wgghost" "$OUT.wgstop" "$OUT.wgsta0" "$OUT.wgexit" \ | 1633 | "$OUT.wgghost" "$OUT.wgstop" "$OUT.wgsta0" "$OUT.wgexit" \ |
| 1570 | "$OUT.wgws3" "$OUT.wgws3.err" "$SOCK67" "$WGKEY" \ | 1634 | "$OUT.wgws3" "$OUT.wgws3.err" "$SOCK67" "$WGKEY" \ |
| 1571 | "$OUT.sp.d" "$OUT.sph" "$OUT.spws" "$OUT.spws.err" \ | 1635 | "$OUT.sp.d" "$OUT.sph" "$OUT.spws" "$OUT.spws.err" \ |
| 1572 | "$OUT.spghost" "$OUT.spstop" "$SOCK68" | 1636 | "$OUT.spghost" "$OUT.spstop" "$SOCK68" \ |
| 1637 | "$OUT.up.d" "$OUT.upcap" "$OUT.upcap.err" "$OUT.uppc" \ | ||
| 1638 | "$OUT.upm1" "$OUT.upm2" "$OUT.upm3" "$OUT.upst1" "$OUT.upst2" \ | ||
| 1639 | "$OUT.upref1" "$OUT.upref2" "$OUT.upok" "$OUT.upsta" "$OUT.upstop" \ | ||
| 1640 | "$SOCK69" "$UPBIN" \ | ||
| 1641 | "$OUT.url.d" "$OUT.urlm1" "$OUT.urlm2" "$OUT.urlup" "$OUT.urlstop" \ | ||
| 1642 | "$SOCK70" \ | ||
| 1643 | "$OUT.uag.d" "$OUT.uag" "$OUT.uag.err" "$OUT.uag.in" "$OUT.uagenv" \ | ||
| 1644 | "$OUT.uagup" "$OUT.uagstop" "$SOCK71" \ | ||
| 1645 | "$UPAGENT" "$UPAGKEY" "$UPAGKEY.pub" \ | ||
| 1646 | "$OUT.uqc.d" "$OUT.uqc" "$OUT.uqc.err" "$OUT.uqc.in" "$OUT.uqup" \ | ||
| 1647 | "$OUT.uqsta" "$OUT.uqstop" "$SOCK72" "$UPKEY" | ||
| 1648 | # The upgrade leg's private HOME, for the reason it has one: a session | ||
| 1649 | # shell that read the developer's rc files would set its own title. | ||
| 1650 | rm -rf "$UPHOME" | ||
| 1573 | # ...and the state homes those legs read their walls back out of, plus | 1651 | # ...and the state homes those legs read their walls back out of, plus |
| 1574 | # the spin leg's shim dir (its dial log lives inside it). | 1652 | # the spin leg's shim dir (its dial log lives inside it). |
| 1575 | rm -rf "$HYSTATE" "$WGSTATE" "$SPSTATE" "$SPDIR" | 1653 | rm -rf "$HYSTATE" "$WGSTATE" "$SPSTATE" "$SPDIR" |
| @@ -9019,8 +9097,510 @@ rm -rf "$SPSTATE" "$SPDIR" | |||
| 9019 | ok "a refusal the birth cannot fix backs off instead of spinning" | 9097 | ok "a refusal the birth cannot fix backs off instead of spinning" |
| 9020 | 9098 | ||
| 9021 | 9099 | ||
| 9022 | [ "$OK_COUNT" = "77" ] || { | 9100 | # --- muxd upgrade: the daemon becomes the new binary, holding everything --- |
| 9023 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 77 —" | 9101 | # |
| 9102 | # `muxd upgrade` execs the candidate OVER the running daemon: same pid, same | ||
| 9103 | # children, same descriptors. Nothing on the wire can carry the proof — a | ||
| 9104 | # client reconnecting to a freshly started daemon sees exactly the same | ||
| 9105 | # snapshot — so the witness is the session shell's own pid, put on the grid | ||
| 9106 | # before the exec and read off it after. A restart cannot fake that: a new | ||
| 9107 | # daemon forks a new shell, and a new shell has a new pid. | ||
| 9108 | # | ||
| 9109 | # One daemon, one attached client, and the whole story in order: what the | ||
| 9110 | # session had before, two refusals that must change nothing, the exec, and | ||
| 9111 | # then every one of those things again on the far side. | ||
| 9112 | # | ||
| 9113 | # bash with marks on, because two of the claims are the mechanism's: | ||
| 9114 | # `muxa run` answers under `marks` before the exec and must answer under | ||
| 9115 | # `marks` after it. The FIRST one after is the assertion that matters — | ||
| 9116 | # the manifest carries a return watermark, and one carried into the new | ||
| 9117 | # seq space is a watermark from the future that no return can pass, which | ||
| 9118 | # measured as the first await after every upgrade timing out at 30s while | ||
| 9119 | # the shell had already answered. | ||
| 9120 | UPVER=$("$MUXD" --version | awk '{print $2}') | ||
| 9121 | case "$UPVER" in | ||
| 9122 | ?*.?*) ;; | ||
| 9123 | *) echo "e2e FAIL: upgrade: muxd --version gave no version to refuse with: [$UPVER]" | ||
| 9124 | exit 1;; | ||
| 9125 | esac | ||
| 9126 | mkdir -p "$UPHOME" | ||
| 9127 | MUX_SHELL_INTEGRATION=1 HOME="$UPHOME" "$MUXD" run --sock "$SOCK69" \ | ||
| 9128 | --shell /bin/bash > "$OUT.up.d" 2>&1 & | ||
| 9129 | D69PID=$! | ||
| 9130 | wait_sock "$SOCK69" "$OUT.up.d" "upgrade daemon never bound" | ||
| 9131 | |||
| 9132 | # Marks BEFORE the exec, so "marks after" is a comparison rather than a | ||
| 9133 | # hope: a daemon that never had them would fail this line first. | ||
| 9134 | timeout 20 "$MUXA" run --sock "$SOCK69" --timeout 8000 'echo up-marks-pre' > "$OUT.upm1" 2>&1 || { | ||
| 9135 | echo "e2e FAIL: upgrade: muxa run failed before the upgrade even started:" | ||
| 9136 | cat "$OUT.upm1"; exit 1; } | ||
| 9137 | grep -qF '"mechanism":"marks"' "$OUT.upm1" || { | ||
| 9138 | echo "e2e FAIL: upgrade: the session had no marks to lose:" | ||
| 9139 | cat "$OUT.upm1"; exit 1; } | ||
| 9140 | |||
| 9141 | # The attached client, on a real pty, held across the exec. Its script | ||
| 9142 | # stops on `expect up-resumed`, which nothing in the script produces — the | ||
| 9143 | # suite's own post-upgrade `muxa run` does, below. That is the rendezvous: | ||
| 9144 | # the fixture reads the master the whole time it waits, so the capture is | ||
| 9145 | # continuous across the tear, and the needle can only arrive through the | ||
| 9146 | # connection the client re-dialled. | ||
| 9147 | # | ||
| 9148 | # `unset PROMPT_COMMAND; PROMPT_COMMAND=_mux_precmd` keeps mux's own precmd | ||
| 9149 | # (shellint's `precmd_fn`, which is where the marks come from) and drops | ||
| 9150 | # every other member. A distribution rc appends a title-setting member | ||
| 9151 | # there, and it repaints the title on every prompt — the title this leg | ||
| 9152 | # sets would be gone by the next one. `unset` first because a scalar | ||
| 9153 | # assignment onto bash's array spelling lands on element 0 and leaves the | ||
| 9154 | # rest. | ||
| 9155 | timeout 150 "$PTYCLIENT" --cols 100 --rows 30 \ | ||
| 9156 | --out "$OUT.upcap" --err "$OUT.upcap.err" -- \ | ||
| 9157 | "$MUX" --sock "$SOCK69" > "$OUT.uppc" 2>&1 <<'EOF' & | ||
| 9158 | settle 1000 25000 | ||
| 9159 | send unset PROMPT_COMMAND; PROMPT_COMMAND=_mux_precmd\n | ||
| 9160 | settle 800 25000 | ||
| 9161 | send printf '\\033]0;uptitle\\007'\n | ||
| 9162 | expect \x1b]0;uptitle\x07 25000 | ||
| 9163 | send printf 'up-%s\n' pre\n | ||
| 9164 | expect up-pre 25000 | ||
| 9165 | send echo shpid=$$\n | ||
| 9166 | expect shpid= 25000 | ||
| 9167 | settle 800 25000 | ||
| 9168 | expect \x1b]0;uptitle\x07 90000 | ||
| 9169 | expect up-resumed 60000 | ||
| 9170 | settle 800 25000 | ||
| 9171 | send printf 'up-%s\n' typed\n | ||
| 9172 | expect up-typed 25000 | ||
| 9173 | send \x1cd | ||
| 9174 | waitexit 15000 | ||
| 9175 | EOF | ||
| 9176 | UPPCPID=$! | ||
| 9177 | |||
| 9178 | # Daemon truth for the pid, and the number this leg is built around. The | ||
| 9179 | # echoed command line spells `$$`, so only the shell's own answer has | ||
| 9180 | # digits after the `=` — grepping for a bare `shpid=` would match the echo. | ||
| 9181 | wait_grid "$SOCK69" "shpid=[0-9]" "upgrade: the session shell never printed its pid" | ||
| 9182 | UPSHPID=$(dump_session "$SOCK69" | sed -n 's/.*shpid=\([0-9][0-9]*\).*/\1/p' | head -1) | ||
| 9183 | [ -n "$UPSHPID" ] || { | ||
| 9184 | echo "e2e FAIL: upgrade: no shell pid read off the grid — the witness would be vacuous" | ||
| 9185 | dump_session "$SOCK69"; exit 1; } | ||
| 9186 | |||
| 9187 | "$MUXD" stats --sock "$SOCK69" > "$OUT.upst1" 2>&1 || { | ||
| 9188 | echo "e2e FAIL: upgrade: stats did not answer before the upgrade"; exit 1; } | ||
| 9189 | UPATT1=$(sed -n 's/.*attaches=\([0-9]*\).*/\1/p' "$OUT.upst1") | ||
| 9190 | UPSNAP1=$(sed -n 's/^snapshots=\([0-9]*\).*/\1/p' "$OUT.upst1") | ||
| 9191 | [ -n "$UPATT1" ] && [ -n "$UPSNAP1" ] || { | ||
| 9192 | echo "e2e FAIL: upgrade: no counters read out of stats; the carry pin would be vacuous:" | ||
| 9193 | cat "$OUT.upst1"; exit 1; } | ||
| 9194 | |||
| 9195 | # 1. Refused: same version, no flag. The reason names BOTH versions, which | ||
| 9196 | # is the difference between a policy an operator can act on and a "no". | ||
| 9197 | set +e | ||
| 9198 | "$MUXD" upgrade --sock "$SOCK69" > "$OUT.upref1" 2>&1 | ||
| 9199 | UPRC=$? | ||
| 9200 | set -e | ||
| 9201 | [ "$UPRC" -eq 1 ] || { | ||
| 9202 | echo "e2e FAIL: upgrade: a same-version upgrade exited $UPRC, want 1:" | ||
| 9203 | cat "$OUT.upref1"; exit 1; } | ||
| 9204 | grep -qF "refused: version: $UPVER is not newer than $UPVER" "$OUT.upref1" || { | ||
| 9205 | echo "e2e FAIL: upgrade: the refusal did not name both versions:" | ||
| 9206 | cat "$OUT.upref1"; exit 1; } | ||
| 9207 | |||
| 9208 | # 2. Refused: a candidate that is not executable. `muxd upgrade` offers its | ||
| 9209 | # OWN path (/proc/self/exe), so the only way to spell this refusal is to | ||
| 9210 | # take the exec bit off a copy AFTER it has started — and the ordering is | ||
| 9211 | # made deterministic rather than hoped for. The daemon is held under | ||
| 9212 | # SIGSTOP so it cannot read the request until the chmod has landed; | ||
| 9213 | # `readlink /proc/PID/exe` is the witness that the copy has already | ||
| 9214 | # exec'd, so the chmod cannot beat it and turn the leg into an exec | ||
| 9215 | # failure that asserts nothing. | ||
| 9216 | cp "$MUXD" "$UPBIN" | ||
| 9217 | chmod 755 "$UPBIN" | ||
| 9218 | kill -STOP "$D69PID" | ||
| 9219 | "$UPBIN" upgrade --sock "$SOCK69" --allow-same-version > "$OUT.upref2" 2>&1 & | ||
| 9220 | UPXPID=$! | ||
| 9221 | _i=0 | ||
| 9222 | while [ "$(readlink "/proc/$UPXPID/exe" 2>/dev/null)" != "$UPBIN" ]; do | ||
| 9223 | _i=$((_i + 1)) | ||
| 9224 | [ "$_i" -lt $(( 100 * TIME_SCALE )) ] || { | ||
| 9225 | kill -CONT "$D69PID" 2>/dev/null || true | ||
| 9226 | echo "e2e FAIL: upgrade: the candidate copy never exec'd; the refusal below" | ||
| 9227 | echo " would have been about a binary that never asked" | ||
| 9228 | exit 1; } | ||
| 9229 | sleep 0.05 | ||
| 9230 | done | ||
| 9231 | chmod 000 "$UPBIN" | ||
| 9232 | kill -CONT "$D69PID" | ||
| 9233 | set +e | ||
| 9234 | wait "$UPXPID" | ||
| 9235 | UPRC=$? | ||
| 9236 | set -e | ||
| 9237 | chmod 755 "$UPBIN" | ||
| 9238 | [ "$UPRC" -eq 1 ] || { | ||
| 9239 | echo "e2e FAIL: upgrade: an unexecutable candidate exited $UPRC, want 1:" | ||
| 9240 | cat "$OUT.upref2"; exit 1; } | ||
| 9241 | grep -qF "refused: path: not executable" "$OUT.upref2" || { | ||
| 9242 | echo "e2e FAIL: upgrade: the refusal did not name the path check:" | ||
| 9243 | cat "$OUT.upref2"; exit 1; } | ||
| 9244 | |||
| 9245 | # ...and two refusals changed NOTHING. A refusal that had torn a client | ||
| 9246 | # down, or written the manifest and left the fd table half open, would | ||
| 9247 | # still have printed the words above. | ||
| 9248 | timeout 20 "$MUXA" status --sock "$SOCK69" > "$OUT.upsta" 2>&1 || { | ||
| 9249 | echo "e2e FAIL: upgrade: the daemon stopped answering after refusing:" | ||
| 9250 | cat "$OUT.upsta"; exit 1; } | ||
| 9251 | wait_grid "$SOCK69" "shpid=$UPSHPID" "upgrade: a refusal cost the session its shell" | ||
| 9252 | |||
| 9253 | # 3. The exec itself. | ||
| 9254 | set +e | ||
| 9255 | "$MUXD" upgrade --sock "$SOCK69" --allow-same-version > "$OUT.upok" 2>&1 | ||
| 9256 | UPRC=$? | ||
| 9257 | set -e | ||
| 9258 | [ "$UPRC" -eq 0 ] || { | ||
| 9259 | echo "e2e FAIL: upgrade: the upgrade exited $UPRC, want 0:" | ||
| 9260 | cat "$OUT.upok" "$OUT.up.d"; exit 1; } | ||
| 9261 | grep -qF "muxd: upgraded to $UPVER" "$OUT.upok" || { | ||
| 9262 | echo "e2e FAIL: upgrade: no verdict naming the version it landed on:" | ||
| 9263 | cat "$OUT.upok"; exit 1; } | ||
| 9264 | |||
| 9265 | # The pid is the feature. Not "a daemon is answering" — the same process, | ||
| 9266 | # now running a different image, which /proc/PID/cmdline says in one word: | ||
| 9267 | # a daemon that had restarted would carry the original `run --sock` argv. | ||
| 9268 | kill -0 "$D69PID" 2>/dev/null || { | ||
| 9269 | echo "e2e FAIL: upgrade: the daemon pid $D69PID is gone — that is a restart, not an upgrade" | ||
| 9270 | cat "$OUT.up.d"; exit 1; } | ||
| 9271 | tr '\0' ' ' < "/proc/$D69PID/cmdline" > "$OUT.upst2" 2>/dev/null || true | ||
| 9272 | grep -qF -- "--resume-fd" "$OUT.upst2" || { | ||
| 9273 | echo "e2e FAIL: upgrade: pid $D69PID is not running the resumed argv; it holds:" | ||
| 9274 | cat "$OUT.upst2"; exit 1; } | ||
| 9275 | |||
| 9276 | # Counters CARRY, and it is asserted HERE rather than at the end for a | ||
| 9277 | # reason about blame: the re-attach poll below counts `attaches` up from | ||
| 9278 | # the reading taken before the exec, so a daemon that had zeroed its | ||
| 9279 | # counters fails THAT poll — reported as a client that never came back, | ||
| 9280 | # which is the wrong diagnosis of the wrong bug. `-ge`, because the | ||
| 9281 | # re-attach this reading races has not necessarily landed yet. | ||
| 9282 | "$MUXD" stats --sock "$SOCK69" > "$OUT.upst2" 2>&1 || { | ||
| 9283 | echo "e2e FAIL: upgrade: stats did not answer after the exec"; cat "$OUT.up.d"; exit 1; } | ||
| 9284 | UPATT2=$(sed -n 's/.*attaches=\([0-9]*\).*/\1/p' "$OUT.upst2") | ||
| 9285 | UPSNAP2=$(sed -n 's/^snapshots=\([0-9]*\).*/\1/p' "$OUT.upst2") | ||
| 9286 | [ -n "$UPATT2" ] && [ -n "$UPSNAP2" ] || { | ||
| 9287 | echo "e2e FAIL: upgrade: no counters read out of stats after the exec:" | ||
| 9288 | cat "$OUT.upst2"; exit 1; } | ||
| 9289 | [ "$UPATT2" -ge "$UPATT1" ] || { | ||
| 9290 | echo "e2e FAIL: upgrade: attaches went $UPATT1 -> $UPATT2 across the exec; the" | ||
| 9291 | echo " counters were reset, not carried, and an upgrade now looks like" | ||
| 9292 | echo " a restart to anything sampling this daemon" | ||
| 9293 | exit 1; } | ||
| 9294 | [ "$UPSNAP2" -ge "$UPSNAP1" ] || { | ||
| 9295 | echo "e2e FAIL: upgrade: snapshots went $UPSNAP1 -> $UPSNAP2 across the exec; the" | ||
| 9296 | echo " counters were reset, not carried" | ||
| 9297 | exit 1; } | ||
| 9298 | |||
| 9299 | # The client came back on its own. Counted with `attaches=`, not with | ||
| 9300 | # `clients=`: the client is re-dialled within a millisecond of the exec, so | ||
| 9301 | # a poll that only looked for clients=1 would be satisfied by the reading it | ||
| 9302 | # took before the tear and would pass on a daemon that never dropped anyone. | ||
| 9303 | _i=0 | ||
| 9304 | while : ; do | ||
| 9305 | _upst=$("$MUXD" stats --sock "$SOCK69" 2>/dev/null || true) | ||
| 9306 | _upatt=$(printf '%s' "$_upst" | sed -n 's/.*attaches=\([0-9]*\).*/\1/p') | ||
| 9307 | case "$_upst" in | ||
| 9308 | *"session 0 clients=1"*) | ||
| 9309 | if [ -n "$_upatt" ] && [ "$_upatt" -gt "$UPATT1" ]; then break; fi ;; | ||
| 9310 | esac | ||
| 9311 | _i=$((_i + 1)) | ||
| 9312 | [ "$_i" -lt $(( 200 * TIME_SCALE )) ] || { | ||
| 9313 | echo "e2e FAIL: upgrade: the attached client never re-attached after the exec;" | ||
| 9314 | echo " stats holds: $_upst" | ||
| 9315 | cat "$OUT.up.d"; exit 1; } | ||
| 9316 | sleep 0.1 | ||
| 9317 | done | ||
| 9318 | |||
| 9319 | # The grid crossed with the process: the marker the client typed, and the | ||
| 9320 | # pid of the shell that is still running under the new image. | ||
| 9321 | wait_grid "$SOCK69" "up-pre" "upgrade: the pre-upgrade marker did not survive the exec" | ||
| 9322 | wait_grid "$SOCK69" "shpid=$UPSHPID" "upgrade: the session is a NEW shell, not the one that crossed" | ||
| 9323 | |||
| 9324 | # The FIRST muxa run after the exec, and it is also what releases the | ||
| 9325 | # attached client's `expect up-resumed` above — one command, two claims. | ||
| 9326 | set +e | ||
| 9327 | timeout 40 "$MUXA" run --sock "$SOCK69" --timeout 20000 'echo up-resumed' > "$OUT.upm2" 2>&1 | ||
| 9328 | UPRC=$? | ||
| 9329 | set -e | ||
| 9330 | [ "$UPRC" -eq 0 ] || { | ||
| 9331 | echo "e2e FAIL: upgrade: the FIRST muxa run after the exec exited $UPRC — a carried" | ||
| 9332 | echo " return watermark is a watermark from the future and no return passes it:" | ||
| 9333 | cat "$OUT.upm2"; exit 1; } | ||
| 9334 | grep -qF '"mechanism":"marks"' "$OUT.upm2" || { | ||
| 9335 | echo "e2e FAIL: upgrade: the first await after the exec fell off marks:" | ||
| 9336 | cat "$OUT.upm2"; exit 1; } | ||
| 9337 | grep -qF '"exit_code":0' "$OUT.upm2" || { | ||
| 9338 | echo "e2e FAIL: upgrade: no real exit code came back after the exec:" | ||
| 9339 | cat "$OUT.upm2"; exit 1; } | ||
| 9340 | # The client's own verdict: the title came back on the re-attach, the | ||
| 9341 | # marker arrived through the new image, and a keystroke typed AFTER the | ||
| 9342 | # exec reached the shell. | ||
| 9343 | set +e | ||
| 9344 | wait "$UPPCPID" | ||
| 9345 | UPRC=$? | ||
| 9346 | set -e | ||
| 9347 | UPPCPID="" | ||
| 9348 | [ "$UPRC" -eq 0 ] || { | ||
| 9349 | echo "e2e FAIL: upgrade: the attached client exited $UPRC across the exec:" | ||
| 9350 | cat "$OUT.uppc" "$OUT.upcap.err"; exit 1; } | ||
| 9351 | |||
| 9352 | # A NONZERO exit code too, because `"exit_code":0` is also what a mechanism | ||
| 9353 | # that guesses would say. After the client has gone, not beside it: two | ||
| 9354 | # writers typing into one shell's pty interleave, and the leg would be | ||
| 9355 | # racing itself. Spelled as a CHILD, not as `exit 7`, which is the session | ||
| 9356 | # shell's own exit — the session would end, the daemon with it, and the | ||
| 9357 | # counter reading below would be taken from nothing. Not `(exit 7)` either: | ||
| 9358 | # a subshell sets `_mux_ran` in the subshell, so shellint's `precmd_fn` | ||
| 9359 | # sees an untouched prompt in the parent and emits no `D;` mark at all | ||
| 9360 | # (measured: the await ran its whole budget out). | ||
| 9361 | timeout 40 "$MUXA" run --sock "$SOCK69" --timeout 20000 'sh -c "exit 7"' > "$OUT.upm3" 2>&1 || true | ||
| 9362 | grep -qF '"exit_code":7' "$OUT.upm3" || { | ||
| 9363 | echo "e2e FAIL: upgrade: the shell's own exit code did not survive the exec:" | ||
| 9364 | cat "$OUT.upm3"; exit 1; } | ||
| 9365 | |||
| 9366 | # ...and the re-attach was COUNTED onto them rather than replacing them. | ||
| 9367 | # The carry was asserted above with `-ge`; this is the other half, taken | ||
| 9368 | # once the client's own snapshot has certainly been served. | ||
| 9369 | "$MUXD" stats --sock "$SOCK69" > "$OUT.upst2" 2>&1 | ||
| 9370 | UPATT2=$(sed -n 's/.*attaches=\([0-9]*\).*/\1/p' "$OUT.upst2") | ||
| 9371 | UPSNAP2=$(sed -n 's/^snapshots=\([0-9]*\).*/\1/p' "$OUT.upst2") | ||
| 9372 | [ -n "$UPATT2" ] && [ -n "$UPSNAP2" ] || { | ||
| 9373 | echo "e2e FAIL: upgrade: no counters read out of stats after the client left:" | ||
| 9374 | cat "$OUT.upst2"; exit 1; } | ||
| 9375 | [ "$UPATT2" -gt "$UPATT1" ] || { | ||
| 9376 | echo "e2e FAIL: upgrade: attaches is still $UPATT2 — the re-attach after the exec" | ||
| 9377 | echo " was never counted" | ||
| 9378 | exit 1; } | ||
| 9379 | [ "$UPSNAP2" -gt "$UPSNAP1" ] || { | ||
| 9380 | echo "e2e FAIL: upgrade: snapshots is still $UPSNAP2 — the re-attach was served" | ||
| 9381 | echo " without a snapshot, which no fresh epoch allows" | ||
| 9382 | exit 1; } | ||
| 9383 | |||
| 9384 | assert_stopped "$SOCK69" "$D69PID" "upgrade" "$OUT.upstop" | ||
| 9385 | D69PID="" | ||
| 9386 | rm -f "$UPBIN" | ||
| 9387 | rm -rf "$UPHOME" | ||
| 9388 | ok "muxd upgrade keeps the shell, its pid, its title, its marks and its counters" | ||
| 9389 | |||
| 9390 | # --- ...and a candidate that cannot adopt hands the daemon back ----------- | ||
| 9391 | # | ||
| 9392 | # The rollback: the new image fails partway through adoption and execs the | ||
| 9393 | # OLD binary — whose path the manifest carries — with the same manifest fd. | ||
| 9394 | # Armed through the ENVIRONMENT rather than the flag it shares a name with, | ||
| 9395 | # and that is not a convenience: `execUpgrade` builds a fixed argv | ||
| 9396 | # (`run --resume-fd N`), so nothing typed here can put `--resume-fail-at` | ||
| 9397 | # in front of the candidate. The daemon's environment is what crosses an | ||
| 9398 | # exec, so the abort is armed on the daemon before any upgrade asks. | ||
| 9399 | # | ||
| 9400 | # The daemon's own log is the only thing that can say a rollback HAPPENED — | ||
| 9401 | # `muxd upgrade` prints its verdict the moment the daemon accepts, and the | ||
| 9402 | # process that would have corrected it is gone. Without that line every | ||
| 9403 | # assertion below would pass just as well on an upgrade that simply worked. | ||
| 9404 | mkdir -p "$UPHOME" | ||
| 9405 | MUX_SHELL_INTEGRATION=1 MUX_RESUME_FAIL_AT=session HOME="$UPHOME" \ | ||
| 9406 | "$MUXD" run --sock "$SOCK70" --shell /bin/bash > "$OUT.url.d" 2>&1 & | ||
| 9407 | D70PID=$! | ||
| 9408 | wait_sock "$SOCK70" "$OUT.url.d" "rollback daemon never bound" | ||
| 9409 | |||
| 9410 | timeout 20 "$MUXA" run --sock "$SOCK70" --timeout 8000 'echo rollpid=$$' > "$OUT.urlm1" 2>&1 || { | ||
| 9411 | echo "e2e FAIL: rollback: muxa run failed before the upgrade:" | ||
| 9412 | cat "$OUT.urlm1"; exit 1; } | ||
| 9413 | wait_grid "$SOCK70" "rollpid=[0-9]" "rollback: the session shell never printed its pid" | ||
| 9414 | UPROLLPID=$(dump_session "$SOCK70" | sed -n 's/.*rollpid=\([0-9][0-9]*\).*/\1/p' | head -1) | ||
| 9415 | [ -n "$UPROLLPID" ] || { | ||
| 9416 | echo "e2e FAIL: rollback: no shell pid read off the grid"; dump_session "$SOCK70"; exit 1; } | ||
| 9417 | |||
| 9418 | set +e | ||
| 9419 | "$MUXD" upgrade --sock "$SOCK70" --allow-same-version > "$OUT.urlup" 2>&1 | ||
| 9420 | UPRC=$? | ||
| 9421 | set -e | ||
| 9422 | [ "$UPRC" -eq 0 ] || { | ||
| 9423 | echo "e2e FAIL: rollback: upgrade exited $UPRC — the rolled-back daemon must serve" | ||
| 9424 | echo " well enough for the confirm to pass:" | ||
| 9425 | cat "$OUT.urlup" "$OUT.url.d"; exit 1; } | ||
| 9426 | wait_for "$OUT.url.d" "adoption failed at session" 15 || { | ||
| 9427 | echo "e2e FAIL: rollback: the candidate adopted instead of aborting — the abort" | ||
| 9428 | echo " was not armed, and nothing below tests a rollback:" | ||
| 9429 | cat "$OUT.url.d"; exit 1; } | ||
| 9430 | grep -qF "exec'ing" "$OUT.url.d" || { | ||
| 9431 | echo "e2e FAIL: rollback: the abort never named the binary it went back to:" | ||
| 9432 | cat "$OUT.url.d"; exit 1; } | ||
| 9433 | |||
| 9434 | kill -0 "$D70PID" 2>/dev/null || { | ||
| 9435 | echo "e2e FAIL: rollback: the daemon pid $D70PID is gone; a failed adoption took" | ||
| 9436 | echo " the sessions with it" | ||
| 9437 | cat "$OUT.url.d"; exit 1; } | ||
| 9438 | wait_grid "$SOCK70" "rollpid=$UPROLLPID" "rollback: the session lost its shell to a failed adoption" | ||
| 9439 | timeout 40 "$MUXA" run --sock "$SOCK70" --timeout 20000 'echo roll-post' > "$OUT.urlm2" 2>&1 || { | ||
| 9440 | echo "e2e FAIL: rollback: the daemon that came back cannot run a command:" | ||
| 9441 | cat "$OUT.urlm2" "$OUT.url.d"; exit 1; } | ||
| 9442 | grep -qF '"mechanism":"marks"' "$OUT.urlm2" || { | ||
| 9443 | echo "e2e FAIL: rollback: the re-adopted session lost its marks:" | ||
| 9444 | cat "$OUT.urlm2"; exit 1; } | ||
| 9445 | |||
| 9446 | assert_stopped "$SOCK70" "$D70PID" "rollback" "$OUT.urlstop" | ||
| 9447 | D70PID="" | ||
| 9448 | rm -rf "$UPHOME" | ||
| 9449 | ok "a candidate that cannot adopt execs the old binary back, session and marks intact" | ||
| 9450 | |||
| 9451 | # --- ...and the session's agent socket crosses the exec ------------------- | ||
| 9452 | # | ||
| 9453 | # The per-session agent listener is a descriptor the daemon owns and the | ||
| 9454 | # shell reaches by path ($SSH_AUTH_SOCK, named after the daemon's pid). It | ||
| 9455 | # has to cross the exec on both counts: the fd by inheritance, the path | ||
| 9456 | # because the pid does not change. Nothing else in this suite would notice | ||
| 9457 | # if it did not — the agent channels themselves are deliberately NOT | ||
| 9458 | # carried, so the only thing that can say the listener survived is a real | ||
| 9459 | # `ssh-add -l` answering through a re-dialled `-A` client. | ||
| 9460 | # | ||
| 9461 | # Its own agent rather than the forwarding legs' AGENT48: that one is left | ||
| 9462 | # under SIGSTOP by the mute-offerer leg, and a mute agent is exactly what | ||
| 9463 | # this leg cannot tell from a broken listener. | ||
| 9464 | "$MUXD" run --sock "$SOCK71" --shell /bin/sh > "$OUT.uag.d" 2>&1 & | ||
| 9465 | D71PID=$! | ||
| 9466 | wait_sock "$SOCK71" "$OUT.uag.d" "agent-upgrade daemon never bound" | ||
| 9467 | |||
| 9468 | ssh-agent -a "$UPAGENT" > "$OUT.uagenv" 2>&1 | ||
| 9469 | UPAGPID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.uagenv") | ||
| 9470 | [ -n "$UPAGPID" ] || { | ||
| 9471 | echo "e2e FAIL: agent-upgrade: ssh-agent printed no pid for the trap to hold it by:" | ||
| 9472 | cat "$OUT.uagenv"; exit 1; } | ||
| 9473 | ssh-keygen -q -t ed25519 -N '' -C mux-e2e-upgrade -f "$UPAGKEY" | ||
| 9474 | SSH_AUTH_SOCK="$UPAGENT" ssh-add "$UPAGKEY" > /dev/null 2>&1 | ||
| 9475 | UPFP=$(ssh-keygen -lf "$UPAGKEY" | awk '{print $2}') | ||
| 9476 | case "$UPFP" in | ||
| 9477 | SHA256:?*) ;; | ||
| 9478 | *) echo "e2e FAIL: agent-upgrade: ssh-keygen -lf gave no fingerprint: [$UPFP]"; exit 1;; | ||
| 9479 | esac | ||
| 9480 | |||
| 9481 | pipe_mux "$OUT.uag" "$OUT.uag.err" env SSH_AUTH_SOCK="$UPAGENT" timeout 90 \ | ||
| 9482 | "$MUX" -A --sock "$SOCK71" | ||
| 9483 | UPATT1=$("$MUXD" stats --sock "$SOCK71" | sed -n 's/.*attaches=\([0-9]*\).*/\1/p') | ||
| 9484 | [ -n "$UPATT1" ] || { echo "e2e FAIL: agent-upgrade: no attaches counter to compare against"; exit 1; } | ||
| 9485 | # `agpre=` rather than the fingerprint alone: the shell echoes what is | ||
| 9486 | # typed, and `$?` is not expanded in the echo, so only ssh-add's own status | ||
| 9487 | # can put a digit there. The fingerprint is asserted beside it because a | ||
| 9488 | # status of 0 says an agent answered, not that it was the client's. | ||
| 9489 | pipe_send 'ssh-add -l; echo agpre=$?\n' | ||
| 9490 | await_out "$OUT.uag" "agpre=0" "agent-upgrade: ssh-add never answered before the upgrade" | ||
| 9491 | # Counted by OCCURRENCE, not by matching line: this capture is an escape | ||
| 9492 | # stream whose one "line" is the whole session, so `grep -c` would answer 1 | ||
| 9493 | # for both answers and the comparison after the exec would be vacuous. | ||
| 9494 | UPFP1=$(grep -aoF -- "$UPFP" "$OUT.uag" | wc -l) | ||
| 9495 | [ "$UPFP1" -ge 1 ] || { | ||
| 9496 | echo "e2e FAIL: agent-upgrade: the client's key never came back before the upgrade" | ||
| 9497 | echo " (wanted $UPFP)"; exit 1; } | ||
| 9498 | |||
| 9499 | set +e | ||
| 9500 | "$MUXD" upgrade --sock "$SOCK71" --allow-same-version > "$OUT.uagup" 2>&1 | ||
| 9501 | UPRC=$? | ||
| 9502 | set -e | ||
| 9503 | [ "$UPRC" -eq 0 ] || { | ||
| 9504 | echo "e2e FAIL: agent-upgrade: upgrade exited $UPRC:" | ||
| 9505 | cat "$OUT.uagup" "$OUT.uag.d"; exit 1; } | ||
| 9506 | _i=0 | ||
| 9507 | while : ; do | ||
| 9508 | _upst=$("$MUXD" stats --sock "$SOCK71" 2>/dev/null || true) | ||
| 9509 | _upatt=$(printf '%s' "$_upst" | sed -n 's/.*attaches=\([0-9]*\).*/\1/p') | ||
| 9510 | case "$_upst" in | ||
| 9511 | *"session 0 clients=1"*) | ||
| 9512 | if [ -n "$_upatt" ] && [ "$_upatt" -gt "$UPATT1" ]; then break; fi ;; | ||
| 9513 | esac | ||
| 9514 | _i=$((_i + 1)) | ||
| 9515 | [ "$_i" -lt $(( 200 * TIME_SCALE )) ] || { | ||
| 9516 | echo "e2e FAIL: agent-upgrade: the -A client never re-attached; stats holds: $_upst" | ||
| 9517 | cat "$OUT.uag.d"; exit 1; } | ||
| 9518 | sleep 0.1 | ||
| 9519 | done | ||
| 9520 | |||
| 9521 | # The same question a second time, and `agdone=` is a needle the first | ||
| 9522 | # answer cannot satisfy. | ||
| 9523 | pipe_send 'ssh-add -l; echo agdone=$?\n' | ||
| 9524 | await_out "$OUT.uag" "agdone=0" "agent-upgrade: ssh-add stopped answering across the exec" | ||
| 9525 | UPFP2=$(grep -aoF -- "$UPFP" "$OUT.uag" | wc -l) | ||
| 9526 | [ "$UPFP2" -gt "$UPFP1" ] || { | ||
| 9527 | echo "e2e FAIL: agent-upgrade: ssh-add exited 0 but the key came back $UPFP1 time(s)" | ||
| 9528 | echo " before the exec and $UPFP2 after — no NEW answer crossed"; exit 1; } | ||
| 9529 | pipe_detach "agent-upgrade client" | ||
| 9530 | |||
| 9531 | assert_stopped "$SOCK71" "$D71PID" "agent-upgrade" "$OUT.uagstop" | ||
| 9532 | D71PID="" | ||
| 9533 | kill "$UPAGPID" 2>/dev/null || true | ||
| 9534 | wait_pid_gone "$UPAGPID" "agent-upgrade: ssh-agent killed by tracked pid" | ||
| 9535 | UPAGPID="" | ||
| 9536 | ok "the session's agent socket crosses the exec: ssh-add -l answers again" | ||
| 9537 | |||
| 9538 | # --- ...and a QUIC client is serving again in a breath, not after a timeout | ||
| 9539 | # | ||
| 9540 | # Unix clients need no goodbye — their fds are close-on-exec and the exec is | ||
| 9541 | # their EOF. A QUIC peer has no such thing: silence is indistinguishable from | ||
| 9542 | # a bad network, and a client that waited it out would sit on a blank wall | ||
| 9543 | # for the whole idle timeout. So the daemon sends CONNECTION_CLOSE to every | ||
| 9544 | # connection before it execs, and what that buys is the number asserted | ||
| 9545 | # here: back in service in a fraction of `--quic-idle-ms`, not after it. | ||
| 9546 | head -c 32 /dev/urandom > "$UPKEY" | ||
| 9547 | chmod 600 "$UPKEY" | ||
| 9548 | "$MUXD" run --sock "$SOCK72" --shell /bin/sh \ | ||
| 9549 | --quic "127.0.0.1:$UPQPORT" --key "$UPKEY" --quic-idle-ms 15000 > "$OUT.uqc.d" 2>&1 & | ||
| 9550 | D72PID=$! | ||
| 9551 | wait_sock "$SOCK72" "$OUT.uqc.d" "quic-upgrade daemon never bound" | ||
| 9552 | |||
| 9553 | pipe_mux "$OUT.uqc" "$OUT.uqc.err" timeout 90 "$MUX" "quic://127.0.0.1:$UPQPORT" \ | ||
| 9554 | --key "$UPKEY" --quic-idle-ms 15000 | ||
| 9555 | pipe_send 'printf "uq-%%s\\n" pre\n' | ||
| 9556 | await_out "$OUT.uqc" "uq-pre" "quic-upgrade: the QUIC client never got its pre-upgrade marker" | ||
| 9557 | |||
| 9558 | UPATT1=$("$MUXD" stats --sock "$SOCK72" | sed -n 's/.*attaches=\([0-9]*\).*/\1/p') | ||
| 9559 | [ -n "$UPATT1" ] || { echo "e2e FAIL: quic-upgrade: no attaches counter to compare against"; exit 1; } | ||
| 9560 | UPT0=$(date +%s%N) | ||
| 9561 | set +e | ||
| 9562 | "$MUXD" upgrade --sock "$SOCK72" --allow-same-version > "$OUT.uqup" 2>&1 | ||
| 9563 | UPRC=$? | ||
| 9564 | set -e | ||
| 9565 | [ "$UPRC" -eq 0 ] || { | ||
| 9566 | echo "e2e FAIL: quic-upgrade: upgrade exited $UPRC:" | ||
| 9567 | cat "$OUT.uqup" "$OUT.uqc.d"; exit 1; } | ||
| 9568 | _i=0 | ||
| 9569 | while : ; do | ||
| 9570 | _upst=$("$MUXD" stats --sock "$SOCK72" 2>/dev/null || true) | ||
| 9571 | _upatt=$(printf '%s' "$_upst" | sed -n 's/.*attaches=\([0-9]*\).*/\1/p') | ||
| 9572 | case "$_upst" in | ||
| 9573 | *"session 0 clients=1"*) | ||
| 9574 | if [ -n "$_upatt" ] && [ "$_upatt" -gt "$UPATT1" ]; then break; fi ;; | ||
| 9575 | esac | ||
| 9576 | _i=$((_i + 1)) | ||
| 9577 | [ "$_i" -lt $(( 400 * TIME_SCALE )) ] || { | ||
| 9578 | echo "e2e FAIL: quic-upgrade: the QUIC client never re-attached; stats holds: $_upst" | ||
| 9579 | cat "$OUT.uqc.d"; exit 1; } | ||
| 9580 | sleep 0.05 | ||
| 9581 | done | ||
| 9582 | UPT1=$(date +%s%N) | ||
| 9583 | UPMS=$(( (UPT1 - UPT0) / 1000000 )) | ||
| 9584 | # Half the idle timeout, which is the only bound that means anything here: | ||
| 9585 | # a client that had waited the connection out would arrive at 15000ms and | ||
| 9586 | # still pass a generous absolute budget. Measured at ~60ms. | ||
| 9587 | [ "$UPMS" -lt $(( 7500 * TIME_SCALE )) ] || { | ||
| 9588 | echo "e2e FAIL: quic-upgrade: the QUIC client took ${UPMS}ms to come back —" | ||
| 9589 | echo " that is the idle timeout expiring, not a CONNECTION_CLOSE" | ||
| 9590 | exit 1; } | ||
| 9591 | |||
| 9592 | # ...and it is a session, not just a connection: a round trip that did not | ||
| 9593 | # exist before the exec. | ||
| 9594 | pipe_send 'printf "uq-%%s\\n" post\n' | ||
| 9595 | await_out "$OUT.uqc" "uq-post" "quic-upgrade: the reconnected QUIC client cannot reach its shell" | ||
| 9596 | pipe_detach "quic-upgrade client" | ||
| 9597 | |||
| 9598 | assert_stopped "$SOCK72" "$D72PID" "quic-upgrade" "$OUT.uqstop" | ||
| 9599 | D72PID="" | ||
| 9600 | ok "a QUIC client is served again within a breath of the exec, not after the idle timeout" | ||
| 9601 | |||
| 9602 | [ "$OK_COUNT" = "81" ] || { | ||
| 9603 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 81 —" | ||
| 9024 | echo " a scenario was added (update the pin) or silently lost" | 9604 | echo " a scenario was added (update the pin) or silently lost" |
| 9025 | exit 1 | 9605 | exit 1 |
| 9026 | } | 9606 | } |