a73x

69a8393b

test: a wall polling two boxes starts no daemon; Enter and a cold mux HOST each start one

a73x   2026-08-28 19:53

Commit message
test: a wall polling two boxes starts no daemon; Enter and a cold mux HOST each start one

test/e2e.sh
Old New
@@ -177,8 +177,8 @@ done
177 # one of those and adds a convergence point would be pinning a fact every 177 # one of those and adds a convergence point would be pinning a fact every
178 # leg above already establishes. 178 # leg above already establishes.
179 179
180 [ "$OK_COUNT" = "86" ] || { 180 [ "$OK_COUNT" = "87" ] || {
181 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 86 —" 181 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 87 —"
182 echo " a scenario was added (update the pin) or silently lost" 182 echo " a scenario was added (update the pin) or silently lost"
183 exit 1 183 exit 1
184 } 184 }
test/e2e_04_handoff.sh
Old New
@@ -62,18 +62,15 @@ HDEADPORT=$(( 16000 + ($$ % 4000) ))
62 # command was plain argv; it execs a program literally named 62 # command was plain argv; it execs a program literally named
63 # `PATH=... muxd endpoint` now, and dies at 127 without an announce. 63 # `PATH=... muxd endpoint` now, and dies at 127 without an announce.
64 # 64 #
65 # The arity guard covers the case that would NOT be loud: `ssh HOST` with 65 # The option loop and the arity guard come from `ssh_shim_head`, which
66 # no command at all. There, `shift` empties "$@" and a bare `exec` is a 66 # both of this suite's fake sshs share: the recipes these scenarios drive
67 # no-op, so the shim would exit 0 having run nothing — silence that looks 67 # happen to be the asking ones, which pass no `-o`, and a shim that is
68 # exactly like success. Note what the guard's 97 does and does not buy: 68 # right only by that coincidence breaks as "no announce" the day the entry
69 # nothing prints it, and the client experiences it only as an ssh that 69 # dial gains a flag.
70 # closed without an announce. It converts "quietly wrong" into "visibly
71 # no announce", which is a diagnosis this suite can act on.
72 mkdir -p "$SSHIM_DIR" "$HRUN" "$HRUN2" 70 mkdir -p "$SSHIM_DIR" "$HRUN" "$HRUN2"
73 : > "$SSHIM_PIDLOG" 71 : > "$SSHIM_PIDLOG"
74 cat > "$SSHIM_DIR/ssh" <<'SHIM' 72 ssh_shim_head "$SSHIM_DIR/ssh"
75 #!/bin/sh 73 cat >> "$SSHIM_DIR/ssh" <<'SHIM'
76 [ $# -ge 2 ] || exit 97
77 echo $$ >> "${SSHIM_PIDLOG:?}" 74 echo $$ >> "${SSHIM_PIDLOG:?}"
78 shift 75 shift
79 exec /bin/sh -c "$*" 76 exec /bin/sh -c "$*"
test/e2e_09_hosts.sh
Old New
@@ -808,3 +808,168 @@ PK1PID=""
808 assert_stopped "$PKSOCK2" "$PK2PID" "picker" "$OUT.pk2stop" 808 assert_stopped "$PKSOCK2" "$PK2PID" "picker" "$OUT.pk2stop"
809 PK2PID="" 809 PK2PID=""
810 ok "the picker births on the host a digit names, forgets a host without ending it, adds one back, and gives the keyboard back" 810 ok "the picker births on the host a digit names, forgets a host without ending it, adds one back, and gives the keyboard back"
811
812 # ---- reading a host never starts a daemon; asking does ------------------
813 #
814 # `muxd endpoint` used to auto-start a daemon before answering, and the
815 # wall runs it over ssh once a second per listed host. A listed box with
816 # nothing on it therefore got a daemon — and a shell in session 0 — from a
817 # READ, and a `muxd stop` typed on that box was undone a second later.
818 #
819 # The rule now: a daemon starts when somebody asks. `mux HOST` and the
820 # picker's Enter ask, and answer a refusal by running `muxd start` over the
821 # same ssh; the poller, `mux hosts` and every redial report `unreachable`
822 # and leave the box alone.
823 #
824 # The oracle is the OS, never the daemon: the socket INODE on the far side,
825 # and a walk of /proc for a `muxd run` born under that box's runtime dir. A
826 # daemon asked whether it exists cannot answer no.
827 #
828 # Two fake boxes, because one cannot show that a birth landed on the row
829 # the user chose: the shim reads the host word and hands the remote command
830 # a runtime dir of that box's own, so `muxd`'s default socket resolves to a
831 # different path per host — which is exactly what two machines are.
832 NSHIM="${TMPDIR:-/tmp}/mux-e2e-nostart-shim-$$"
833 defer_rm "$NSHIM"
834 NSTATE="${TMPDIR:-/tmp}/mux-e2e-nostart-state-$$"
835 defer_rm "$NSTATE"
836 NRUNA="${TMPDIR:-/tmp}/mux-e2e-nostart-a-$$"
837 defer_rm "$NRUNA"
838 NRUNB="${TMPDIR:-/tmp}/mux-e2e-nostart-b-$$"
839 defer_rm "$NRUNB"
840 NSOCKA="$NRUNA/muxd.sock"
841 NSOCKB="$NRUNB/muxd.sock"
842 defer_sock "$NSOCKA" "$NSOCKB"
843 NSHIMLOG="$NSHIM/hosts"
844 export NRUNA NRUNB NSHIMLOG
845 mkdir -p "$NSHIM" "$NRUNA" "$NRUNB" "$NSTATE/mux"
846 : > "$NSHIMLOG"
847 # The option loop and the arity guard are `ssh_shim_head`'s; what is this
848 # leg's own is the runtime dir per host word, which is what makes two fake
849 # boxes two machines — `muxd`'s default socket resolves to a different path
850 # on each.
851 ssh_shim_head "$NSHIM/ssh"
852 cat >> "$NSHIM/ssh" <<'SHIM'
853 case "$1" in
854 nostart-a@*) XDG_RUNTIME_DIR="${NRUNA:?}" ;;
855 nostart-b@*) XDG_RUNTIME_DIR="${NRUNB:?}" ;;
856 *) exit 96 ;;
857 esac
858 export XDG_RUNTIME_DIR
859 echo "$1" >> "${NSHIMLOG:?}"
860 shift
861 exec /bin/sh -c "$*"
862 SHIM
863 chmod +x "$NSHIM/ssh"
864 NPATH="$NSHIM:$(cd "$(dirname "$MUXD")" && pwd):$PATH"
865 printf 'nostart-a@127.0.0.1\nnostart-b@127.0.0.1\n' > "$NSTATE/mux/hosts"
866
867 # The /proc walk. `muxd run` and not `muxd` — the transient `muxd endpoint`
868 # and `muxd start` processes share the runtime dir and are not daemons —
869 # and `environ` rather than a name, because a daemon `muxd start` spawns
870 # carries no --sock to grep for: it took the default from this variable.
871 nostart_daemons() {
872 _nd=0
873 for _ne in /proc/[0-9]*/environ; do
874 _np=${_ne#/proc/}
875 _np=${_np%/environ}
876 # The redirect itself is what fails on another user's process (and
877 # on one that exits mid-walk), so the silencing has to wrap the
878 # whole command and not just `tr`.
879 { tr '\0' '\n' < "$_ne"; } 2>/dev/null | grep -qxF "XDG_RUNTIME_DIR=$1" || continue
880 { tr '\0' ' ' < "/proc/$_np/cmdline"; } 2>/dev/null | grep -q 'muxd run' || continue
881 _nd=$((_nd + 1))
882 done
883 echo "$_nd"
884 }
885
886 nostart_empty() { # $1 = runtime dir, $2 = socket, $3 = what was just done
887 _nc=$(nostart_daemons "$1")
888 [ "$_nc" -eq 0 ] || {
889 echo "e2e FAIL: no-start: $3 left $_nc muxd run under $1, want 0"; exit 1; }
890 [ ! -e "$2" ] || {
891 echo "e2e FAIL: no-start: $3 bound $2 — a read started a daemon"; exit 1; }
892 }
893
894 # (1) `mux hosts` READS every listed box. Both rows unreachable, both boxes
895 # untouched.
896 XDG_STATE_HOME="$NSTATE" PATH="$NPATH" timeout 40 "$MUX" hosts > "$OUT.nsh" 2>&1 || {
897 echo "e2e FAIL: no-start: mux hosts failed:"; cat "$OUT.nsh"; exit 1; }
898 [ "$(grep -c '\[unreachable\]' "$OUT.nsh")" = "2" ] || {
899 echo "e2e FAIL: no-start: mux hosts did not call both boxes unreachable:"
900 cat "$OUT.nsh"; exit 1; }
901 nostart_empty "$NRUNA" "$NSOCKA" "mux hosts"
902 nostart_empty "$NRUNB" "$NSOCKB" "mux hosts"
903 # The shim really ran, so the emptiness above is a refusal and not a leg
904 # that never dialled: one line per box, at least.
905 grep -q 'nostart-a@' "$NSHIMLOG" && grep -q 'nostart-b@' "$NSHIMLOG" || {
906 echo "e2e FAIL: no-start: mux hosts dialled neither box:"; cat "$NSHIMLOG"; exit 1; }
907
908 # (2) and (3) in one wall. The wall polls both boxes once a second — three
909 # seconds of that must change nothing — and then Enter on ROW 2 asks, which
910 # must give box B a daemon and box A none. Row 2 and not row 1: a birth
911 # that reached for the first row, or for the focus's host, would pass a
912 # one-row fixture and this one catches it.
913 set +e
914 SHELL=/bin/sh XDG_STATE_HOME="$NSTATE" PATH="$NPATH" timeout 90 "$PTYCLIENT" \
915 --cols 100 --rows 30 --out "$OUT.nscap" --err "$OUT.nscap.err" -- \
916 "$MUX" > "$OUT.nspc" 2>&1 <<'EOF'
917 expect unreachable 25000
918 settle 3000 25000
919 send 2
920 settle 500 15000
921 send \r
922 settle 2000 30000
923 send printf 'ns-born-%s\n' ok\n
924 expect ns-born-ok 25000
925 send \x1cd
926 waitexit 15000
927 EOF
928 RC=$?
929 set -e
930 [ "$RC" -eq 0 ] || {
931 echo "e2e FAIL: no-start: the picker leg exited $RC:"
932 cat "$OUT.nspc"; echo "--- stderr ---"; cat "$OUT.nscap.err"; exit 1; }
933 # Box A was polled for the whole run and never asked for: still nothing.
934 nostart_empty "$NRUNA" "$NSOCKA" "a wall polling it"
935 # Box B was chosen. EXACTLY one daemon — a start per backoff, or a start on
936 # every poll cycle, would show as several.
937 NCB=$(nostart_daemons "$NRUNB")
938 [ "$NCB" -eq 1 ] || {
939 echo "e2e FAIL: no-start: picker Enter left $NCB muxd run under $NRUNB, want 1"; exit 1; }
940 [ -S "$NSOCKB" ] || {
941 echo "e2e FAIL: no-start: picker Enter started nothing on box B ($NSOCKB)"; exit 1; }
942 # ...and it is a real session, named `0` off the daemon's own empty list.
943 dump_session "$NSOCKB" 0 | grep -q 'ns-born-ok' || {
944 echo "e2e FAIL: no-start: box B's session 0 never ran the marker:"
945 "$MUXD" stats --sock "$NSOCKB"; exit 1; }
946
947 # (4) A cold `mux HOST` entry, on the box the wall left alone. One daemon,
948 # and the client said so in its own voice — `muxd start`'s progress reaches
949 # the user's terminal over the same inherited stderr ssh's own diagnostics
950 # ride on.
951 pipe_mux "$OUT.nsa" "$OUT.nsa.err" env SHELL=/bin/sh XDG_STATE_HOME="$NSTATE" \
952 PATH="$NPATH" timeout 60 "$MUX" "nostart-a@127.0.0.1"
953 pipe_send 'printf "ns-cold-%%s\\n" ok\n'
954 await_out "$OUT.nsa" "ns-cold-ok" "no-start: the cold entry never served a session"
955 pipe_detach "no-start cold entry client"
956 NCA=$(nostart_daemons "$NRUNA")
957 [ "$NCA" -eq 1 ] || {
958 echo "e2e FAIL: no-start: a cold mux HOST left $NCA muxd run under $NRUNA, want 1"
959 cat "$OUT.nsa.err"; exit 1; }
960 [ -S "$NSOCKA" ] || {
961 echo "e2e FAIL: no-start: the cold entry started nothing on box A"; exit 1; }
962 [ "$(grep -c '^muxd: starting' "$OUT.nsa.err")" = "1" ] || {
963 echo "e2e FAIL: no-start: the cold entry did not run muxd start exactly once:"
964 cat "$OUT.nsa.err"; exit 1; }
965 # ...and box A goes down by the daemon's own verb, which makes the teardown
966 # one more assertion rather than a daemon handed to the trap: what a `muxd
967 # stop` owes on a box somebody started is exactly what an untouched box
968 # already shows — no `muxd run` under that runtime dir, and no socket.
969 # `assert_stopped` is not usable here: it wants a pid, and this daemon was
970 # forked by an ssh shim on the far side of a client.
971 "$MUXD" stop --sock "$NSOCKA" > "$OUT.nsastop" 2>&1 || {
972 echo "e2e FAIL: no-start: muxd stop on box A exited nonzero:"
973 cat "$OUT.nsastop"; exit 1; }
974 nostart_empty "$NRUNA" "$NSOCKA" "muxd stop"
975 ok "reading a host leaves it alone; a picker Enter and a cold mux HOST each start one daemon"
test/e2e_lib.sh
Old New
@@ -464,6 +464,36 @@ repaints() {
464 grep -a -F -o "$_clr" "$1" 2>/dev/null | wc -l 464 grep -a -F -o "$_clr" "$1" 2>/dev/null | wc -l
465 } 465 }
466 466
467 # ssh_shim_head FILE — the prologue every fake ssh in this suite needs,
468 # written fresh over FILE; the caller appends its own body and chmods.
469 #
470 # Real ssh takes options before the host word, and the client spells some:
471 # `handoff.recipeFor` adds `-o BatchMode=yes` to every recipe that must not
472 # ask, and the wall's poller runs those. A shim that read `$1` as the host
473 # would exec `BatchMode=yes ...` and fail exactly like an unreachable box —
474 # the state the no-start legs assert, so the fixture would be lying in the
475 # direction of a pass. The arity guard is the other half: `ssh HOST` with
476 # no command leaves `"$@"` empty after the shift, and a bare exec there is
477 # a no-op that exits 0 — silence shaped like success. 97 converts that into
478 # a visible "no announce".
479 #
480 # Here, not per group, because the coupling was invisible: the option loop
481 # is correct in one shim and absent in the other only because those legs
482 # happen to drive asking recipes, and nothing said so.
483 ssh_shim_head() {
484 cat > "$1" <<'SHIMHEAD'
485 #!/bin/sh
486 while [ $# -gt 0 ]; do
487 case "$1" in
488 -o) shift 2 ;;
489 -*) shift ;;
490 *) break ;;
491 esac
492 done
493 [ $# -ge 2 ] || exit 97
494 SHIMHEAD
495 }
496
467 # rail_cols FILE — the column of every vertical rail in a capture, 497 # rail_cols FILE — the column of every vertical rail in a capture,
468 # ascending and deduplicated. A rail paints as a CUP followed by reverse 498 # ascending and deduplicated. A rail paints as a CUP followed by reverse
469 # video, which is how a label bar starts too, so column 1 is a bar and 499 # video, which is how a label bar starts too, so column 1 is a bar and