a73x

37d38ee4

refactor: the trap reads a register, not four hand-kept lists

a73x   2026-08-26 18:34

Commit message
refactor: the trap reads a register, not four hand-kept lists

cleanup() named every daemon, socket and capture the suite makes: a kill
list of 137 $DnnPID variables, a stop list, a 350-name rm list and the
backstop beside it. Adding a leg meant four edits somewhere else, and the
edits got missed — 102 captures were absent from those lists by
2026-08-19, enough that `make soak` refused to start.

Now the statement that creates a thing registers it. start_daemon spawns,
registers the pid and the socket, and waits for the bind in one call;
defer_kill, defer_sock and defer_rm take everything a leg makes by hand.
Captures are not registered at all: every one is spelled from $OUT, and
the trap removes that whole set by the pattern leak_sweep already reads it
by — a pattern cannot forget a leg.

cleanup() goes from 542 lines to 117, and the 82 declarations that existed
only so the trap could name them under `set -u` go with it. The trap's
plain `kill` becomes softkill on the way past: a signal sent to a kcov
tracer is dropped, which is why the hub leg had to learn that already.

The long-lived daemon becomes $D1PID. start_daemon owns $DPID, and the
scenario 5,000 lines below that asks whether the tear killed the daemon
needs a name no later spawn can overwrite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

test/e2e.sh
Old New
@@ -94,16 +94,121 @@ done
94 echo " daemon can still report a command's real exit code" 94 echo " daemon can still report a command's real exit code"
95 exit 1; } 95 exit 1; }
96 96
97 # ---- the cleanup registry ---------------------------------------------
98 # Every process and every artifact a leg creates is REGISTERED where it is
99 # created, and the EXIT trap walks the registers. Before this, cleanup()
100 # carried four hand-maintained lists naming ~137 $SOCKnn/$DnnPID variables,
101 # and a leg added without all four edits leaked: 102 captures were missing
102 # from them by 2026-08-19, enough that `make soak` refused to start. A list
103 # kept somewhere other than the code it describes is a list that goes
104 # stale, so there is no list — registering IS the statement that creates
105 # the thing.
106 #
107 # Captures are the exception, and deliberately so: every one of them is
108 # spelled from $OUT, and the trap removes that whole set by the same
109 # pattern leak_sweep already reads it by. A pattern cannot forget a leg,
110 # and forgetting was the failure. What has to be registered is what no
111 # pattern can find — pids, and the artifacts spelled somewhere other than
112 # $OUT.
113 #
114 # Newline-joined strings rather than arrays: the shebang is /bin/sh and the
115 # build's shell gate lints this file as POSIX sh. No path here can hold a
116 # newline (every one is spelled from $TMPDIR, $OUT and $$ in this file),
117 # and cleanup sets IFS to a newline before walking them, so a $TMPDIR with
118 # a space in it still reads as one path.
119 E2E_KILL=""
120 E2E_SOCK=""
121 E2E_RM=""
122
123 # defer_kill PID... — a process the trap must end. Newest first, so
124 # teardown runs in the reverse of creation order and a client is signalled
125 # before the daemon it is attached to.
126 #
127 # An `if` rather than `[ -n "$_dk" ] && ...`, for wait_sock's reason: a
128 # false guard as the last command in a function becomes that function's
129 # exit status, and under `set -e` registering an empty pid would abort the
130 # caller.
131 defer_kill() {
132 for _dk in "$@"; do
133 if [ -n "$_dk" ]; then
134 # Already registered is not an error: pipe_mux names the same
135 # FIFO on every call, and a leg that starts two daemons on one
136 # path registers it twice. Registering twice must cost nothing,
137 # or callers start reasoning about who registered first.
138 case "
139 $E2E_KILL" in
140 *"
141 $_dk
142 "*) continue ;;
143 esac
144 E2E_KILL="$_dk
145 $E2E_KILL"
146 fi
147 done
148 }
149
150 # defer_sock PATH... — a path a daemon may be listening on. The trap asks
151 # `muxd stop` there before it kills anything and long before it unlinks the
152 # path: a daemon this shell never forked — one a proxy, `muxd start` or the
153 # handoff spawned — has no pid here, and the socket is the only handle
154 # there is.
155 defer_sock() {
156 for _ds in "$@"; do
157 if [ -n "$_ds" ]; then
158 # Already registered is not an error: pipe_mux names the same
159 # FIFO on every call, and a leg that starts two daemons on one
160 # path registers it twice. Registering twice must cost nothing,
161 # or callers start reasoning about who registered first.
162 case "
163 $E2E_SOCK" in
164 *"
165 $_ds
166 "*) continue ;;
167 esac
168 E2E_SOCK="$_ds
169 $E2E_SOCK"
170 fi
171 done
172 }
173
174 # defer_rm PATH... — an artifact the trap must remove: a key, a generated
175 # script, a private HOME, a state home. Registered at the line that spells
176 # the path, so the two can never drift apart. Files go out through the leak
177 # sweep's banking; directories are removed whole.
178 defer_rm() {
179 for _dr in "$@"; do
180 if [ -n "$_dr" ]; then
181 # Already registered is not an error: pipe_mux names the same
182 # FIFO on every call, and a leg that starts two daemons on one
183 # path registers it twice. Registering twice must cost nothing,
184 # or callers start reasoning about who registered first.
185 case "
186 $E2E_RM" in
187 *"
188 $_dr
189 "*) continue ;;
190 esac
191 E2E_RM="$_dr
192 $E2E_RM"
193 fi
194 done
195 }
196
197
97 SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" 198 SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock"
199 defer_sock "$SOCK"
98 OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" 200 OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$"
99 # M10: hermetic XDG homes. Key-default scenarios must see OUR key or none, 201 # M10: hermetic XDG homes. Key-default scenarios must see OUR key or none,
100 # never the developer's real ~/.config/mux/key. 202 # never the developer's real ~/.config/mux/key.
101 XDG_CONFIG_HOME="${TMPDIR:-/tmp}/mux-e2e-cfg-$$" 203 XDG_CONFIG_HOME="${TMPDIR:-/tmp}/mux-e2e-cfg-$$"
204 defer_rm "$XDG_CONFIG_HOME"
102 XDG_STATE_HOME="${TMPDIR:-/tmp}/mux-e2e-state-$$" 205 XDG_STATE_HOME="${TMPDIR:-/tmp}/mux-e2e-state-$$"
206 defer_rm "$XDG_STATE_HOME"
103 # M14: and the same for the handoff's per-host cache. It holds a KEY, and 207 # M14: and the same for the handoff's per-host cache. It holds a KEY, and
104 # the M14 scenarios both read and poison it — neither of which may ever 208 # the M14 scenarios both read and poison it — neither of which may ever
105 # touch the developer's real ~/.cache/mux. 209 # touch the developer's real ~/.cache/mux.
106 XDG_CACHE_HOME="${TMPDIR:-/tmp}/mux-e2e-cache-$$" 210 XDG_CACHE_HOME="${TMPDIR:-/tmp}/mux-e2e-cache-$$"
211 defer_rm "$XDG_CACHE_HOME"
107 export XDG_CONFIG_HOME XDG_STATE_HOME XDG_CACHE_HOME 212 export XDG_CONFIG_HOME XDG_STATE_HOME XDG_CACHE_HOME
108 # ...and the same argument for $SHELL, which is not an XDG home but is read 213 # ...and the same argument for $SHELL, which is not an XDG home but is read
109 # the same way: every daemon this suite AUTO-STARTS gets no --shell flag and 214 # the same way: every daemon this suite AUTO-STARTS gets no --shell flag and
@@ -132,62 +237,63 @@ unset MUX_KEY_FILE
132 # asserting against the developer's keyring. The legs that want an agent 237 # asserting against the developer's keyring. The legs that want an agent
133 # export one per command, at a path this file made. 238 # export one per command, at a path this file made.
134 unset SSH_AUTH_SOCK 239 unset SSH_AUTH_SOCK
135 # Second daemon, used only by the M7 abort scenario; declared here so the 240 # Second daemon, used only by the M7 abort scenario.
136 # trap below can reference them under `set -u` before they are ever started.
137 SOCK2="${TMPDIR:-/tmp}/muxd-e2e-abort-$$.sock" 241 SOCK2="${TMPDIR:-/tmp}/muxd-e2e-abort-$$.sock"
138 D2PID="" 242 defer_sock "$SOCK2"
139 # Third daemon, for the restart scenario: it gets killed and started again on 243 # Third daemon, for the restart scenario: it gets killed and started again on
140 # the same path, so it cannot share the long-lived one. 244 # the same path, so it cannot share the long-lived one.
141 SOCK3="${TMPDIR:-/tmp}/muxd-e2e-restart-$$.sock" 245 SOCK3="${TMPDIR:-/tmp}/muxd-e2e-restart-$$.sock"
142 D3PID="" 246 defer_sock "$SOCK3"
143 # Fourth daemon, for the M8 --quic scenario: it is the only one holding a UDP 247 # Fourth daemon, for the M8 --quic scenario: it is the only one holding a UDP
144 # port, so it gets its own path rather than sharing the long-lived one. 248 # port, so it gets its own path rather than sharing the long-lived one.
145 SOCK4="${TMPDIR:-/tmp}/muxd-e2e-quic-$$.sock" 249 SOCK4="${TMPDIR:-/tmp}/muxd-e2e-quic-$$.sock"
250 defer_sock "$SOCK4" "$SOCK4.nokey" "$SOCK4.second"
146 QKEY="${TMPDIR:-/tmp}/mux-e2e-key-$$" 251 QKEY="${TMPDIR:-/tmp}/mux-e2e-key-$$"
147 D4PID="" 252 defer_rm "$QKEY" "$QKEY.wrong" "$QKEY.bad"
148 # A port out of the way of the ephemeral range, made per-run so two suites can 253 # A port out of the way of the ephemeral range, made per-run so two suites can
149 # overlap. Collisions surface as a loud bind failure, never as a silent pass. 254 # overlap. Collisions surface as a loud bind failure, never as a silent pass.
150 QPORT=$(( 21000 + ($$ % 4000) )) 255 QPORT=$(( 21000 + ($$ % 4000) ))
151 # M9 prediction: each scenario needs a session whose LINE DISCIPLINE it 256 # M9 prediction: each scenario needs a session whose LINE DISCIPLINE it
152 # controls, so they cannot share the long-lived /bin/sh daemon. 257 # controls, so they cannot share the long-lived /bin/sh daemon.
153 SOCK5="${TMPDIR:-/tmp}/muxd-e2e-pred-$$.sock" 258 SOCK5="${TMPDIR:-/tmp}/muxd-e2e-pred-$$.sock"
154 D5PID="" 259 defer_sock "$SOCK5"
155 SOCK6="${TMPDIR:-/tmp}/muxd-e2e-pw-$$.sock" 260 SOCK6="${TMPDIR:-/tmp}/muxd-e2e-pw-$$.sock"
156 D6PID="" 261 defer_sock "$SOCK6"
157 SOCK7="${TMPDIR:-/tmp}/muxd-e2e-raw-$$.sock" 262 SOCK7="${TMPDIR:-/tmp}/muxd-e2e-raw-$$.sock"
158 D7PID="" 263 defer_sock "$SOCK7"
159 PWSH="${TMPDIR:-/tmp}/mux-e2e-pw-$$.sh" 264 PWSH="${TMPDIR:-/tmp}/mux-e2e-pw-$$.sh"
265 defer_rm "$PWSH"
160 # M10 key-resolution daemons. Two, not one: the first scenario's client 266 # M10 key-resolution daemons. Two, not one: the first scenario's client
161 # detaches and its daemon is killed, and the second must not be sharing 267 # detaches and its daemon is killed, and the second must not be sharing
162 # either. Ports in bands of their own so a concurrent suite cannot collide. 268 # either. Ports in bands of their own so a concurrent suite cannot collide.
163 SOCK9="${TMPDIR:-/tmp}/muxd-e2e-envkey-$$.sock" 269 SOCK9="${TMPDIR:-/tmp}/muxd-e2e-envkey-$$.sock"
164 D9PID="" 270 defer_sock "$SOCK9"
165 SOCK10="${TMPDIR:-/tmp}/muxd-e2e-flagwins-$$.sock" 271 SOCK10="${TMPDIR:-/tmp}/muxd-e2e-flagwins-$$.sock"
166 D10PID="" 272 defer_sock "$SOCK10"
167 QPORT2=$(( 26000 + ($$ % 4000) )) 273 QPORT2=$(( 26000 + ($$ % 4000) ))
168 QPORT3=$(( 31000 + ($$ % 4000) )) 274 QPORT3=$(( 31000 + ($$ % 4000) ))
169 # M10 `muxd start`. These daemons are spawned DETACHED, so the suite never 275 # M10 `muxd start`. These daemons are spawned DETACHED, so the suite never
170 # holds their pids as shell jobs — it reads them off the up-line and kills 276 # holds their pids as shell jobs — it reads them off the up-line and kills
171 # by that tracked pid, never by name. 277 # by that tracked pid, never by name.
172 SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock" 278 SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock"
279 defer_sock "$SOCK8"
173 SOCK8T="${TMPDIR:-/tmp}/muxd-e2e-trunc-$$.sock" 280 SOCK8T="${TMPDIR:-/tmp}/muxd-e2e-trunc-$$.sock"
281 defer_sock "$SOCK8T"
174 SOCK11="${TMPDIR:-/tmp}/muxd-e2e-goal-$$.sock" 282 SOCK11="${TMPDIR:-/tmp}/muxd-e2e-goal-$$.sock"
283 defer_sock "$SOCK11"
175 QPORT4=$(( 36000 + ($$ % 4000) )) 284 QPORT4=$(( 36000 + ($$ % 4000) ))
176 SPID=""
177 TPID=""
178 GPID=""
179 # M12 pty scenarios. Each needs a daemon whose grid size it owns: tp2 285 # M12 pty scenarios. Each needs a daemon whose grid size it owns: tp2
180 # resizes the grid twice and tp1's session is a scrollback-generating 286 # resizes the grid twice and tp1's session is a scrollback-generating
181 # wrapper, so neither can share the long-lived /bin/sh daemon. 287 # wrapper, so neither can share the long-lived /bin/sh daemon.
182 SOCK12="${TMPDIR:-/tmp}/muxd-e2e-tp2-$$.sock" 288 SOCK12="${TMPDIR:-/tmp}/muxd-e2e-tp2-$$.sock"
183 D12PID="" 289 defer_sock "$SOCK12"
184 SOCK13="${TMPDIR:-/tmp}/muxd-e2e-tp1-$$.sock" 290 SOCK13="${TMPDIR:-/tmp}/muxd-e2e-tp1-$$.sock"
185 D13PID="" 291 defer_sock "$SOCK13"
186 # tp1's session: a wrapper that fills the scrollback and then becomes cat. 292 # tp1's session: a wrapper that fills the scrollback and then becomes cat.
187 TP1SH="${TMPDIR:-/tmp}/mux-e2e-tp1-$$.sh" 293 TP1SH="${TMPDIR:-/tmp}/mux-e2e-tp1-$$.sh"
294 defer_rm "$TP1SH"
188 # tp1 runs its fixture in the BACKGROUND — the tear has to happen while the 295 # tp1 runs its fixture in the BACKGROUND — the tear has to happen while the
189 # script is mid-flight — so its pid is tracked and killed like a daemon's. 296 # script is mid-flight — so its pid is tracked and killed like a daemon's.
190 TP1PID=""
191 # M13 auto-start + stop. Two paths: the proxy arc's daemon is spawned BY 297 # M13 auto-start + stop. Two paths: the proxy arc's daemon is spawned BY
192 # the proxy, the pty leg's by local mux under the M12 fixture. Both are 298 # the proxy, the pty leg's by local mux under the M12 fixture. Both are
193 # torn down by `muxd stop` — the verb under test is also the cleanup, and 299 # torn down by `muxd stop` — the verb under test is also the cleanup, and
@@ -195,27 +301,33 @@ TP1PID=""
195 # each pid is read off the up-line its own spawner printed, which is the 301 # each pid is read off the up-line its own spawner printed, which is the
196 # only handle that can never name a bystander. 302 # only handle that can never name a bystander.
197 SOCK14="${TMPDIR:-/tmp}/muxd-e2e-astart-$$.sock" 303 SOCK14="${TMPDIR:-/tmp}/muxd-e2e-astart-$$.sock"
304 defer_sock "$SOCK14"
198 SOCK15="${TMPDIR:-/tmp}/muxd-e2e-aspty-$$.sock" 305 SOCK15="${TMPDIR:-/tmp}/muxd-e2e-aspty-$$.sock"
199 APID="" 306 defer_sock "$SOCK15"
200 PAPID=""
201 # M14 ssh→QUIC handoff. The shim that stands in for ssh (built where the 307 # M14 ssh→QUIC handoff. The shim that stands in for ssh (built where the
202 # scenarios run, declared here so the trap can reach it), and a RUNTIME DIR 308 # scenarios run), and a RUNTIME DIR
203 # per daemon: the handoff dials the DEFAULT socket on the far side, and the 309 # per daemon: the handoff dials the DEFAULT socket on the far side, and the
204 # far side is this box, so each scenario's default socket has to be one of 310 # far side is this box, so each scenario's default socket has to be one of
205 # ours rather than the operator's. Two daemons, no more — the one a cold 311 # ours rather than the operator's. Two daemons, no more — the one a cold
206 # attach auto-starts, and the key-mismatch one that cannot be shared 312 # attach auto-starts, and the key-mismatch one that cannot be shared
207 # because it holds a key nothing else on this box has. 313 # because it holds a key nothing else on this box has.
208 SSHIM_DIR="${TMPDIR:-/tmp}/muxd-e2e-sshim-$$" 314 SSHIM_DIR="${TMPDIR:-/tmp}/muxd-e2e-sshim-$$"
315 defer_rm "$SSHIM_DIR"
209 SSHIM_PIDLOG="$SSHIM_DIR/pids" 316 SSHIM_PIDLOG="$SSHIM_DIR/pids"
210 export SSHIM_PIDLOG 317 export SSHIM_PIDLOG
211 HRUN="${TMPDIR:-/tmp}/mux-e2e-hrun-$$" 318 HRUN="${TMPDIR:-/tmp}/mux-e2e-hrun-$$"
319 defer_rm "$HRUN"
212 HRUN2="${TMPDIR:-/tmp}/mux-e2e-hrun2-$$" 320 HRUN2="${TMPDIR:-/tmp}/mux-e2e-hrun2-$$"
321 defer_rm "$HRUN2"
213 SOCK16="$HRUN/muxd.sock" 322 SOCK16="$HRUN/muxd.sock"
214 SOCK17="$HRUN2/muxd.sock" 323 SOCK17="$HRUN2/muxd.sock"
324 defer_sock "$SOCK16" "$SOCK17"
215 HKEY="${TMPDIR:-/tmp}/mux-e2e-hkey-$$" 325 HKEY="${TMPDIR:-/tmp}/mux-e2e-hkey-$$"
326 defer_rm "$HKEY"
216 # A config home that is a FILE: `muxd endpoint` cannot create a key under 327 # A config home that is a FILE: `muxd endpoint` cannot create a key under
217 # it and cannot find one, which is the announce-none scenario's lever. 328 # it and cannot find one, which is the announce-none scenario's lever.
218 HCFGBAD="${TMPDIR:-/tmp}/mux-e2e-hnokey-$$" 329 HCFGBAD="${TMPDIR:-/tmp}/mux-e2e-hnokey-$$"
330 defer_rm "$HCFGBAD"
219 # Bands of their own, like every other port here, so a concurrent suite 331 # Bands of their own, like every other port here, so a concurrent suite
220 # cannot collide — and BELOW the ephemeral range (32768–60999 here), which 332 # cannot collide — and BELOW the ephemeral range (32768–60999 here), which
221 # for HDEADPORT is not housekeeping but the scenario's correctness. It is 333 # for HDEADPORT is not housekeeping but the scenario's correctness. It is
@@ -225,47 +337,38 @@ HCFGBAD="${TMPDIR:-/tmp}/mux-e2e-hnokey-$$"
225 # quietly asserts a heal that never had to happen. 337 # quietly asserts a heal that never had to happen.
226 HQPORT=$(( 11000 + ($$ % 4000) )) 338 HQPORT=$(( 11000 + ($$ % 4000) ))
227 HDEADPORT=$(( 16000 + ($$ % 4000) )) 339 HDEADPORT=$(( 16000 + ($$ % 4000) ))
228 HAPID="" 340 # The pipe_mux client in flight, and its FIFO. Both are registered by
229 HDPID="" 341 # pipe_mux itself; its `timeout` stays the primary guarantee and the trap
230 # The pipe_mux client in flight, and its FIFO. Bounded by its own 342 # is the backstop for a scenario that exits between pipe_mux and
231 # `timeout`, which stays the primary guarantee; this is the trap's backstop 343 # pipe_detach, leaving a client the shell would otherwise orphan.
232 # for a scenario that exits between pipe_mux and pipe_detach, leaving a
233 # client the shell would otherwise orphan. Declared here so the trap can
234 # read them under `set -u`.
235 PIPE_PID=""
236 PIPE_IN=""
237 # M-web. Three daemons (the passivity pin's, the hub-basic one, and the 344 # M-web. Three daemons (the passivity pin's, the hub-basic one, and the
238 # tear scenario's two incarnations share SOCK20), two hub processes, and 345 # tear scenario's two incarnations share SOCK20), two hub processes, and
239 # a port band of its own following the 5000-spacing convention — INSIDE 346 # a port band of its own following the 5000-spacing convention — INSIDE
240 # the ephemeral range, which is fine for ports we bind (only provably-DEAD 347 # the ephemeral range, which is fine for ports we bind (only provably-DEAD
241 # ports must stay below 32768; see HDEADPORT above). 348 # ports must stay below 32768; see HDEADPORT above).
242 SOCK18="${TMPDIR:-/tmp}/muxd-e2e-web-a-$$.sock" 349 SOCK18="${TMPDIR:-/tmp}/muxd-e2e-web-a-$$.sock"
350 defer_sock "$SOCK18"
243 SOCK19="${TMPDIR:-/tmp}/muxd-e2e-web-b-$$.sock" 351 SOCK19="${TMPDIR:-/tmp}/muxd-e2e-web-b-$$.sock"
352 defer_sock "$SOCK19"
244 SOCK20="${TMPDIR:-/tmp}/muxd-e2e-web-c-$$.sock" 353 SOCK20="${TMPDIR:-/tmp}/muxd-e2e-web-c-$$.sock"
354 defer_sock "$SOCK20"
245 WPORT=$(( 41000 + ($$ % 4000) )) 355 WPORT=$(( 41000 + ($$ % 4000) ))
246 WPORT2=$(( 46000 + ($$ % 4000) )) 356 WPORT2=$(( 46000 + ($$ % 4000) ))
247 D14PID=""
248 D15PID=""
249 D16PID=""
250 D17PID=""
251 W1PID=""
252 W2PID=""
253 WCLIPID=""
254 # M18. Two daemons — the CLI multi-session block's and the wall's — and one 357 # M18. Two daemons — the CLI multi-session block's and the wall's — and one
255 # more hub on the next 5000-spaced port. Only two sockets for four-and-some 358 # more hub on the next 5000-spaced port. Only two sockets for four-and-some
256 # sessions, which is the whole point of the milestone: sessions are named on 359 # sessions, which is the whole point of the milestone: sessions are named on
257 # one socket, not spread across one socket each. 360 # one socket, not spread across one socket each.
258 SOCK21="${TMPDIR:-/tmp}/muxd-e2e-m18-$$.sock" 361 SOCK21="${TMPDIR:-/tmp}/muxd-e2e-m18-$$.sock"
362 defer_sock "$SOCK21"
259 SOCK22="${TMPDIR:-/tmp}/muxd-e2e-m18web-$$.sock" 363 SOCK22="${TMPDIR:-/tmp}/muxd-e2e-m18web-$$.sock"
364 defer_sock "$SOCK22"
260 WPORT3=$(( 51000 + ($$ % 4000) )) 365 WPORT3=$(( 51000 + ($$ % 4000) ))
261 D18PID=""
262 D19PID=""
263 W3PID=""
264 # ...and a third, the only M18 daemon holding a UDP port: it serves the 366 # ...and a third, the only M18 daemon holding a UDP port: it serves the
265 # session socket AND a QUIC listener at once, which is what lets one block 367 # session socket AND a QUIC listener at once, which is what lets one block
266 # ask whether a session name means the same session on either transport. 368 # ask whether a session name means the same session on either transport.
267 # Next 5000-spaced band after WPORT3. 369 # Next 5000-spaced band after WPORT3.
268 SOCK23="${TMPDIR:-/tmp}/muxd-e2e-m18quic-$$.sock" 370 SOCK23="${TMPDIR:-/tmp}/muxd-e2e-m18quic-$$.sock"
371 defer_sock "$SOCK23"
269 QPORT5=$(( 56000 + ($$ % 4000) )) 372 QPORT5=$(( 56000 + ($$ % 4000) ))
270 # A key of its own rather than the M8 block's $QKEY: that one is rm_swept 373 # A key of its own rather than the M8 block's $QKEY: that one is rm_swept
271 # the moment the M8 scenarios finish (see the sweep after the key-source 374 # the moment the M8 scenarios finish (see the sweep after the key-source
@@ -273,7 +376,7 @@ QPORT5=$(( 56000 + ($$ % 4000) ))
273 # deleted file would make this block's daemon fail to bind for a reason 376 # deleted file would make this block's daemon fail to bind for a reason
274 # that has nothing to do with sessions. 377 # that has nothing to do with sessions.
275 M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$" 378 M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$"
276 D20PID="" 379 defer_rm "$M18KEY"
277 380
278 # The bell leg's own daemon. A socket of its own is load-bearing rather than 381 # The bell leg's own daemon. A socket of its own is load-bearing rather than
279 # tidiness, and the argument is at the leg itself: that scenario counts BEL 382 # tidiness, and the argument is at the leg itself: that scenario counts BEL
@@ -282,7 +385,7 @@ D20PID=""
282 # attaching there is told so as `ESC]0;...BEL` — which would land in the 385 # attaching there is told so as `ESC]0;...BEL` — which would land in the
283 # capture and make the count assert something other than what it says. 386 # capture and make the count assert something other than what it says.
284 SOCK24="${TMPDIR:-/tmp}/muxd-e2e-bell-$$.sock" 387 SOCK24="${TMPDIR:-/tmp}/muxd-e2e-bell-$$.sock"
285 D21PID="" 388 defer_sock "$SOCK24"
286 389
287 # The dynamic-wall leg: one daemon, one hub restarted three times, and a 390 # The dynamic-wall leg: one daemon, one hub restarted three times, and a
288 # STATE HOME of its own on the 61000 band — the last 5000-spaced one that 391 # STATE HOME of its own on the 61000 band — the last 5000-spaced one that
@@ -293,82 +396,78 @@ D21PID=""
293 # it would be some other block's. Never the developer's ~/.local/state either; that is 396 # it would be some other block's. Never the developer's ~/.local/state either; that is
294 # why every muxweb here is spawned with the override in front of it. 397 # why every muxweb here is spawned with the override in front of it.
295 SOCK25="${TMPDIR:-/tmp}/muxd-e2e-dynwall-$$.sock" 398 SOCK25="${TMPDIR:-/tmp}/muxd-e2e-dynwall-$$.sock"
399 defer_sock "$SOCK25"
296 WPORT4=$(( 61000 + ($$ % 4000) )) 400 WPORT4=$(( 61000 + ($$ % 4000) ))
297 DWSTATE="${TMPDIR:-/tmp}/mux-e2e-dynwall-state-$$" 401 DWSTATE="${TMPDIR:-/tmp}/mux-e2e-dynwall-state-$$"
298 D22PID="" 402 defer_rm "$DWSTATE"
299 W4PID=""
300 403
301 # The CLI wall (`mux wall`): its own daemon, so its two sessions can't be 404 # The CLI wall (`mux wall`): its own daemon, so its two sessions can't be
302 # confused with any other block's. 405 # confused with any other block's.
303 SOCK26="${TMPDIR:-/tmp}/muxd-e2e-cliwall-$$.sock" 406 SOCK26="${TMPDIR:-/tmp}/muxd-e2e-cliwall-$$.sock"
304 D23PID="" 407 defer_sock "$SOCK26"
305 408
306 # The prefix chord (Ctrl-\ as a command key): its own daemon, so a chord 409 # The prefix chord (Ctrl-\ as a command key): its own daemon, so a chord
307 # that fails to detach cannot strand another block's session. 410 # that fails to detach cannot strand another block's session.
308 SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock" 411 SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock"
309 D24PID="" 412 defer_sock "$SOCK27"
310 413
311 # Ctrl-\ c (create a session and switch to it): its own daemon, because the 414 # Ctrl-\ c (create a session and switch to it): its own daemon, because the
312 # assertion is about which sessions EXIST — another block's session on the 415 # assertion is about which sessions EXIST — another block's session on the
313 # same daemon would change the name the chord picks. 416 # same daemon would change the name the chord picks.
314 SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock" 417 SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock"
315 D25PID="" 418 defer_sock "$SOCK28"
316 419
317 # Ctrl-\ n / Ctrl-\ p (step around the ring): its own daemon for the same 420 # Ctrl-\ n / Ctrl-\ p (step around the ring): its own daemon for the same
318 # reason as SOCK28, and more sharply — the ring IS the list of sessions on 421 # reason as SOCK28, and more sharply — the ring IS the list of sessions on
319 # one daemon, so a stray session from another block would change where a 422 # one daemon, so a stray session from another block would change where a
320 # step lands. 423 # step lands.
321 SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock" 424 SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock"
322 D26PID="" 425 defer_sock "$SOCK29"
323
324 # The zoom-era wall legs these belonged to are gone, but the cleanup trap
325 # cleanup trap still names every DxxPID and SOCKxx in bare form and the
326 # script runs under `set -u`. Bound empty so an unbound-variable abort never
327 # replaces a real failure's exit code; the legs that filled them are deleted.
328 SOCK30="" SOCK32="" SOCK38="" SOCK43="" SOCK44=""
329 M4STATE="" ZHASH="" ZHBSH="" ZHFLAG="" ZHSCRIPT=""
330 D27PID="" D29PID="" D35PID="" D40PID="" D41PID=""
331 426
332 # M5 (the self-attach refusal): its own daemon because the assertion reads 427 # M5 (the self-attach refusal): its own daemon because the assertion reads
333 # the SOCKET PATH out of a session shell's environment and compares it to 428 # the SOCKET PATH out of a session shell's environment and compares it to
334 # the one this block started — a shared daemon would make the comparison a 429 # the one this block started — a shared daemon would make the comparison a
335 # tautology about whichever block bound first. 430 # tautology about whichever block bound first.
336 SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock" 431 SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock"
337 D28PID="" 432 defer_sock "$SOCK31"
338 433
339 # The wheel pair. Each needs a scrollback of its own to scroll (or to prove 434 # The wheel pair. Each needs a scrollback of its own to scroll (or to prove
340 # it did not), which the long-lived /bin/sh daemon has no way to hold still, 435 # it did not), which the long-lived /bin/sh daemon has no way to hold still,
341 # and the second one needs a session that ASKS for the mouse — a mode that 436 # and the second one needs a session that ASKS for the mouse — a mode that
342 # would follow every other scenario sharing the daemon around. 437 # would follow every other scenario sharing the daemon around.
343 SOCK33="${TMPDIR:-/tmp}/muxd-e2e-wheel-$$.sock" 438 SOCK33="${TMPDIR:-/tmp}/muxd-e2e-wheel-$$.sock"
344 D30PID="" 439 defer_sock "$SOCK33"
345 WHEELSH="${TMPDIR:-/tmp}/mux-e2e-wheel-$$.sh" 440 WHEELSH="${TMPDIR:-/tmp}/mux-e2e-wheel-$$.sh"
441 defer_rm "$WHEELSH"
346 SOCK34="${TMPDIR:-/tmp}/muxd-e2e-appmouse-$$.sock" 442 SOCK34="${TMPDIR:-/tmp}/muxd-e2e-appmouse-$$.sock"
347 D31PID="" 443 defer_sock "$SOCK34"
348 MOUSESH="${TMPDIR:-/tmp}/mux-e2e-appmouse-$$.sh" 444 MOUSESH="${TMPDIR:-/tmp}/mux-e2e-appmouse-$$.sh"
445 defer_rm "$MOUSESH"
349 # The alternate-screen half of the wheel: its own daemon because the 446 # The alternate-screen half of the wheel: its own daemon because the
350 # session under test is a PAGER holding the alt screen for its whole life, 447 # session under test is a PAGER holding the alt screen for its whole life,
351 # which no other scenario could share a grid with. And the no-terminal 448 # which no other scenario could share a grid with. And the no-terminal
352 # half, whose session is a bare `cat` so that what reaches the pty is 449 # half, whose session is a bare `cat` so that what reaches the pty is
353 # echoed back into the grid the assertion reads. 450 # echoed back into the grid the assertion reads.
354 SOCK35="${TMPDIR:-/tmp}/muxd-e2e-pager-$$.sock" 451 SOCK35="${TMPDIR:-/tmp}/muxd-e2e-pager-$$.sock"
355 D32PID="" 452 defer_sock "$SOCK35"
356 LESSSH="${TMPDIR:-/tmp}/mux-e2e-pager-$$.sh" 453 LESSSH="${TMPDIR:-/tmp}/mux-e2e-pager-$$.sh"
454 defer_rm "$LESSSH"
357 LESSDATA="${TMPDIR:-/tmp}/mux-e2e-pager-$$.txt" 455 LESSDATA="${TMPDIR:-/tmp}/mux-e2e-pager-$$.txt"
456 defer_rm "$LESSDATA"
358 SOCK36="${TMPDIR:-/tmp}/muxd-e2e-pipestdin-$$.sock" 457 SOCK36="${TMPDIR:-/tmp}/muxd-e2e-pipestdin-$$.sock"
359 D33PID="" 458 defer_sock "$SOCK36"
360 459
361 # The focus-skip leg, on a daemon of its own for the grid-size reason: it 460 # The focus-skip leg, on a daemon of its own for the grid-size reason: it
362 # asserts on grid sizes and on how many clients a session ever had at once, 461 # asserts on grid sizes and on how many clients a session ever had at once,
363 # and a session another block created — or another block's client sitting 462 # and a session another block created — or another block's client sitting
364 # on this daemon — would make both numbers say nothing. 463 # on this daemon — would make both numbers say nothing.
365 SOCK37="${TMPDIR:-/tmp}/muxd-e2e-focusskip-$$.sock" 464 SOCK37="${TMPDIR:-/tmp}/muxd-e2e-focusskip-$$.sock"
366 D34PID="" 465 defer_sock "$SOCK37"
367 # The dead tile. Its own daemon because the leg needs a session name that 466 # The dead tile. Its own daemon because the leg needs a session name that
368 # does NOT exist — the daemon refuses the attach — and any other block's 467 # does NOT exist — the daemon refuses the attach — and any other block's
369 # daemon might have one by that name. 468 # daemon might have one by that name.
370 SOCK39="${TMPDIR:-/tmp}/muxd-e2e-deadtile-$$.sock" 469 SOCK39="${TMPDIR:-/tmp}/muxd-e2e-deadtile-$$.sock"
371 D36PID="" 470 defer_sock "$SOCK39"
372 471
373 # The wheel inside the focused tile. Two daemons for the wheel pair's own 472 # The wheel inside the focused tile. Two daemons for the wheel pair's own
374 # reason, turned up one notch: each needs a session whose SHELL is fixed for 473 # reason, turned up one notch: each needs a session whose SHELL is fixed for
@@ -377,32 +476,46 @@ D36PID=""
377 # terminal around. Both use the daemon's DEFAULT session, so the wall 476 # terminal around. Both use the daemon's DEFAULT session, so the wall
378 # spelling is a bare `--sock PATH` and nothing has to create anything. 477 # spelling is a bare `--sock PATH` and nothing has to create anything.
379 SOCK41="${TMPDIR:-/tmp}/muxd-e2e-wallwheel-$$.sock" 478 SOCK41="${TMPDIR:-/tmp}/muxd-e2e-wallwheel-$$.sock"
380 D38PID="" 479 defer_sock "$SOCK41"
381 ZWHEELSH="${TMPDIR:-/tmp}/mux-e2e-wallwheel-$$.sh" 480 ZWHEELSH="${TMPDIR:-/tmp}/mux-e2e-wallwheel-$$.sh"
481 defer_rm "$ZWHEELSH"
382 SOCK42="${TMPDIR:-/tmp}/muxd-e2e-wallappmouse-$$.sock" 482 SOCK42="${TMPDIR:-/tmp}/muxd-e2e-wallappmouse-$$.sock"
383 D39PID="" 483 defer_sock "$SOCK42"
384 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh" 484 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh"
485 defer_rm "$ZMOUSESH"
385 486
386 # The scrollback-rect leg: a focused tile's history page must own only its 487 # The scrollback-rect leg: a focused tile's history page must own only its
387 # sub-rect, so its daemon gets a socket of its own for the grid-size 488 # sub-rect, so its daemon gets a socket of its own for the grid-size
388 # reason every wall leg has. 489 # reason every wall leg has.
389 SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock" 490 SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock"
491 defer_sock "$SOCK53"
390 SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock" 492 SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock"
493 defer_sock "$SOCK54"
391 SOCK55="${TMPDIR:-/tmp}/muxd-e2e-resize-$$.sock" 494 SOCK55="${TMPDIR:-/tmp}/muxd-e2e-resize-$$.sock"
495 defer_sock "$SOCK55"
392 SOCK56="${TMPDIR:-/tmp}/muxd-e2e-spanclear-$$.sock" 496 SOCK56="${TMPDIR:-/tmp}/muxd-e2e-spanclear-$$.sock"
497 defer_sock "$SOCK56"
393 SOCK57="${TMPDIR:-/tmp}/muxd-e2e-hjkl-$$.sock" 498 SOCK57="${TMPDIR:-/tmp}/muxd-e2e-hjkl-$$.sock"
499 defer_sock "$SOCK57"
394 SOCK58="${TMPDIR:-/tmp}/muxd-e2e-splitbirth-$$.sock" 500 SOCK58="${TMPDIR:-/tmp}/muxd-e2e-splitbirth-$$.sock"
501 defer_sock "$SOCK58"
395 SOCK59="${TMPDIR:-/tmp}/muxd-e2e-lprrestore-$$.sock" 502 SOCK59="${TMPDIR:-/tmp}/muxd-e2e-lprrestore-$$.sock"
503 defer_sock "$SOCK59"
396 SOCK60="${TMPDIR:-/tmp}/muxd-e2e-lpheal-$$.sock" 504 SOCK60="${TMPDIR:-/tmp}/muxd-e2e-lpheal-$$.sock"
505 defer_sock "$SOCK60"
397 SOCK61="${TMPDIR:-/tmp}/muxd-e2e-lpdegrade-$$.sock" 506 SOCK61="${TMPDIR:-/tmp}/muxd-e2e-lpdegrade-$$.sock"
507 defer_sock "$SOCK61"
398 SOCK62="${TMPDIR:-/tmp}/muxd-e2e-promptA-$$.sock" 508 SOCK62="${TMPDIR:-/tmp}/muxd-e2e-promptA-$$.sock"
509 defer_sock "$SOCK62"
399 SOCK63="${TMPDIR:-/tmp}/muxd-e2e-promptB-$$.sock" 510 SOCK63="${TMPDIR:-/tmp}/muxd-e2e-promptB-$$.sock"
511 defer_sock "$SOCK63"
400 # The hydrated-create leg. One daemon, and it serves QUIC as well as its 512 # The hydrated-create leg. One daemon, and it serves QUIC as well as its
401 # socket: the leg's whole point is the LOCAL/remote split, so both halves 513 # socket: the leg's whole point is the LOCAL/remote split, so both halves
402 # have to be the same run of the same daemon or the comparison is between 514 # have to be the same run of the same daemon or the comparison is between
403 # two rigs rather than between two kinds of line. Short socket name on 515 # two rigs rather than between two kinds of line. Short socket name on
404 # purpose — the assertion reads a label bar back off a 70-column render. 516 # purpose — the assertion reads a label bar back off a 70-column render.
405 SOCK64="${TMPDIR:-/tmp}/muxd-e2e-hyd-$$.sock" 517 SOCK64="${TMPDIR:-/tmp}/muxd-e2e-hyd-$$.sock"
518 defer_sock "$SOCK64"
406 # Base 6000, below the 5000-spaced band the other QUIC ports share: every 519 # Base 6000, below the 5000-spaced band the other QUIC ports share: every
407 # base from 11000 up is already taken, and a DUPLICATED base passes only 520 # base from 11000 up is already taken, and a DUPLICATED base passes only
408 # for as long as the other leg's daemon happens to be dead by the time 521 # for as long as the other leg's daemon happens to be dead by the time
@@ -410,16 +523,18 @@ SOCK64="${TMPDIR:-/tmp}/muxd-e2e-hyd-$$.sock"
410 # suite's, which is why the base has to be the leg's own. 523 # suite's, which is why the base has to be the leg's own.
411 HYPORT=$(( 6000 + ($$ % 4000) )) 524 HYPORT=$(( 6000 + ($$ % 4000) ))
412 HYKEY="${TMPDIR:-/tmp}/mux-e2e-hydkey-$$" 525 HYKEY="${TMPDIR:-/tmp}/mux-e2e-hydkey-$$"
526 defer_rm "$HYKEY"
413 HYSTATE="${TMPDIR:-/tmp}/mux-e2e-hyd-state-$$" 527 HYSTATE="${TMPDIR:-/tmp}/mux-e2e-hyd-state-$$"
528 defer_rm "$HYSTATE"
414 HYWALL="$HYSTATE/mux/wall" 529 HYWALL="$HYSTATE/mux/wall"
415 D65PID=""
416 # The refused-attach leg: its own daemon, because its assertions are about 530 # The refused-attach leg: its own daemon, because its assertions are about
417 # what a daemon says to a name it does NOT have, and a socket shared with 531 # what a daemon says to a name it does NOT have, and a socket shared with
418 # another leg would let that leg's sessions decide what "does not have" 532 # another leg would let that leg's sessions decide what "does not have"
419 # means. 533 # means.
420 SOCK66="${TMPDIR:-/tmp}/muxd-e2e-refuse-$$.sock" 534 SOCK66="${TMPDIR:-/tmp}/muxd-e2e-refuse-$$.sock"
535 defer_sock "$SOCK66"
421 REFSTATE="${TMPDIR:-/tmp}/mux-e2e-refuse-state-$$" 536 REFSTATE="${TMPDIR:-/tmp}/mux-e2e-refuse-state-$$"
422 D66PID="" 537 defer_rm "$REFSTATE"
423 # The browser half of the same ruling: one daemon serving a socket AND a 538 # The browser half of the same ruling: one daemon serving a socket AND a
424 # QUIC listener, one hub, a state home of its own. Two ports, and the 539 # QUIC listener, one hub, a state home of its own. Two ports, and the
425 # 5000-spaced bands are exhausted — 61000 is the last one that fits under 540 # 5000-spaced bands are exhausted — 61000 is the last one that fits under
@@ -430,22 +545,24 @@ D66PID=""
430 # daemon failing to bind, which is also why the two halves cannot share a 545 # daemon failing to bind, which is also why the two halves cannot share a
431 # base. 546 # base.
432 SOCK67="${TMPDIR:-/tmp}/muxd-e2e-wg-$$.sock" 547 SOCK67="${TMPDIR:-/tmp}/muxd-e2e-wg-$$.sock"
548 defer_sock "$SOCK67"
433 WGPORT=$(( 65000 + ($$ % 250) )) 549 WGPORT=$(( 65000 + ($$ % 250) ))
434 WGQPORT=$(( 65250 + ($$ % 250) )) 550 WGQPORT=$(( 65250 + ($$ % 250) ))
435 WGKEY="${TMPDIR:-/tmp}/mux-e2e-wgkey-$$" 551 WGKEY="${TMPDIR:-/tmp}/mux-e2e-wgkey-$$"
552 defer_rm "$WGKEY"
436 WGSTATE="${TMPDIR:-/tmp}/mux-e2e-wg-state-$$" 553 WGSTATE="${TMPDIR:-/tmp}/mux-e2e-wg-state-$$"
554 defer_rm "$WGSTATE"
437 WGWALL="$WGSTATE/mux/wall" 555 WGWALL="$WGSTATE/mux/wall"
438 # The refusal-spin leg. Its own everything: a shim dir on PATH, a state 556 # The refusal-spin leg. Its own everything: a shim dir on PATH, a state
439 # home holding the wall it restores, and a dial log the shim appends to. 557 # home holding the wall it restores, and a dial log the shim appends to.
440 SOCK68="${TMPDIR:-/tmp}/muxd-e2e-sp-$$.sock" 558 SOCK68="${TMPDIR:-/tmp}/muxd-e2e-sp-$$.sock"
559 defer_sock "$SOCK68"
441 SPPORT=$(( 26000 + ($$ % 4000) )) 560 SPPORT=$(( 26000 + ($$ % 4000) ))
442 SPSTATE="${TMPDIR:-/tmp}/mux-e2e-sp-state-$$" 561 SPSTATE="${TMPDIR:-/tmp}/mux-e2e-sp-state-$$"
562 defer_rm "$SPSTATE"
443 SPDIR="${TMPDIR:-/tmp}/mux-e2e-sp-shim-$$" 563 SPDIR="${TMPDIR:-/tmp}/mux-e2e-sp-shim-$$"
564 defer_rm "$SPDIR"
444 SPINLOG="$SPDIR/dials" 565 SPINLOG="$SPDIR/dials"
445 D67PID=""
446 W5PID=""
447 D68PID=""
448 W6PID=""
449 # `muxd upgrade` — four daemons, because each one is a different daemon 566 # `muxd upgrade` — four daemons, because each one is a different daemon
450 # LIFECYCLE and no two can share a process: the same-binary leg's daemon 567 # LIFECYCLE and no two can share a process: the same-binary leg's daemon
451 # ends up running a second image, the rollback leg's is born with the abort 568 # ends up running a second image, the rollback leg's is born with the abort
@@ -453,46 +570,37 @@ W6PID=""
453 # QUIC leg's owns a UDP socket. They run one after another, so the last of 570 # QUIC leg's owns a UDP socket. They run one after another, so the last of
454 # them is the only leg here that needs a port. 571 # them is the only leg here that needs a port.
455 SOCK69="${TMPDIR:-/tmp}/muxd-e2e-upgrade-$$.sock" 572 SOCK69="${TMPDIR:-/tmp}/muxd-e2e-upgrade-$$.sock"
573 defer_sock "$SOCK69"
456 SOCK70="${TMPDIR:-/tmp}/muxd-e2e-uproll-$$.sock" 574 SOCK70="${TMPDIR:-/tmp}/muxd-e2e-uproll-$$.sock"
575 defer_sock "$SOCK70"
457 SOCK71="${TMPDIR:-/tmp}/muxd-e2e-upagent-$$.sock" 576 SOCK71="${TMPDIR:-/tmp}/muxd-e2e-upagent-$$.sock"
577 defer_sock "$SOCK71"
458 SOCK72="${TMPDIR:-/tmp}/muxd-e2e-upquic-$$.sock" 578 SOCK72="${TMPDIR:-/tmp}/muxd-e2e-upquic-$$.sock"
579 defer_sock "$SOCK72"
459 SOCK73="${TMPDIR:-/tmp}/muxd-e2e-stopgone-$$.sock" 580 SOCK73="${TMPDIR:-/tmp}/muxd-e2e-stopgone-$$.sock"
581 defer_sock "$SOCK73"
460 # The next 4000-wide band DOWN from the hydrate leg's 6000: every base from 582 # The next 4000-wide band DOWN from the hydrate leg's 6000: every base from
461 # 11000 up is taken, and the 61000+ tail above the ephemeral range is spoken 583 # 11000 up is taken, and the 61000+ tail above the ephemeral range is spoken
462 # for by the two browser ports. A collision here reads as this leg's daemon 584 # for by the two browser ports. A collision here reads as this leg's daemon
463 # failing to bind, the same verdict every other band gives. 585 # failing to bind, the same verdict every other band gives.
464 UPQPORT=$(( 2000 + ($$ % 4000) )) 586 UPQPORT=$(( 2000 + ($$ % 4000) ))
465 UPKEY="${TMPDIR:-/tmp}/mux-e2e-upkey-$$" 587 UPKEY="${TMPDIR:-/tmp}/mux-e2e-upkey-$$"
588 defer_rm "$UPKEY"
466 # A HOME of its own for the session shell. The title assertion below is 589 # A HOME of its own for the session shell. The title assertion below is
467 # about a title the SESSION set, and a distribution's rc file repaints the 590 # about a title the SESSION set, and a distribution's rc file repaints the
468 # window title from PROMPT_COMMAND on every prompt (shellint.bash_init says 591 # window title from PROMPT_COMMAND on every prompt (shellint.bash_init says
469 # so of Arch's) — read out of the developer's own dotfiles, this leg would 592 # so of Arch's) — read out of the developer's own dotfiles, this leg would
470 # assert on whatever their prompt happens to spell. 593 # assert on whatever their prompt happens to spell.
471 UPHOME="${TMPDIR:-/tmp}/mux-e2e-uphome-$$" 594 UPHOME="${TMPDIR:-/tmp}/mux-e2e-uphome-$$"
595 defer_rm "$UPHOME"
472 # The candidate binary the refusal leg offers: a copy, so its exec bit can 596 # The candidate binary the refusal leg offers: a copy, so its exec bit can
473 # be taken away without touching the one every other scenario runs. 597 # be taken away without touching the one every other scenario runs.
474 UPBIN="${TMPDIR:-/tmp}/mux-e2e-upcand-$$" 598 UPBIN="${TMPDIR:-/tmp}/mux-e2e-upcand-$$"
599 defer_rm "$UPBIN"
475 UPAGENT="${TMPDIR:-/tmp}/mux-e2e-upagent-$$.sock" 600 UPAGENT="${TMPDIR:-/tmp}/mux-e2e-upagent-$$.sock"
601 defer_rm "$UPAGENT"
476 UPAGKEY="${TMPDIR:-/tmp}/mux-e2e-upagkey-$$" 602 UPAGKEY="${TMPDIR:-/tmp}/mux-e2e-upagkey-$$"
477 D69PID="" 603 defer_rm "$UPAGKEY" "$UPAGKEY.pub"
478 D70PID=""
479 D71PID=""
480 D72PID=""
481 D73PID=""
482 UPAGPID=""
483 UPPCPID=""
484 UPCPID=""
485 D54PID=""
486 D55PID=""
487 D56PID=""
488 D57PID=""
489 D58PID=""
490 D59PID=""
491 D60PID=""
492 D61PID=""
493 D62PID=""
494 D63PID=""
495 D64PID=""
496 604
497 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its 605 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its
498 # own, for the dynamic-wall leg's reason turned up one notch: what these 606 # own, for the dynamic-wall leg's reason turned up one notch: what these
@@ -502,15 +610,18 @@ D64PID=""
502 # The daemon is separate because the tile spelling contains the socket 610 # The daemon is separate because the tile spelling contains the socket
503 # path, which is what the assertions grep for. 611 # path, which is what the assertions grep for.
504 SOCK40="${TMPDIR:-/tmp}/muxd-e2e-wallhist-$$.sock" 612 SOCK40="${TMPDIR:-/tmp}/muxd-e2e-wallhist-$$.sock"
613 defer_sock "$SOCK40"
505 WHSTATE="${TMPDIR:-/tmp}/mux-e2e-wallhist-state-$$" 614 WHSTATE="${TMPDIR:-/tmp}/mux-e2e-wallhist-state-$$"
615 defer_rm "$WHSTATE"
506 WHWALL="$WHSTATE/mux/wall" 616 WHWALL="$WHSTATE/mux/wall"
507 # A state home whose wall FILE is a directory: the unwritable case, which 617 # A state home whose wall FILE is a directory: the unwritable case, which
508 # has to warn and let the attach happen anyway. 618 # has to warn and let the attach happen anyway.
509 WHBAD="${TMPDIR:-/tmp}/mux-e2e-wallbad-$$" 619 WHBAD="${TMPDIR:-/tmp}/mux-e2e-wallbad-$$"
620 defer_rm "$WHBAD"
510 # And a third, for the `x` block: its wall is built by two attaches and 621 # And a third, for the `x` block: its wall is built by two attaches and
511 # then eaten by `x`, so it must start empty and stay its own. 622 # then eaten by `x`, so it must start empty and stay its own.
512 WHXSTATE="${TMPDIR:-/tmp}/mux-e2e-wallx-state-$$" 623 WHXSTATE="${TMPDIR:-/tmp}/mux-e2e-wallx-state-$$"
513 D37PID="" 624 defer_rm "$WHXSTATE"
514 625
515 # The convergence block: `mux TARGET` is a wall of one tile whose rect is 626 # The convergence block: `mux TARGET` is a wall of one tile whose rect is
516 # the whole terminal. Three daemons and three state homes, each of them its own 627 # the whole terminal. Three daemons and three state homes, each of them its own
@@ -518,21 +629,25 @@ D37PID=""
518 # back is the wall FILE, and the shared $XDG_STATE_HOME is every socket the 629 # back is the wall FILE, and the shared $XDG_STATE_HOME is every socket the
519 # suite has ever attached to. 630 # suite has ever attached to.
520 SOCK45="${TMPDIR:-/tmp}/muxd-e2e-converge-$$.sock" 631 SOCK45="${TMPDIR:-/tmp}/muxd-e2e-converge-$$.sock"
632 defer_sock "$SOCK45"
521 CVSTATE="${TMPDIR:-/tmp}/mux-e2e-converge-state-$$" 633 CVSTATE="${TMPDIR:-/tmp}/mux-e2e-converge-state-$$"
634 defer_rm "$CVSTATE"
522 CVWALL="$CVSTATE/mux/wall" 635 CVWALL="$CVSTATE/mux/wall"
523 D45PID=""
524 # The ring-grows-the-wall leg needs TWO state homes: one for the client 636 # The ring-grows-the-wall leg needs TWO state homes: one for the client
525 # under test, and one for the setup client whose attach must NOT leave a 637 # under test, and one for the setup client whose attach must NOT leave a
526 # tile the leg would then find already there. That absence is the case 638 # tile the leg would then find already there. That absence is the case
527 # under test — a sibling the daemon knows and the wall does not. 639 # under test — a sibling the daemon knows and the wall does not.
528 SOCK46="${TMPDIR:-/tmp}/muxd-e2e-ringgrow-$$.sock" 640 SOCK46="${TMPDIR:-/tmp}/muxd-e2e-ringgrow-$$.sock"
641 defer_sock "$SOCK46"
529 RGSTATE="${TMPDIR:-/tmp}/mux-e2e-ringgrow-state-$$" 642 RGSTATE="${TMPDIR:-/tmp}/mux-e2e-ringgrow-state-$$"
643 defer_rm "$RGSTATE"
530 RGOTHER="${TMPDIR:-/tmp}/mux-e2e-ringgrow-other-$$" 644 RGOTHER="${TMPDIR:-/tmp}/mux-e2e-ringgrow-other-$$"
531 D46PID="" 645 defer_rm "$RGOTHER"
532 # The exit-semantics leg: a session ending in the focused tile. 646 # The exit-semantics leg: a session ending in the focused tile.
533 SOCK47="${TMPDIR:-/tmp}/muxd-e2e-exitsem-$$.sock" 647 SOCK47="${TMPDIR:-/tmp}/muxd-e2e-exitsem-$$.sock"
648 defer_sock "$SOCK47"
534 XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$" 649 XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$"
535 D47PID="" 650 defer_rm "$XESTATE"
536 651
537 # Agent forwarding: one socket path, two daemons in turn. The positive leg 652 # Agent forwarding: one socket path, two daemons in turn. The positive leg
538 # ends by typing `exit`, which ends that daemon's only session and takes the 653 # ends by typing `exit`, which ends that daemon's only session and takes the
@@ -545,13 +660,15 @@ D47PID=""
545 # session shell, so the room it has is the room every other socket in this 660 # session shell, so the room it has is the room every other socket in this
546 # file was given. 661 # file was given.
547 SOCK48="${TMPDIR:-/tmp}/muxd-e2e-agentfwd-$$.sock" 662 SOCK48="${TMPDIR:-/tmp}/muxd-e2e-agentfwd-$$.sock"
663 defer_sock "$SOCK48"
548 SOCK51="${TMPDIR:-/tmp}/muxd-e2e-inband-$$.sock" 664 SOCK51="${TMPDIR:-/tmp}/muxd-e2e-inband-$$.sock"
549 D51PID="" 665 defer_sock "$SOCK51"
550 SOCK52="${TMPDIR:-/tmp}/muxd-e2e-cursor-$$.sock" 666 SOCK52="${TMPDIR:-/tmp}/muxd-e2e-cursor-$$.sock"
551 D52PID="" 667 defer_sock "$SOCK52"
552 AGENT48="${TMPDIR:-/tmp}/mux-e2e-agent-$$.sock" 668 AGENT48="${TMPDIR:-/tmp}/mux-e2e-agent-$$.sock"
553 AGENT48PID="" 669 defer_rm "$AGENT48"
554 AGENT48KEY="${TMPDIR:-/tmp}/mux-e2e-agentkey-$$" 670 AGENT48KEY="${TMPDIR:-/tmp}/mux-e2e-agentkey-$$"
671 defer_rm "$AGENT48KEY" "$AGENT48KEY.pub"
555 D42PID="" 672 D42PID=""
556 673
557 # The flip leg gets a daemon of its own, and not for isolation's sake: the 674 # The flip leg gets a daemon of its own, and not for isolation's sake: the
@@ -561,12 +678,17 @@ D42PID=""
561 # the leg would pass having proved nothing. Beside the sockets for the 678 # the leg would pass having proved nothing. Beside the sockets for the
562 # sun_path reason above; these are dialled from inside a session too. 679 # sun_path reason above; these are dialled from inside a session too.
563 SOCK49="${TMPDIR:-/tmp}/muxd-e2e-agentflip-$$.sock" 680 SOCK49="${TMPDIR:-/tmp}/muxd-e2e-agentflip-$$.sock"
681 defer_sock "$SOCK49"
564 AGENT49A="${TMPDIR:-/tmp}/mux-e2e-agent2a-$$.sock" 682 AGENT49A="${TMPDIR:-/tmp}/mux-e2e-agent2a-$$.sock"
683 defer_rm "$AGENT49A"
565 AGENT49B="${TMPDIR:-/tmp}/mux-e2e-agent2b-$$.sock" 684 AGENT49B="${TMPDIR:-/tmp}/mux-e2e-agent2b-$$.sock"
685 defer_rm "$AGENT49B"
566 AGENT49APID="" 686 AGENT49APID=""
567 AGENT49BPID="" 687 AGENT49BPID=""
568 AGENT49AKEY="${TMPDIR:-/tmp}/mux-e2e-agent2akey-$$" 688 AGENT49AKEY="${TMPDIR:-/tmp}/mux-e2e-agent2akey-$$"
689 defer_rm "$AGENT49AKEY" "$AGENT49AKEY.pub"
569 AGENT49BKEY="${TMPDIR:-/tmp}/mux-e2e-agent2bkey-$$" 690 AGENT49BKEY="${TMPDIR:-/tmp}/mux-e2e-agent2bkey-$$"
691 defer_rm "$AGENT49BKEY" "$AGENT49BKEY.pub"
570 D43PID="" 692 D43PID=""
571 FLIPAPID="" 693 FLIPAPID=""
572 694
@@ -574,6 +696,7 @@ FLIPAPID=""
574 # every other leg does: the wall scenario above stops its daemon on the way 696 # every other leg does: the wall scenario above stops its daemon on the way
575 # out, so there is nothing left to reuse. 697 # out, so there is nothing left to reuse.
576 SOCK50="${TMPDIR:-/tmp}/muxd-e2e-wallmouse-$$.sock" 698 SOCK50="${TMPDIR:-/tmp}/muxd-e2e-wallmouse-$$.sock"
699 defer_sock "$SOCK50"
577 D48PID="" 700 D48PID=""
578 701
579 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one 702 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one
@@ -742,6 +865,34 @@ wait_sock() {
742 } 865 }
743 } 866 }
744 867
868 # start_daemon SOCK LOG LABEL [muxd args...] — a daemon in the background,
869 # registered before it is waited for. Sets $DPID, the pid to end it by.
870 # $DPID is scratch: the NEXT start_daemon overwrites it, so a leg that
871 # still needs its daemon further down keeps the pid under a name of its
872 # own. The long-lived daemon is $D1PID for exactly that reason — it is
873 # read some 5,000 lines after it is started.
874 #
875 # The registration is the whole reason this exists. A `muxd run &` written
876 # out by hand is three statements — the spawn, the pid, the wait — plus a
877 # fourth in cleanup() saying how to end it, and the fourth is the one that
878 # got forgotten. Here the spawn IS the registration and there is no fourth
879 # place to edit.
880 #
881 # Only for the plain shape. A daemon that needs an environment in front of
882 # it writes its own spawn and calls defer_kill/defer_sock itself: `env` or
883 # a VAR=VAL prefix on a FUNCTION call does not scope to the function — in
884 # POSIX sh it assigns in THIS shell and the value stays there for every
885 # scenario after it.
886 start_daemon() {
887 _sds="$1"; _sdl="$2"; _sdlab="$3"; shift 3
888 "$MUXD" run --sock "$_sds" "$@" > "$_sdl" 2>&1 &
889 DPID=$!
890 defer_kill "$DPID"
891 defer_sock "$_sds"
892 wait_sock "$_sds" "$_sdl" "$_sdlab"
893 }
894
895
745 # pipe_mux OUT ERR CMD... — a client on a plain pipe, and the pipe kept 896 # pipe_mux OUT ERR CMD... — a client on a plain pipe, and the pipe kept
746 # open. ERR is the stderr capture, "" to leave it on the suite's own. 897 # open. ERR is the stderr capture, "" to leave it on the suite's own.
747 # The suite's non-tty scenarios used to be `{ printf cmd; sleep 2; printf 898 # The suite's non-tty scenarios used to be `{ printf cmd; sleep 2; printf
@@ -759,6 +910,7 @@ wait_sock() {
759 pipe_mux() { 910 pipe_mux() {
760 PIPE_OUT="$1"; PIPE_ERR="$2"; shift 2 911 PIPE_OUT="$1"; PIPE_ERR="$2"; shift 2
761 PIPE_IN="$PIPE_OUT.in" 912 PIPE_IN="$PIPE_OUT.in"
913 defer_rm "$PIPE_IN"
762 rm -f "$PIPE_IN" 914 rm -f "$PIPE_IN"
763 mkfifo "$PIPE_IN" 915 mkfifo "$PIPE_IN"
764 exec 9<>"$PIPE_IN" 916 exec 9<>"$PIPE_IN"
@@ -768,6 +920,7 @@ pipe_mux() {
768 "$@" < "$PIPE_IN" > "$PIPE_OUT" & 920 "$@" < "$PIPE_IN" > "$PIPE_OUT" &
769 fi 921 fi
770 PIPE_PID=$! 922 PIPE_PID=$!
923 defer_kill "$PIPE_PID"
771 } 924 }
772 925
773 # pipe_send FMT [ARGS] — printf into the open client, same spelling the 926 # pipe_send FMT [ARGS] — printf into the open client, same spelling the
@@ -988,6 +1141,7 @@ watch_clients() {
988 sleep 0.2 1141 sleep 0.2
989 done ) > "$2" & 1142 done ) > "$2" &
990 WATCH_PID=$! 1143 WATCH_PID=$!
1144 defer_kill "$WATCH_PID"
991 } 1145 }
992 1146
993 # unwatch_clients FILE — stop sampling. 1147 # unwatch_clients FILE — stop sampling.
@@ -1348,519 +1502,93 @@ cleanup() {
1348 # it. Every verdict below is composed onto THIS number, never in place of 1502 # it. Every verdict below is composed onto THIS number, never in place of
1349 # it: a run that failed at scenario 6 exits with scenario 6's failure. 1503 # it: a run that failed at scenario 6 exits with scenario 6's failure.
1350 _rc=$? 1504 _rc=$?
1351 # `${DPID:-}` rather than `$DPID`: DPID is the one variable this trap 1505 # The registers are newline-joined and the paths in them may hold spaces
1352 # reads that is assigned AFTER the trap is installed, and the scenarios 1506 # (a $TMPDIR nobody here chose); a newline IFS is what keeps each entry
1353 # above the first daemon (--version) can fail before it ever is. Under 1507 # one entry.
1354 # `set -u` the bare form would abort the trap on an unbound variable, 1508 _oifs=$IFS
1355 # skipping the rm below — a failing suite would also leave its files. 1509 IFS='
1356 [ -n "${DPID:-}" ] && kill "$DPID" 2>/dev/null || true 1510 '
1357 # `|| true` on every one of these: under `set -e` a kill of an 1511 # Sockets first, and by the daemon's own verb. `muxd stop` on a path
1358 # already-dead pid would abort the trap itself, skipping the rm below and 1512 # nobody serves is a no-op that exits 0, and on a daemon this shell never
1359 # failing a suite that had actually passed. 1513 # forked — one a proxy, `muxd start` or the handoff spawned — it is the
1360 [ -n "$D2PID" ] && kill "$D2PID" 2>/dev/null || true 1514 # only handle there is. Before the kills, and long before the unlink
1361 [ -n "$D3PID" ] && kill "$D3PID" 2>/dev/null || true 1515 # below: unlinking first would leave a live daemon nothing could reach
1362 [ -n "$D4PID" ] && kill "$D4PID" 2>/dev/null || true
1363 [ -n "$D5PID" ] && kill "$D5PID" 2>/dev/null || true
1364 [ -n "$D6PID" ] && kill "$D6PID" 2>/dev/null || true
1365 [ -n "$D7PID" ] && kill "$D7PID" 2>/dev/null || true
1366 [ -n "$D9PID" ] && kill "$D9PID" 2>/dev/null || true
1367 [ -n "$D10PID" ] && kill "$D10PID" 2>/dev/null || true
1368 [ -n "$D12PID" ] && kill "$D12PID" 2>/dev/null || true
1369 [ -n "$D13PID" ] && kill "$D13PID" 2>/dev/null || true
1370 # The fixture, by the pid the suite holds. Killing it closes the pty
1371 # master, and that SIGHUPs the client on the slave — the same guarantee
1372 # a real terminal gives, which is why the fixture never reaps its own
1373 # child and this trap does not chase one.
1374 [ -n "$TP1PID" ] && kill "$TP1PID" 2>/dev/null || true
1375 # The agent-flip leg's backgrounded client, on the same guarantee: its
1376 # own client dies with the pty master this kill closes.
1377 [ -n "$FLIPAPID" ] && kill "$FLIPAPID" 2>/dev/null || true
1378 # Detached daemons: killed by the pid their own up-line reported.
1379 [ -n "$SPID" ] && kill "$SPID" 2>/dev/null || true
1380 [ -n "$TPID" ] && kill "$TPID" 2>/dev/null || true
1381 [ -n "$GPID" ] && kill "$GPID" 2>/dev/null || true
1382 # The M13 auto-started daemons, by the pid their spawner's up-line
1383 # reported. Cleared by the scenarios themselves once `muxd stop` has
1384 # been OBSERVED to end them, so a pid here means a failing run.
1385 [ -n "$APID" ] && kill "$APID" 2>/dev/null || true
1386 [ -n "$PAPID" ] && kill "$PAPID" 2>/dev/null || true
1387 # ...and the same two by socket, because a spawn that failed between
1388 # the fork and the up-line leaves a daemon this trap holds no pid for.
1389 # `muxd stop` on a path nobody serves is a no-op that exits 0.
1390 [ -n "${MUXD:-}" ] && "$MUXD" stop --sock "$SOCK14" >/dev/null 2>&1 || true
1391 [ -n "${MUXD:-}" ] && "$MUXD" stop --sock "$SOCK15" >/dev/null 2>&1 || true
1392 # M14's two, the same way and for the same reason: the cold-handoff
1393 # daemon is spawned by `muxd endpoint` and is nobody's child here, so
1394 # its pid is read off the up-line and the socket stop backstops the
1395 # window before that read. The stops come BEFORE the rm -rf below —
1396 # unlinking a runtime dir first would leave a live daemon this trap
1397 # could no longer reach by path.
1398 [ -n "$HDPID" ] && kill "$HDPID" 2>/dev/null || true
1399 [ -n "$HAPID" ] && kill "$HAPID" 2>/dev/null || true
1400 # The backgrounded clients, by tracked pid. Their `timeout` bounds them
1401 # anyway; this only shortens the wait on a run that failed between a
1402 # launch and its wait.
1403 # Whatever pipe_mux client a failing scenario left mid-flight, and the
1404 # FIFO that was its stdin.
1405 [ -n "$PIPE_PID" ] && kill "$PIPE_PID" 2>/dev/null || true
1406 [ -n "$PIPE_IN" ] && rm -f "$PIPE_IN" || true
1407 [ -n "${MUXD:-}" ] && "$MUXD" stop --sock "$SOCK16" >/dev/null 2>&1 || true
1408 [ -n "${MUXD:-}" ] && "$MUXD" stop --sock "$SOCK17" >/dev/null 2>&1 || true
1409 # M-web: hubs and daemons by tracked pid, then stop-by-socket for any
1410 # daemon that died between fork and up-line. Hoisted up here with the
1411 # other kills so that every kill precedes the sweep below — a daemon
1412 # killed AFTER it would be swept before it had written its verdict, and
1413 # the sweep's answer would be about a lifecycle that had not ended.
1414 [ -n "$W1PID" ] && kill "$W1PID" 2>/dev/null || true
1415 [ -n "$W2PID" ] && kill "$W2PID" 2>/dev/null || true
1416 [ -n "$WCLIPID" ] && kill "$WCLIPID" 2>/dev/null || true
1417 [ -n "$D14PID" ] && kill "$D14PID" 2>/dev/null || true
1418 [ -n "$D15PID" ] && kill "$D15PID" 2>/dev/null || true
1419 [ -n "$D16PID" ] && kill "$D16PID" 2>/dev/null || true
1420 [ -n "$D17PID" ] && kill "$D17PID" 2>/dev/null || true
1421 # M18's two daemons and its hub. A multi-session daemon holds N shells,
1422 # so one leaked pid here is N leaked processes — the kill is by the
1423 # tracked pid all the same, and killing the daemon closes every pty
1424 # master it owns, which is what ends the shells on the other side.
1425 [ -n "$W3PID" ] && kill "$W3PID" 2>/dev/null || true
1426 [ -n "$D18PID" ] && kill "$D18PID" 2>/dev/null || true
1427 [ -n "$D19PID" ] && kill "$D19PID" 2>/dev/null || true
1428 [ -n "$D20PID" ] && kill "$D20PID" 2>/dev/null || true
1429 [ -n "$D21PID" ] && kill "$D21PID" 2>/dev/null || true
1430 # The dynamic-wall leg's hub and daemon. The hub variable is re-assigned
1431 # across three incarnations on the same port, so a pid here is whichever
1432 # one the run died under.
1433 [ -n "$W4PID" ] && kill "$W4PID" 2>/dev/null || true
1434 [ -n "${W5PID:-}" ] && kill "$W5PID" 2>/dev/null || true
1435 [ -n "${W6PID:-}" ] && kill "$W6PID" 2>/dev/null || true
1436 [ -n "$D22PID" ] && kill "$D22PID" 2>/dev/null || true
1437 [ -n "$D23PID" ] && kill "$D23PID" 2>/dev/null || true
1438 [ -n "$D24PID" ] && kill "$D24PID" 2>/dev/null || true
1439 [ -n "$D25PID" ] && kill "$D25PID" 2>/dev/null || true
1440 [ -n "$D26PID" ] && kill "$D26PID" 2>/dev/null || true
1441 [ -n "$D27PID" ] && kill "$D27PID" 2>/dev/null || true
1442 [ -n "$D28PID" ] && kill "$D28PID" 2>/dev/null || true
1443 [ -n "$D29PID" ] && kill "$D29PID" 2>/dev/null || true
1444 [ -n "$D30PID" ] && kill "$D30PID" 2>/dev/null || true
1445 [ -n "$D31PID" ] && kill "$D31PID" 2>/dev/null || true
1446 [ -n "$D32PID" ] && kill "$D32PID" 2>/dev/null || true
1447 [ -n "$D33PID" ] && kill "$D33PID" 2>/dev/null || true
1448 [ -n "$D34PID" ] && kill "$D34PID" 2>/dev/null || true
1449 [ -n "$D35PID" ] && kill "$D35PID" 2>/dev/null || true
1450 [ -n "$D36PID" ] && kill "$D36PID" 2>/dev/null || true
1451 [ -n "$D37PID" ] && kill "$D37PID" 2>/dev/null || true
1452 [ -n "$D38PID" ] && kill "$D38PID" 2>/dev/null || true
1453 [ -n "$D39PID" ] && kill "$D39PID" 2>/dev/null || true
1454 [ -n "$D40PID" ] && kill "$D40PID" 2>/dev/null || true
1455 [ -n "$D41PID" ] && kill "$D41PID" 2>/dev/null || true
1456 [ -n "$D42PID" ] && kill "$D42PID" 2>/dev/null || true
1457 [ -n "$D43PID" ] && kill "$D43PID" 2>/dev/null || true
1458 [ -n "${D51PID:-}" ] && kill "$D51PID" 2>/dev/null || true
1459 [ -n "${D52PID:-}" ] && kill "$D52PID" 2>/dev/null || true
1460 [ -n "${D55PID:-}" ] && kill "$D55PID" 2>/dev/null || true
1461 [ -n "${D57PID:-}" ] && kill "$D57PID" 2>/dev/null || true
1462 [ -n "${D58PID:-}" ] && kill "$D58PID" 2>/dev/null || true
1463 [ -n "${D59PID:-}" ] && kill "$D59PID" 2>/dev/null || true
1464 [ -n "${D60PID:-}" ] && kill "$D60PID" 2>/dev/null || true
1465 [ -n "${D61PID:-}" ] && kill "$D61PID" 2>/dev/null || true
1466 [ -n "${D62PID:-}" ] && kill "$D62PID" 2>/dev/null || true
1467 [ -n "${D63PID:-}" ] && kill "$D63PID" 2>/dev/null || true
1468 [ -n "${D64PID:-}" ] && kill "$D64PID" 2>/dev/null || true
1469 [ -n "${D65PID:-}" ] && kill "$D65PID" 2>/dev/null || true
1470 [ -n "${D66PID:-}" ] && kill "$D66PID" 2>/dev/null || true
1471 # The upgrade legs' daemons. CONT first for AGENT48PID's reason turned
1472 # on a daemon: the non-executable-candidate refusal holds this one under
1473 # SIGSTOP while it takes the exec bit off the candidate, and a run that
1474 # dies inside that window leaves a process no SIGTERM can reach.
1475 [ -n "${D69PID:-}" ] && kill -CONT "$D69PID" 2>/dev/null || true
1476 [ -n "${D69PID:-}" ] && kill "$D69PID" 2>/dev/null || true
1477 [ -n "${D70PID:-}" ] && kill "$D70PID" 2>/dev/null || true
1478 [ -n "${D71PID:-}" ] && kill "$D71PID" 2>/dev/null || true
1479 [ -n "${D72PID:-}" ] && kill "$D72PID" 2>/dev/null || true
1480 [ -n "${D73PID:-}" ] && kill "$D73PID" 2>/dev/null || true
1481 [ -n "${UPPCPID:-}" ] && kill "$UPPCPID" 2>/dev/null || true
1482 [ -n "${UPCPID:-}" ] && kill "$UPCPID" 2>/dev/null || true
1483 # The ssh-agents the forwarding legs start. Not mux processes and so not
1484 # the leak sweep's business, but they are daemons this file forked: left
1485 # alive they outlive the suite holding a private key, which is the one
1486 # kind of leak worth chasing even on a green run.
1487 # CONT first: the mute-offerer leg holds this agent under SIGSTOP, and
1488 # a stopped process does not see SIGTERM until it runs again.
1489 [ -n "$AGENT48PID" ] && kill -CONT "$AGENT48PID" 2>/dev/null || true
1490 [ -n "$AGENT48PID" ] && kill "$AGENT48PID" 2>/dev/null || true
1491 [ -n "$AGENT49APID" ] && kill "$AGENT49APID" 2>/dev/null || true
1492 [ -n "$AGENT49BPID" ] && kill "$AGENT49BPID" 2>/dev/null || true
1493 [ -n "${UPAGPID:-}" ] && kill "$UPAGPID" 2>/dev/null || true
1494 # The stops still precede the socket rm below, like SOCK14-17 above:
1495 # unlinking a socket first would leave a live daemon nothing could reach
1496 # by path. 1516 # by path.
1497 [ -S "$SOCK18" ] && "$MUXD" stop --sock "$SOCK18" 2>/dev/null || true 1517 for _cs in $E2E_SOCK; do
1498 [ -S "$SOCK19" ] && "$MUXD" stop --sock "$SOCK19" 2>/dev/null || true 1518 if [ -S "$_cs" ] && [ -n "${MUXD:-}" ]; then
1499 [ -S "$SOCK20" ] && "$MUXD" stop --sock "$SOCK20" 2>/dev/null || true 1519 "$MUXD" stop --sock "$_cs" > /dev/null 2>&1 || true
1500 [ -S "$SOCK25" ] && "$MUXD" stop --sock "$SOCK25" 2>/dev/null || true 1520 fi
1501 [ -S "$SOCK26" ] && "$MUXD" stop --sock "$SOCK26" 2>/dev/null || true 1521 done
1502 [ -S "$SOCK27" ] && "$MUXD" stop --sock "$SOCK27" 2>/dev/null || true 1522 # Then every registered process. softkill rather than a plain `kill`,
1503 [ -S "$SOCK28" ] && "$MUXD" stop --sock "$SOCK28" 2>/dev/null || true 1523 # which is what this trap used to send: under the coverage harness the
1504 [ -S "$SOCK29" ] && "$MUXD" stop --sock "$SOCK29" 2>/dev/null || true 1524 # pid the suite holds is a kcov TRACER, and a signal sent to a tracer is
1505 [ -S "$SOCK30" ] && "$MUXD" stop --sock "$SOCK30" 2>/dev/null || true 1525 # dropped — measured on the hub leg, where `kill $W3PID` left both alive.
1506 [ -S "$SOCK31" ] && "$MUXD" stop --sock "$SOCK31" 2>/dev/null || true 1526 # TERM rather than hardkill's KILL, because a daemon writes its leak
1507 [ -S "$SOCK32" ] && "$MUXD" stop --sock "$SOCK32" 2>/dev/null || true 1527 # verdict on the way OUT and a SIGKILLed one writes nothing: the sweep
1508 [ -S "$SOCK33" ] && "$MUXD" stop --sock "$SOCK33" 2>/dev/null || true 1528 # below would then be reading a lifecycle that never ended.
1509 [ -S "$SOCK34" ] && "$MUXD" stop --sock "$SOCK34" 2>/dev/null || true 1529 #
1510 [ -S "$SOCK35" ] && "$MUXD" stop --sock "$SOCK35" 2>/dev/null || true 1530 # CONT first, and unconditionally: two legs hold their subject under
1511 [ -S "$SOCK36" ] && "$MUXD" stop --sock "$SOCK36" 2>/dev/null || true 1531 # SIGSTOP (the mute-offerer's agent, the refusal leg's daemon), and a
1512 [ -S "$SOCK37" ] && "$MUXD" stop --sock "$SOCK37" 2>/dev/null || true 1532 # stopped process does not see TERM until it runs again. On anything not
1513 [ -S "$SOCK38" ] && "$MUXD" stop --sock "$SOCK38" 2>/dev/null || true 1533 # stopped it is a no-op.
1514 [ -S "$SOCK39" ] && "$MUXD" stop --sock "$SOCK39" 2>/dev/null || true 1534 #
1515 [ -S "$SOCK40" ] && "$MUXD" stop --sock "$SOCK40" 2>/dev/null || true 1535 # `|| true` on both, for the reason every kill in this trap has always
1516 [ -S "$SOCK41" ] && "$MUXD" stop --sock "$SOCK41" 2>/dev/null || true 1536 # carried one: under `set -e` a signal to an already-dead pid would abort
1517 [ -S "$SOCK42" ] && "$MUXD" stop --sock "$SOCK42" 2>/dev/null || true 1537 # the trap itself and skip everything below it.
1518 [ -S "$SOCK43" ] && "$MUXD" stop --sock "$SOCK43" 2>/dev/null || true 1538 for _ck in $E2E_KILL; do
1519 [ -S "$SOCK44" ] && "$MUXD" stop --sock "$SOCK44" 2>/dev/null || true 1539 kill -CONT "$_ck" 2>/dev/null || true
1520 [ -S "$SOCK45" ] && "$MUXD" stop --sock "$SOCK45" 2>/dev/null || true 1540 softkill "$_ck" || true
1521 [ -S "$SOCK46" ] && "$MUXD" stop --sock "$SOCK46" 2>/dev/null || true 1541 done
1522 [ -S "$SOCK47" ] && "$MUXD" stop --sock "$SOCK47" 2>/dev/null || true
1523 [ -S "$SOCK48" ] && "$MUXD" stop --sock "$SOCK48" 2>/dev/null || true
1524 [ -S "$SOCK49" ] && "$MUXD" stop --sock "$SOCK49" 2>/dev/null || true
1525 [ -S "$SOCK50" ] && "$MUXD" stop --sock "$SOCK50" 2>/dev/null || true
1526 [ -S "$SOCK52" ] && "$MUXD" stop --sock "$SOCK52" 2>/dev/null || true
1527 [ -S "$SOCK53" ] && "$MUXD" stop --sock "$SOCK53" 2>/dev/null || true
1528 [ -S "$SOCK54" ] && "$MUXD" stop --sock "$SOCK54" 2>/dev/null || true
1529 [ -S "$SOCK55" ] && "$MUXD" stop --sock "$SOCK55" 2>/dev/null || true
1530 [ -S "$SOCK56" ] && "$MUXD" stop --sock "$SOCK56" 2>/dev/null || true
1531 [ -S "$SOCK57" ] && "$MUXD" stop --sock "$SOCK57" 2>/dev/null || true
1532 [ -S "$SOCK58" ] && "$MUXD" stop --sock "$SOCK58" 2>/dev/null || true
1533 [ -S "$SOCK59" ] && "$MUXD" stop --sock "$SOCK59" 2>/dev/null || true
1534 [ -S "$SOCK60" ] && "$MUXD" stop --sock "$SOCK60" 2>/dev/null || true
1535 [ -S "$SOCK61" ] && "$MUXD" stop --sock "$SOCK61" 2>/dev/null || true
1536 [ -S "$SOCK64" ] && "$MUXD" stop --sock "$SOCK64" 2>/dev/null || true
1537 [ -S "$SOCK66" ] && "$MUXD" stop --sock "$SOCK66" 2>/dev/null || true
1538 [ -S "$SOCK67" ] && "$MUXD" stop --sock "$SOCK67" 2>/dev/null || true
1539 [ -S "$SOCK68" ] && "$MUXD" stop --sock "$SOCK68" 2>/dev/null || true
1540 [ -S "$SOCK69" ] && "$MUXD" stop --sock "$SOCK69" 2>/dev/null || true
1541 [ -S "$SOCK70" ] && "$MUXD" stop --sock "$SOCK70" 2>/dev/null || true
1542 [ -S "$SOCK71" ] && "$MUXD" stop --sock "$SOCK71" 2>/dev/null || true
1543 [ -S "$SOCK72" ] && "$MUXD" stop --sock "$SOCK72" 2>/dev/null || true
1544 [ -S "$SOCK73" ] && "$MUXD" stop --sock "$SOCK73" 2>/dev/null || true
1545
1546 # ---- the leak sweep (hygiene kit, 6a) ---- 1542 # ---- the leak sweep (hygiene kit, 6a) ----
1547 # Here rather than at the bottom of the file, which `set -e` reaches only 1543 # Here rather than at the bottom of the file, which `set -e` reaches only
1548 # on a passing run: a leak that arrives alongside a defect is reported 1544 # on a passing run: a leak that arrives alongside a defect is reported
1549 # with it. Between the kills above and the rms below, which is the one 1545 # with it. Between the kills above and the rms below, which is the one
1550 # window where every daemon has finished its exit path and every capture 1546 # window where every daemon has finished its exit path and every capture
1551 # still exists. 1547 # still exists.
1552 reap_briefly "${DPID:-}" "$D2PID" "$D3PID" "$D4PID" "$D5PID" "$D6PID" \ 1548 #
1553 "$D7PID" "$D9PID" "$D10PID" "$D12PID" "$D13PID" "$SPID" "$TPID" \ 1549 # A killed daemon writes its verdict on the way out, so the sweep waits
1554 "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \ 1550 # for the processes to be gone first.
1555 "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \ 1551 # shellcheck disable=SC2086 # IFS is a newline: each entry is one word
1556 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ 1552 reap_briefly $E2E_KILL
1557 "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \
1558 "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \
1559 "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" "$D43PID" "$D54PID" \
1560 "$D55PID" "$D56PID" "$D57PID" "$D58PID" "$D59PID" \
1561 "$D60PID" "$D61PID" "$D62PID" "$D63PID" "$D64PID" "$D65PID" \
1562 "$D66PID" "$D67PID" "$D68PID" "$D69PID" "$D70PID" \
1563 "$D71PID" "$D72PID" "$D73PID"
1564 _leak=0 1553 _leak=0
1565 leak_sweep "$_rc" || _leak=1 1554 leak_sweep "$_rc" || _leak=1
1566 1555
1567 rm -f "$SOCK8" "$SOCK8T" "$SOCK11" "$OUT.start" "$OUT.start2" "$OUT.s8" \ 1556 # The registered artifacts: sockets, keys, generated scripts, private
1568 "$OUT.ra" "$OUT.rb" "$OUT.goal" "$OUT.g9" "$OUT.dead" \ 1557 # HOMEs, state homes. Through rm_swept, so a registered FILE still banks
1569 "$SOCK9" "$SOCK10" "$OUT.env1" "$OUT.env2" \ 1558 # its leak verdict on the way out exactly as a mid-suite removal does;
1570 "$OUT.s8.err" "$OUT.race" "$OUT.g9.err" "$OUT.env1.err" "$OUT.env2.err" \ 1559 # directories go whole.
1571 "$OUT.m7.err" "$OUT.m7b.err" "$OUT.qc.err" "$OUT.qr.err" "$OUT.qk.err" \ 1560 for _cr in $E2E_SOCK $E2E_RM; do
1572 "$SOCK5" "$SOCK6" "$SOCK7" "$PWSH" \ 1561 if [ -d "$_cr" ]; then
1573 "$OUT.p1" "$OUT.p1.early" "$OUT.pb" "$OUT.pw" "$OUT.rw" "$OUT.rwl" "$OUT.pr" \ 1562 rm -rf "$_cr"
1574 "$OUT.p1.err" "$OUT.pb.err" "$OUT.pw.err" "$OUT.rw.err" "$OUT.rwl.err" "$OUT.pr.err" \ 1563 else
1575 "$OUT.p1.d" "$OUT.pw.d" "$OUT.rw.d" "$OUT.rwl.d" "$OUT.pr.d" \ 1564 rm_swept "$_cr"
1576 "$OUT.d1.d" "$OUT.d2.d" "$OUT.d3a.d" "$OUT.d3b.d" "$OUT.d4.d" \ 1565 fi
1577 "$OUT.d9.d" "$OUT.d10.d" "$OUT.d17.d" \ 1566 done
1578 "$SOCK" "$SOCK2" "$SOCK3" "$SOCK4" "$SOCK4.second" "$QKEY" "$QKEY.bad" \ 1567 IFS=$_oifs
1579 "$OUT" "$OUT.kill" "$OUT.re" "$OUT.a" "$OUT.st" \ 1568
1580 "$OUT.b" "$OUT.via" "$OUT.dead" "$OUT.abort" "$OUT.m7" "$OUT.m7b" \ 1569 # ...and the captures, by the pattern leak_sweep just read them by rather
1581 "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY.wrong" \ 1570 # than by a list of some 350 names. The list is what failed: every
1582 "$OUT.nokey" "$SOCK4.nokey" \ 1571 # capture had to be remembered in two places, 102 of them were not, and
1583 "$OUT.doctored" "$OUT.doctored.render" "$OUT.doctored.dump" \ 1572 # by 2026-08-19 `make soak` refused to start. A pattern cannot forget a
1584 "$OUT.doctored.render.n" "$OUT.doctored.dump.n" "$OUT.doctored.diff" \ 1573 # leg.
1585 "$OUT.doctored.rvt" "$OUT.doctored.dvt"
1586 # The M12 fixture controls, spelled from $OUT rather than from $PCLOG:
1587 # that variable is assigned mid-file, so on a failure above it "${PCLOG:-}.2"
1588 # would expand to a bare ".2" — an rm target in the CWD, not a temp file.
1589 rm -f "$OUT.pc" "$OUT.pc.err" "$OUT.pc2" "$OUT.pc2.err" "$OUT.pc3" "$OUT.pc3.err" \
1590 "$OUT.pc.log" "$OUT.pc.log.2" "$OUT.pc.log.3"
1591 rm -f "$SOCK12" "$OUT.tp2.d" \
1592 "$OUT.tp2a" "$OUT.tp2a.err" "$OUT.tp2a.log" \
1593 "$OUT.tp2b" "$OUT.tp2b.err" "$OUT.tp2b.log"
1594 # tp1, including the doctored copy and the convergence residue its
1595 # cannot-fail control leaves behind BY DESIGN: that check passes when
1596 # converged_quiet fails, and a failing converged_quiet keeps its files.
1597 rm -f "$SOCK13" "$TP1SH" "$OUT.tp1.d" \
1598 "$OUT.tp1" "$OUT.tp1.err" "$OUT.tp1.log" \
1599 "$OUT.tp1.doc" "$OUT.tp1.doc.render" "$OUT.tp1.doc.dump" \
1600 "$OUT.tp1.doc.render.n" "$OUT.tp1.doc.dump.n" "$OUT.tp1.doc.diff" \
1601 "$OUT.tp1.doc.rvt" "$OUT.tp1.doc.dvt"
1602 # M13 auto-start + stop.
1603 rm -f "$SOCK14" "$SOCK15" \
1604 "$OUT.as" "$OUT.as.err" "$OUT.as2" "$OUT.as2.err" \
1605 "$OUT.stop" "$OUT.stop2" \
1606 "$OUT.pa" "$OUT.pa.err" "$OUT.pa.log"
1607 # The side-channel leg: its capture, the fixture's log, and the script
1608 # the session runs. Spelled from $OUT rather than from a variable of its
1609 # own, for the reason the M12 controls above are.
1610 rm -f "$OUT.clip" "$OUT.clip.err" "$OUT.clip.log" "$OUT.clip.sh"
1611 # The title leg's identical four. Its own rm_swept runs only on success,
1612 # so without this line the residue survives exactly the runs where the
1613 # leg failed and somebody is looking at it.
1614 rm -f "$OUT.title" "$OUT.title.err" "$OUT.title.log" "$OUT.title.sh"
1615 # The wheel pair: two captures, two fixture logs, two session scripts
1616 # and the daemon logs beside them.
1617 rm -f "$OUT.whl" "$OUT.whl.err" "$OUT.whl.log" "$OUT.whl.d" "$WHEELSH" \
1618 "$OUT.mse" "$OUT.mse.err" "$OUT.mse.log" "$OUT.mse.d" "$MOUSESH" \
1619 "$SOCK41" "$OUT.zw.d" "$OUT.zwcap" "$OUT.zwcap.err" "$OUT.zwpc" \
1620 "$OUT.zwcapg" "$OUT.zwstop" "$ZWHEELSH" \
1621 "$SOCK42" "$OUT.zm2.d" "$OUT.zm2cap" "$OUT.zm2cap.err" "$OUT.zm2pc" \
1622 "$OUT.zm2capg" "$OUT.zm2stop" "$ZMOUSESH" \
1623 "$SOCK43" "$SOCK44" "$OUT.zh.da" "$OUT.zh.db" "$OUT.zhcap" \
1624 "$OUT.zhcap.err" "$OUT.zhpc" "$OUT.zhstopa" "$OUT.zhstopb" \
1625 "$ZHASH" "$ZHBSH" "$ZHFLAG" "$ZHSCRIPT" \
1626 "$SOCK33" "$SOCK34" "$OUT.whlcap" "$OUT.msecap" \
1627 "$OUT.whlstop" "$OUT.msestop"
1628 rm -f "$OUT.pgr" "$OUT.pgr.err" "$OUT.pgr.log" "$OUT.pgr.d" "$LESSSH" \
1629 "$LESSDATA" "$OUT.pgrcap" "$OUT.pgrstop" "$SOCK35" \
1630 "$OUT.pipe" "$OUT.pipe.d" "$OUT.pipecap" "$OUT.pipestop" "$SOCK36" \
1631 "$OUT.sb2.d" "$OUT.sb2a" "$OUT.sb2a.err" "$OUT.sb2b" "$OUT.sb2b.err" \
1632 "$OUT.sb2cap" "$OUT.sb2cap.err" "$OUT.sb2pc" "$OUT.sb2stop" "$SOCK53" \
1633 "$OUT.fs.d" "$OUT.fsa" "$OUT.fsa.err" "$OUT.fsb" "$OUT.fsb.err" \
1634 "$OUT.fscap" "$OUT.fscap.err" "$OUT.fspc" "$OUT.fsfa" "$OUT.fsfb" \
1635 "$OUT.fsgrid" "$OUT.fsstop" "$SOCK54" \
1636 "$OUT.rsz.d" "$OUT.rszcap" "$OUT.rszcap.err" "$OUT.rszpc" \
1637 "$OUT.rsza" "$OUT.rsza.err" "$OUT.rszb" "$OUT.rszb.err" \
1638 "$OUT.rszsta" "$OUT.rszstb" "$OUT.rszstop" "$SOCK55" \
1639 "$OUT.spc.d" "$OUT.spca" "$OUT.spca.err" "$OUT.spcb" "$OUT.spcb.err" \
1640 "$OUT.spccap" "$OUT.spccap.err" "$OUT.spcpc" "$OUT.spcgrid" \
1641 "$OUT.spcstop" "$SOCK56" \
1642 "$OUT.hjk.d" "$OUT.hjkcap" "$OUT.hjkcap.err" "$OUT.hjkpc" \
1643 "$OUT.hjkfa" "$OUT.hjkfb" "$OUT.hjkfc" "$OUT.hjkstop" "$SOCK57" \
1644 "$OUT.spl.d" "$OUT.splcap" "$OUT.splcap.err" "$OUT.splpc" \
1645 "$OUT.splsta" "$OUT.splstop" "$SOCK58" \
1646 "$OUT.lpr.d" "$OUT.lpra" "$OUT.lpra.err" "$OUT.lprb" "$OUT.lprb.err" \
1647 "$OUT.lprcap1" "$OUT.lprcap1.err" "$OUT.lprpc1" \
1648 "$OUT.lprcap2" "$OUT.lprcap2.err" "$OUT.lprpc2" \
1649 "$OUT.lprfa" "$OUT.lprfb" "$OUT.lprstop" "$SOCK59" \
1650 "$OUT.lph.d" "$OUT.lpha" "$OUT.lpha.err" "$OUT.lphb" "$OUT.lphb.err" \
1651 "$OUT.lphc" "$OUT.lphc.err" "$OUT.lphcap1" "$OUT.lphcap1.err" \
1652 "$OUT.lphpc1" "$OUT.lphcap2" "$OUT.lphcap2.err" "$OUT.lphpc2" \
1653 "$OUT.lphfa" "$OUT.lphstop" "$SOCK60" \
1654 "$OUT.lpd.d" "$OUT.lpda" "$OUT.lpda.err" \
1655 "$OUT.lpdcap" "$OUT.lpdcap.err" "$OUT.lpdpc" \
1656 "$OUT.lpdfa" "$OUT.lpdstop" "$SOCK61" \
1657 "$OUT.pra.d" "$OUT.prb.d" "$OUT.pra" "$OUT.pra.err" "$OUT.prcap" "$OUT.prcap.err" \
1658 "$OUT.prgrid" \
1659 "$OUT.prpc" "$OUT.prfb" "$OUT.prastop" "$OUT.prbstop" "$SOCK62" "$SOCK63" \
1660 "$OUT.hyd.d" "$OUT.hydcap" "$OUT.hydcap.err" "$OUT.hydpc" "$OUT.hydgrid" \
1661 "$OUT.hydsta" "$OUT.hydstb" "$OUT.hydghost" "$OUT.hydpre" "$OUT.hydfa" \
1662 "$OUT.hydstop" "$OUT.hydfcap" "$OUT.hydfcap.err" "$OUT.hydfpc" \
1663 "$OUT.hydfold" "$OUT.hydfgrid" "$SOCK64" "$HYKEY" \
1664 "$OUT.ref.d" "$OUT.refrun" "$OUT.refstatus" "$OUT.refsend" \
1665 "$OUT.refalive" "$OUT.refstop" "$SOCK66" \
1666 "$OUT.wg.d" "$OUT.wgh" "$OUT.wgws" "$OUT.wgws.err" \
1667 "$OUT.wgws2" "$OUT.wgws2.err" "$OUT.wgpre" "$OUT.wgsta" \
1668 "$OUT.wgghost" "$OUT.wgstop" "$OUT.wgsta0" "$OUT.wgexit" \
1669 "$OUT.wgws3" "$OUT.wgws3.err" "$SOCK67" "$WGKEY" \
1670 "$OUT.sp.d" "$OUT.sph" "$OUT.spws" "$OUT.spws.err" \
1671 "$OUT.spghost" "$OUT.spstop" "$SOCK68" \
1672 "$OUT.up.d" "$OUT.upcap" "$OUT.upcap.err" "$OUT.uppc" \
1673 "$OUT.upm1" "$OUT.upm2" "$OUT.upm3" "$OUT.upst1" "$OUT.upst2" \
1674 "$OUT.upref1" "$OUT.upref2" "$OUT.upok" "$OUT.upsta" "$OUT.upstop" \
1675 "$SOCK69" "$UPBIN" \
1676 "$OUT.url.d" "$OUT.urlm1" "$OUT.urlm2" "$OUT.urlup" "$OUT.urlstop" \
1677 "$SOCK70" \
1678 "$OUT.uag.d" "$OUT.uag" "$OUT.uag.err" "$OUT.uag.in" "$OUT.uagenv" \
1679 "$OUT.uagup" "$OUT.uagstop" "$SOCK71" \
1680 "$UPAGENT" "$UPAGKEY" "$UPAGKEY.pub" \
1681 "$OUT.uqc.d" "$OUT.uqc" "$OUT.uqc.err" "$OUT.uqc.in" "$OUT.uqup" \
1682 "$OUT.uqsta" "$OUT.uqstop" "$SOCK72" "$UPKEY" \
1683 "$OUT.sg" "$OUT.sg.d" "$OUT.sg.stop" "$OUT.sg.sh" "$SOCK73"
1684 # The upgrade leg's private HOME, for the reason it has one: a session
1685 # shell that read the developer's rc files would set its own title.
1686 rm -rf "$UPHOME"
1687 # ...and the state homes those legs read their walls back out of, plus
1688 # the spin leg's shim dir (its dial log lives inside it).
1689 rm -rf "$HYSTATE" "$WGSTATE" "$SPSTATE" "$SPDIR"
1690 # ...and the non-tty capture that leg's session feeds.
1691 rm -f "$OUT.nogate"
1692 # ...and its other half: the paste capture and the file nvim wrote, which
1693 # IS that scenario's assertion rather than a log beside it.
1694 rm -f "$OUT.paste" "$OUT.paste.err" "$OUT.paste.log" "$OUT.pasted.txt"
1695 # ...and the bell leg's five, plus the daemon capture and socket, since
1696 # that leg runs a daemon of its own.
1697 # 1574 #
1698 # All six are ALSO rm_swept mid-suite on success, which is not a conflict: 1575 # Green runs only. A failing run keeps its captures — the .render/.dump/
1699 # rm_swept banks a capture's LEAK verdict into $LEAKBANK before deleting 1576 # .diff files a failed assert_converged leaves behind are that failure's
1700 # it, and the sweep above reads the bank for everything that no longer 1577 # evidence, and soak moves the whole set into its failure dir for the
1701 # exists. So the daemon capture's verdict survives its own deletion, and 1578 # next run's sake.
1702 # this line is the backstop for the runs where that rm_swept never ran — 1579 if [ "$_rc" -eq 0 ]; then
1703 # the failing ones, which are exactly the runs somebody is looking at. The 1580 for _co in "$OUT" "$OUT".*; do
1704 # tp1 leg sweeps its own daemon capture the same way (see its rm_swept), 1581 [ -e "$_co" ] || continue
1705 # so this is that leg's convention rather than a new one. 1582 rm -rf "$_co"
1706 rm -f "$SOCK24" "$OUT.d21.d" \ 1583 done
1707 "$OUT.bell" "$OUT.bell.err" "$OUT.bell.log" "$OUT.bell.sh" 1584 fi
1708 # M14 handoff. The shim, both runtime dirs (each holding its daemon's
1709 # socket), the second key, the unusable config home, and the five
1710 # captures.
1711 rm -f "$HKEY" "$HCFGBAD" \
1712 "$OUT.h1" "$OUT.h1.err" "$OUT.h2" "$OUT.h2.err" "$OUT.h3" "$OUT.h3.err" \
1713 "$OUT.h4" "$OUT.h4.err" "$OUT.h5" "$OUT.h5.err"
1714 rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2"
1715 # M-web captures. Their hubs and daemons were killed with everyone
1716 # else's, up with the kills — every kill in this trap now precedes every
1717 # rm, so the leak sweep sitting between the two reads captures that are
1718 # still on disk and daemons that have already written their verdict.
1719 rm -f "$SOCK18" "$SOCK19" "$SOCK20" \
1720 "$OUT.weba" "$OUT.weba.err" "$OUT.weba.d" \
1721 "$OUT.web1x1" "$OUT.web1x1.err" "$OUT.web1x1.log" \
1722 "$OUT.webb" "$OUT.webb.err" "$OUT.webb.d" "$OUT.webh" \
1723 "$OUT.webws" "$OUT.webws.err" "$OUT.webws.n" "$OUT.webws.dump" \
1724 "$OUT.webws.dump.n" "$OUT.webws.diff" "$OUT.webws.doc" \
1725 "$OUT.webevil" "$OUT.webevil.err" \
1726 "$OUT.webc" "$OUT.webc.err" "$OUT.webc.d" "$OUT.webc2" "$OUT.webc2.err" \
1727 "$OUT.webc2.d" "$OUT.webh2" "$OUT.webws2" "$OUT.webws2.err" \
1728 "$OUT.webws2.n" "$OUT.webws2.dump" "$OUT.webws2.dump.n" \
1729 "$OUT.webws2.diff" "$OUT.webws2.doc" \
1730 "$OUT.webstop" "$OUT.webstop2" "$OUT.webstop3" "$OUT.webstop4"
1731 # M18 captures, both blocks, including the files assert_ws_converged
1732 # derives from each stand-in's dump (.n/.dump/.dump.n/.diff/.doc).
1733 rm -f "$SOCK21" "$SOCK22" \
1734 "$OUT.m18.d" "$OUT.m18a" "$OUT.m18a.err" "$OUT.m18b" "$OUT.m18b.err" \
1735 "$OUT.m18ax" "$OUT.m18ax.err" "$OUT.m18stats" "$OUT.m18zz" \
1736 "$OUT.m18mst" "$OUT.m18mcap" \
1737 "$OUT.m18stop" "$OUT.m18w.d" "$OUT.m18wh" \
1738 "$OUT.m18wa" "$OUT.m18wa.err" "$OUT.m18wb" "$OUT.m18wb.err" \
1739 "$OUT.m18ws0" "$OUT.m18ws0.err" "$OUT.m18ws0.n" "$OUT.m18ws0.dump" \
1740 "$OUT.m18ws0.dump.n" "$OUT.m18ws0.diff" "$OUT.m18ws0.doc" \
1741 "$OUT.m18ws1" "$OUT.m18ws1.err" "$OUT.m18ws1.n" "$OUT.m18ws1.dump" \
1742 "$OUT.m18ws1.dump.n" "$OUT.m18ws1.diff" "$OUT.m18ws1.doc" \
1743 "$OUT.m18wstop" \
1744 "$SOCK23" "$M18KEY" "$OUT.m18q.d" "$OUT.m18qa" "$OUT.m18qa.err" \
1745 "$OUT.m18qb" "$OUT.m18qb.err" "$OUT.m18qx" "$OUT.m18qx.err" \
1746 "$OUT.m18qstop"
1747 # The dynamic-wall leg: its daemon capture, the three hub captures (one
1748 # per incarnation, each kept apart so a failure names WHICH start went
1749 # wrong), the CLI and stand-in captures, and the state home holding the
1750 # wall file this leg reads back.
1751 rm -f "$SOCK25" "$OUT.dw.d" "$OUT.dwh" "$OUT.dwh2" "$OUT.dwh3" \
1752 "$OUT.dwcli" "$OUT.dwcli.err" \
1753 "$OUT.dwws" "$OUT.dwws.err" "$OUT.dwdead" "$OUT.dwdead.err" \
1754 "$OUT.dwstop"
1755 rm -rf "$DWSTATE"
1756 # The attach-history block: its daemon capture, the client captures, and
1757 # the state home holding the wall file both its scenarios read back.
1758 rm -f "$SOCK40" "$OUT.wh.d" "$OUT.wh1" "$OUT.wh1.err" "$OUT.wh2" \
1759 "$OUT.wh2.err" "$OUT.whb" "$OUT.whb.err" "$OUT.whc" "$OUT.whc.err" \
1760 "$OUT.whro" "$OUT.whro.err" "$OUT.whadd" "$OUT.whrm" "$OUT.whst" \
1761 "$OUT.whself" "$OUT.whph" \
1762 "$OUT.whxpc" "$OUT.whxcap" "$OUT.whxcap.err" "$OUT.whxst" \
1763 "$OUT.whxpc2" "$OUT.whxcap2" "$OUT.whxcap2.err" \
1764 "$OUT.whxa" "$OUT.whxa.err" "$OUT.whxb" "$OUT.whxb.err" \
1765 "$OUT.whstop"
1766 rm -rf "$WHSTATE" "$WHBAD" "$WHXSTATE"
1767 # fill_sessions' captures, one per filled name, and the scratch state
1768 # homes the fills write their own wall lines into. A glob rather than a
1769 # list because the names are generated: 28 per caller, and a list would
1770 # go stale the moment max_sessions moves again.
1771 rm -f "$OUT".fill.*
1772 rm -rf "$OUT.nswfill" "$OUT.whfill"
1773 # ...and the wall-chord leg's own state home, which holds the two-line
1774 # wall that leg builds and reads back.
1775 rm -rf "$M4STATE"
1776 # The convergence block: three daemons, three state homes, and the
1777 # captures each leg names in its own failures.
1778 rm -f "$SOCK45" "$OUT.cv.d" "$OUT.cvside" "$OUT.cvside.err" "$OUT.cvinj" \
1779 "$OUT.cvcap" "$OUT.cvcap.err" "$OUT.cvpc" "$OUT.cvstop"
1780 rm -f "$SOCK46" "$OUT.rg.d" "$OUT.rgtwo" "$OUT.rgtwo.err" \
1781 "$OUT.rgcap" "$OUT.rgcap.err" "$OUT.rgpc" "$OUT.rgcapt" "$OUT.rgcap0" \
1782 "$OUT.rgstop"
1783 rm -f "$SOCK47" "$OUT.xe.d" "$OUT.xe1" "$OUT.xe1.err" \
1784 "$OUT.xecap" "$OUT.xecap.err" "$OUT.xepc" "$OUT.xe2" "$OUT.xe2.err" \
1785 "$OUT.xestop"
1786 rm -rf "$CVSTATE" "$RGSTATE" "$RGOTHER" "$XESTATE"
1787 # The agent legs. The private key and its public half go too: this file
1788 # made them, and a keypair left in $TMPDIR is litter with a lifetime.
1789 # The agent's own socket is unlinked by the ssh-agent killed above only
1790 # when it exits cleanly, so it is named here as well.
1791 rm -f "$SOCK48" "$AGENT48" "$AGENT48KEY" "$AGENT48KEY.pub" \
1792 "$OUT.agt.d" "$OUT.agt.env" "$OUT.agt" "$OUT.agt.err" "$OUT.agt.log" \
1793 "$OUT.agtn" "$OUT.agtn.err" "$OUT.agtn.log" "$OUT.agtn.d" "$OUT.agtstop"
1794 rm -f "$SOCK49" "$AGENT49A" "$AGENT49B" \
1795 "$AGENT49AKEY" "$AGENT49AKEY.pub" "$AGENT49BKEY" "$AGENT49BKEY.pub" \
1796 "$OUT.flip.d" \
1797 "$OUT.flipa.env" "$OUT.flipa" "$OUT.flipa.err" "$OUT.flipa.log" \
1798 "$OUT.flipb.env" "$OUT.flipb" "$OUT.flipb.err" "$OUT.flipb.log"
1799 # The M1-M6 prefix/wall captures and the agent legs' three. Absent
1800 # from this trap until 2026-08-22, which is why every green run since
1801 # those features landed left 102 files in $TMPDIR — enough, by 08-19, that
1802 # `make soak` refused to start and no soak had run since. The guard below
1803 # is what makes the next omission say so instead of accumulating.
1804 # agent forwarding: the nested-agent leg and the three no-offer captures.
1805 rm -f "$OUT.agtnest" "$OUT.agtnest.err" "$OUT.agtnest.log" "$OUT.anoag" \
1806 "$OUT.anoag2" "$OUT.anoag3"
1807 # agent forwarding: the mute-offerer leg.
1808 rm -f "$OUT.agtmute" "$OUT.agtmute.err" "$OUT.agtmute.log" "$OUT.agtmute.env" \
1809 "$OUT.agtmute.d"
1810 # the CLI wall (cw*) — captures, injections and stop files.
1811 rm -f "$OUT.cwa" "$OUT.cwa.err" "$OUT.cwall.d" "$OUT.cwb" "$OUT.cwb.err" \
1812 "$OUT.cwcap" "$OUT.cwcap.err" "$OUT.cwinj" "$OUT.cwpc" "$OUT.cwst" \
1813 "$OUT.cwstop"
1814 # the new-session-from-the-wall leg.
1815 rm -f "$OUT.nsw" "$OUT.nswcap" "$OUT.nswcap3" "$OUT.nsw.d" \
1816 "$OUT.nsw.err" "$OUT.nsw.log" "$OUT.nswst" "$OUT.nswst4" \
1817 "$OUT.nswstop"
1818 # the Ctrl-\ prefix leg.
1819 rm -f "$OUT.pfx" "$OUT.pfx.d" "$OUT.pfx.err" "$OUT.pfx.log" "$OUT.pfxst" \
1820 "$OUT.pfxstop"
1821 # the bell leg's ring captures.
1822 rm -f "$OUT.ring" "$OUT.ringcap" "$OUT.ring.d" "$OUT.ring.err" \
1823 "$OUT.ring.log" "$OUT.ringstop"
1824 # self-attach refusal, and the wall file it reads.
1825 rm -f "$OUT.sacap1" "$OUT.sacap2" "$OUT.sacap3" "$OUT.sa.d" "$OUT.saenv" \
1826 "$OUT.saself" "$OUT.sast" "$OUT.sastop" "$OUT.sawall"
1827 # the dead-tile leg.
1828 rm -f "$OUT.zda" "$OUT.zda.err" "$OUT.zdcap" "$OUT.zdcap.err" \
1829 "$OUT.zd.d" "$OUT.zdpc" "$OUT.zdstop" "$OUT.zdcapa" "$OUT.zdgrid"
1830 # focus skip, and its watchers.
1831 rm -f "$OUT.zsa" "$OUT.zsa.err" "$OUT.zsb" "$OUT.zsb.err" "$OUT.zscap" \
1832 "$OUT.zscap.err" "$OUT.zs.d" "$OUT.zsfa" "$OUT.zsfb" "$OUT.zspc" \
1833 "$OUT.zssta" "$OUT.zsstb" "$OUT.zsstop" "$OUT.zswatch"
1834 rm -f "$OUT.swcap" "$OUT.swcap.err" "$OUT.swpc"
1835 rm -f "$OUT.inband" "$OUT.inband.err" "$OUT.inband.log" "$OUT.inband.d" "$OUT.inbstop"
1836 # The cursor-ownership leg: its daemon socket, the three session pipe
1837 # clients, the ptyclient capture and the stop witness.
1838 rm -f "$SOCK52" "$OUT.cu.d" \
1839 "$OUT.cua" "$OUT.cua.err" "$OUT.cub" "$OUT.cub.err" \
1840 "$OUT.cuc" "$OUT.cuc.err" \
1841 "$OUT.cucap" "$OUT.cucap.err" "$OUT.cupc" "$OUT.custop"
1842 rm -f "$SOCK50" "$OUT.wmse.d" "$OUT.wmsa" "$OUT.wmsa.err" "$OUT.wmsb" \
1843 "$OUT.wmsb.err" "$OUT.wmcap" "$OUT.wmcap.err" "$OUT.wmpc" \
1844 "$OUT.wmcap2" "$OUT.wmcap2.err" "$OUT.wmpc2" "$OUT.wmfa" "$OUT.wmfb" \
1845 "$OUT.wmcap3" "$OUT.wmcap3.err" "$OUT.wmpc3" \
1846 "$OUT.wmcap4" "$OUT.wmcap4.err" "$OUT.wmpc4" \
1847 "$OUT.wmcap5" "$OUT.wmcap5.err" "$OUT.wmpc5" \
1848 "$OUT.selcap" "$OUT.selcap.err" "$OUT.selpc" \
1849 "$OUT.wmstop"
1850 # The convergence files a FAILING assert_converged leaves behind
1851 # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not
1852 # chased here: on a failing run they are the evidence.
1853 rm -rf "$XDG_CONFIG_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME" "${NOKEY_CFG:-}"
1854 rm -f "$LEAKBANK"
1855 # Residue, in the leak sweep's shape and for its reason: a run that PASSED 1585 # Residue, in the leak sweep's shape and for its reason: a run that PASSED
1856 # must leave nothing behind, and this trap is the last place that can 1586 # must leave nothing behind, and this trap is the last place that can
1857 # still say so. Matched by pattern rather than by the list above, because 1587 # still say so. Pinned to this run's pid so a suite running concurrently
1858 # the list is what failed — every capture has to be remembered in two 1588 # is never counted, and never deleted.
1859 # places, and 102 of them were not. Pinned to this run's pid so a suite
1860 # running concurrently is never counted, and never deleted.
1861 # 1589 #
1862 # Green runs only. A failing run keeps everything: the convergence files 1590 # What it catches now is a path that is neither registered nor spelled
1863 # noted above are its evidence, and soak sweeps them into its failure dir. 1591 # from $OUT — the one shape both mechanisms above are blind to.
1864 _stray=0 1592 _stray=0
1865 if [ "$_rc" -eq 0 ]; then 1593 if [ "$_rc" -eq 0 ]; then
1866 _left=$(find "${TMPDIR:-/tmp}" -maxdepth 1 \ 1594 _left=$(find "${TMPDIR:-/tmp}" -maxdepth 1 \
@@ -1870,10 +1598,11 @@ cleanup() {
1870 if [ "$_n" -eq 1 ]; then _w=file; else _w=files; fi 1598 if [ "$_n" -eq 1 ]; then _w=file; else _w=files; fi
1871 echo "e2e FAIL: the suite passed but left $_n $_w in ${TMPDIR:-/tmp}:" 1599 echo "e2e FAIL: the suite passed but left $_n $_w in ${TMPDIR:-/tmp}:"
1872 printf '%s\n' "$_left" | sed 's/^/ /' 1600 printf '%s\n' "$_left" | sed 's/^/ /'
1873 echo " A capture is removed by the scenario that made it or" 1601 echo " A capture is spelled from \$OUT and swept by pattern;"
1874 echo " by this trap. One added to neither is invisible until" 1602 echo " anything else is registered where it is created, with"
1875 echo " soak refuses to start; that is what this catches." 1603 echo " defer_rm, defer_sock or start_daemon. A path that is"
1876 printf '%s\n' "$_left" | xargs -r rm -f 1604 echo " neither is invisible until soak refuses to start."
1605 printf '%s\n' "$_left" | xargs -r rm -rf
1877 _stray=1 1606 _stray=1
1878 fi 1607 fi
1879 fi 1608 fi
@@ -1915,10 +1644,8 @@ SUM2=$(sha256sum "$KEYOUT")
1915 [ "$SUM1" = "$SUM2" ] || { echo "e2e FAIL: refused keygen still changed the key"; exit 1; } 1644 [ "$SUM1" = "$SUM2" ] || { echo "e2e FAIL: refused keygen still changed the key"; exit 1; }
1916 ok "keygen creates once, 0600 in a 0700 dir, refuses twice" 1645 ok "keygen creates once, 0600 in a 0700 dir, refuses twice"
1917 1646
1918 "$MUXD" run --sock "$SOCK" --shell /bin/sh > "$OUT.d1.d" 2>&1 & 1647 start_daemon "$SOCK" "$OUT.d1.d" "socket never appeared" --shell /bin/sh
1919 DPID=$! 1648 D1PID=$DPID
1920
1921 wait_sock "$SOCK" "$OUT.d1.d" "socket never appeared"
1922 1649
1923 # Client with piped stdio: types a command, waits for its output, detaches 1650 # Client with piped stdio: types a command, waits for its output, detaches
1924 # with the Ctrl-\ chord (prefix, then prefix again for detach). 1651 # with the Ctrl-\ chord (prefix, then prefix again for detach).
@@ -1934,7 +1661,7 @@ pipe_detach
1934 } 1661 }
1935 1662
1936 # 3. Detach left the daemon running. 1663 # 3. Detach left the daemon running.
1937 kill -0 "$DPID" || { echo "e2e FAIL: daemon died on detach"; exit 1; } 1664 kill -0 "$D1PID" || { echo "e2e FAIL: daemon died on detach"; exit 1; }
1938 1665
1939 # 4. The M11 claim itself: the screen the client painted equals the 1666 # 4. The M11 claim itself: the screen the client painted equals the
1940 # screen the daemon holds — same engine, same formatter, both formats. 1667 # screen the daemon holds — same engine, same formatter, both formats.
@@ -1980,10 +1707,11 @@ ok "styled content survives the paint path"
1980 # mid-escape-sequence, so a half-drawn capture is the expected shape here. 1707 # mid-escape-sequence, so a half-drawn capture is the expected shape here.
1981 { printf 'seq 1 60\n'; sleep 2; } | "$MUX" --sock "$SOCK" > "$OUT.kill" & 1708 { printf 'seq 1 60\n'; sleep 2; } | "$MUX" --sock "$SOCK" > "$OUT.kill" &
1982 CPID=$! 1709 CPID=$!
1710 defer_kill "$CPID"
1983 sleep 1 1711 sleep 1
1984 hardkill "$CPID" 1712 hardkill "$CPID"
1985 sleep 1 1713 sleep 1
1986 kill -0 "$DPID" || { echo "e2e FAIL: daemon died after client kill -9"; exit 1; } 1714 kill -0 "$D1PID" || { echo "e2e FAIL: daemon died after client kill -9"; exit 1; }
1987 1715
1988 { sleep 1; printf '\034\034'; } | timeout 30 "$MUX" --sock "$SOCK" > "$OUT.re" 1716 { sleep 1; printf '\034\034'; } | timeout 30 "$MUX" --sock "$SOCK" > "$OUT.re"
1989 grep -q "60" "$OUT.re" || { 1717 grep -q "60" "$OUT.re" || {
@@ -1998,9 +1726,11 @@ rm_swept "$OUT.kill" "$OUT.re"
1998 { sleep 0.5; printf 'printf "m5-%%s\\n" both\n'; sleep 2.5; printf '\034\034'; } | \ 1726 { sleep 0.5; printf 'printf "m5-%%s\\n" both\n'; sleep 2.5; printf '\034\034'; } | \
1999 "$MUX" --sock "$SOCK" > "$OUT.a" & 1727 "$MUX" --sock "$SOCK" > "$OUT.a" &
2000 APID=$! 1728 APID=$!
1729 defer_kill "$APID"
2001 { sleep 4; printf 'printf "m5-%%s\\n" after-a-left\n'; sleep 2.5; printf '\034\034'; } | \ 1730 { sleep 4; printf 'printf "m5-%%s\\n" after-a-left\n'; sleep 2.5; printf '\034\034'; } | \
2002 "$MUX" --sock "$SOCK" > "$OUT.b" & 1731 "$MUX" --sock "$SOCK" > "$OUT.b" &
2003 BPID=$! 1732 BPID=$!
1733 defer_kill "$BPID"
2004 wait "$APID" "$BPID" 1734 wait "$APID" "$BPID"
2005 1735
2006 # The marker text never appears in the echoed command line ("m5-%s" plus a 1736 # The marker text never appears in the echoed command line ("m5-%s" plus a
@@ -2010,7 +1740,7 @@ grep -q "m5-both" "$OUT.b" || { echo "e2e FAIL: client B missing shared output";
2010 grep -q "m5-after-a-left" "$OUT.b" || { 1740 grep -q "m5-after-a-left" "$OUT.b" || {
2011 echo "e2e FAIL: client B lost its input path after A detached"; exit 1; 1741 echo "e2e FAIL: client B lost its input path after A detached"; exit 1;
2012 } 1742 }
2013 kill -0 "$DPID" || { echo "e2e FAIL: daemon died in two-client scenario"; exit 1; } 1743 kill -0 "$D1PID" || { echo "e2e FAIL: daemon died in two-client scenario"; exit 1; }
2014 # B's capture, not A's: A detached before B's marker landed, so A's stream ends 1744 # B's capture, not A's: A detached before B's marker landed, so A's stream ends
2015 # on a grid the daemon has since moved past. B saw both, and its convergence 1745 # on a grid the daemon has since moved past. B saw both, and its convergence
2016 # covers the same grid. 1746 # covers the same grid.
@@ -2031,7 +1761,7 @@ pipe_mux "$OUT.via" "" env XDG_RUNTIME_DIR=/nonexistent-mux-e2e "$MUX" --via "$M
2031 pipe_send 'printf "m6-%%s\\n" via-pipe\n' 1761 pipe_send 'printf "m6-%%s\\n" via-pipe\n'
2032 await_out "$OUT.via" "m6-via-pipe" "--via transport" 1762 await_out "$OUT.via" "m6-via-pipe" "--via transport"
2033 pipe_detach 1763 pipe_detach
2034 kill -0 "$DPID" || { echo "e2e FAIL: daemon died in --via scenario"; exit 1; } 1764 kill -0 "$D1PID" || { echo "e2e FAIL: daemon died in --via scenario"; exit 1; }
2035 assert_converged "$OUT.via" "$SOCK" "via transport" 1765 assert_converged "$OUT.via" "$SOCK" "via transport"
2036 rm_swept "$OUT.via" 1766 rm_swept "$OUT.via"
2037 1767
@@ -2111,14 +1841,14 @@ rm_swept "$OUT.dead" "$OUT.long" "$OUT.longc"
2111 # of the transport would abort here instead (SIGABRT = 134). 1841 # of the transport would abort here instead (SIGABRT = 134).
2112 # No convergence: the client exits while reconnecting and its daemon is dead, 1842 # No convergence: the client exits while reconnecting and its daemon is dead,
2113 # so there is nothing left to dump against. 1843 # so there is nothing left to dump against.
2114 "$MUXD" run --sock "$SOCK2" --shell /bin/sh > "$OUT.d2.d" 2>&1 & 1844 start_daemon "$SOCK2" "$OUT.d2.d" "second socket never appeared" --shell /bin/sh
2115 D2PID=$! 1845 D2PID=$DPID
2116 wait_sock "$SOCK2" "$OUT.d2.d" "second socket never appeared"
2117 1846
2118 set +e 1847 set +e
2119 { printf 'echo m7-abort-live\n'; sleep 3; printf '\034'; sleep 2; } | \ 1848 { printf 'echo m7-abort-live\n'; sleep 3; printf '\034'; sleep 2; } | \
2120 timeout 20 "$MUX" --sock "$SOCK2" > "$OUT.abort" 2>&1 & 1849 timeout 20 "$MUX" --sock "$SOCK2" > "$OUT.abort" 2>&1 &
2121 CLIPID=$! 1850 CLIPID=$!
1851 defer_kill "$CLIPID"
2122 # Let the session establish, then take the daemon away mid-session. 1852 # Let the session establish, then take the daemon away mid-session.
2123 sleep 2 1853 sleep 2
2124 hardkill "$D2PID" 1854 hardkill "$D2PID"
@@ -2160,7 +1890,7 @@ PPID_PROXY=$(proxy_pid "$SOCK")
2160 kill -9 "$PPID_PROXY" 1890 kill -9 "$PPID_PROXY"
2161 # The tear must have hit the transport only; the daemon is what we are 1891 # The tear must have hit the transport only; the daemon is what we are
2162 # proving survives, so say so out loud rather than inferring it later. 1892 # proving survives, so say so out loud rather than inferring it later.
2163 kill -0 "$DPID" || { echo "e2e FAIL: the tear killed the daemon, not the proxy"; exit 1; } 1893 kill -0 "$D1PID" || { echo "e2e FAIL: the tear killed the daemon, not the proxy"; exit 1; }
2164 1894
2165 await_repaint "$OUT.m7" "$PAINTS_BEFORE" "m7 client never resumed after the transport was killed" 1895 await_repaint "$OUT.m7" "$PAINTS_BEFORE" "m7 client never resumed after the transport was killed"
2166 pipe_send 'printf "m7-%%s\\n" after\n' 1896 pipe_send 'printf "m7-%%s\\n" after\n'
@@ -2181,9 +1911,8 @@ rm_swept "$OUT.m7" "$OUT.m7.err"
2181 # one on the same path. The seq the client holds belongs to a session that no 1911 # one on the same path. The seq the client holds belongs to a session that no
2182 # longer exists, so the epoch fence must refuse it and serve a snapshot of the 1912 # longer exists, so the epoch fence must refuse it and serve a snapshot of the
2183 # fresh session instead. Needs its own daemon, since this one gets killed. 1913 # fresh session instead. Needs its own daemon, since this one gets killed.
2184 "$MUXD" run --sock "$SOCK3" --shell /bin/sh > "$OUT.d3a.d" 2>&1 & 1914 start_daemon "$SOCK3" "$OUT.d3a.d" "restart-scenario socket never appeared" --shell /bin/sh
2185 D3PID=$! 1915 D3PID=$DPID
2186 wait_sock "$SOCK3" "$OUT.d3a.d" "restart-scenario socket never appeared"
2187 1916
2188 pipe_mux "$OUT.m7b" "$OUT.m7b.err" timeout 40 "$MUX" --sock "$SOCK3" 1917 pipe_mux "$OUT.m7b" "$OUT.m7b.err" timeout 40 "$MUX" --sock "$SOCK3"
2189 pipe_send 'printf "m7b-%%s\\n" pre-restart\n' 1918 pipe_send 'printf "m7b-%%s\\n" pre-restart\n'
@@ -2197,9 +1926,8 @@ wait_for "$OUT.m7b" "m7b-pre-restart" 20 || {
2197 PAINTS_BEFORE=$(repaints "$OUT.m7b") 1926 PAINTS_BEFORE=$(repaints "$OUT.m7b")
2198 hardkill "$D3PID" 1927 hardkill "$D3PID"
2199 sleep 0.5 1928 sleep 0.5
2200 "$MUXD" run --sock "$SOCK3" --shell /bin/sh > "$OUT.d3b.d" 2>&1 & 1929 start_daemon "$SOCK3" "$OUT.d3b.d" "daemon did not rebind the stale socket" --shell /bin/sh
2201 D3PID=$! 1930 D3PID=$DPID
2202 wait_sock "$SOCK3" "$OUT.d3b.d" "daemon did not rebind the stale socket"
2203 1931
2204 await_repaint "$OUT.m7b" "$PAINTS_BEFORE" "m7b client never resumed into the restarted daemon" 1932 await_repaint "$OUT.m7b" "$PAINTS_BEFORE" "m7b client never resumed into the restarted daemon"
2205 pipe_send 'printf "m7b-%%s\\n" post-restart\n' 1933 pipe_send 'printf "m7b-%%s\\n" post-restart\n'
@@ -2285,6 +2013,7 @@ chmod 644 "$QKEY.bad"
2285 # The no-key refusal must not find the suite's own keygen'd key, so it runs 2013 # The no-key refusal must not find the suite's own keygen'd key, so it runs
2286 # against a config home that has never had one written to it. 2014 # against a config home that has never had one written to it.
2287 NOKEY_CFG="${TMPDIR:-/tmp}/mux-e2e-nokey-$$" 2015 NOKEY_CFG="${TMPDIR:-/tmp}/mux-e2e-nokey-$$"
2016 defer_rm "$NOKEY_CFG"
2288 refuse 2 "$NOKEY_CFG" --quic "127.0.0.1:$QPORT" 2017 refuse 2 "$NOKEY_CFG" --quic "127.0.0.1:$QPORT"
2289 refuse 2 "$XDG_CONFIG_HOME" --key "$QKEY" 2018 refuse 2 "$XDG_CONFIG_HOME" --key "$QKEY"
2290 refuse 2 "$XDG_CONFIG_HOME" --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 0 2019 refuse 2 "$XDG_CONFIG_HOME" --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 0
@@ -2304,10 +2033,9 @@ rm -rf "$NOKEY_CFG"
2304 ok "no key anywhere is refused, and says how to make one" 2033 ok "no key anywhere is refused, and says how to make one"
2305 2034
2306 # The accepted case. 2035 # The accepted case.
2307 "$MUXD" run --sock "$SOCK4" --shell /bin/sh \ 2036 start_daemon "$SOCK4" "$OUT.d4.d" "--quic daemon never bound its session socket" --shell /bin/sh \
2308 --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 3000 > "$OUT.d4.d" 2>&1 & 2037 --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 3000
2309 D4PID=$! 2038 D4PID=$DPID
2310 wait_sock "$SOCK4" "$OUT.d4.d" "--quic daemon never bound its session socket"
2311 2039
2312 # The UDP port is actually held. /proc/net/udp rather than ss or lsof: it is 2040 # The UDP port is actually held. /proc/net/udp rather than ss or lsof: it is
2313 # always there on the platform this daemon runs on, and needs no privileges. 2041 # always there on the platform this daemon runs on, and needs no privileges.
@@ -2448,9 +2176,11 @@ grep -q "did not answer" "$OUT.qc" || {
2448 # A fifo rather than a pipeline, so what is timed is the CLIENT's exit and 2176 # A fifo rather than a pipeline, so what is timed is the CLIENT's exit and
2449 # not how long the writer happened to hang around afterwards. 2177 # not how long the writer happened to hang around afterwards.
2450 QFIFO="${TMPDIR:-/tmp}/mux-e2e-abort-fifo-$$" 2178 QFIFO="${TMPDIR:-/tmp}/mux-e2e-abort-fifo-$$"
2179 defer_rm "$QFIFO"
2451 mkfifo "$QFIFO" 2180 mkfifo "$QFIFO"
2452 ( sleep 0.3; printf '\034'; sleep 20 ) > "$QFIFO" & 2181 ( sleep 0.3; printf '\034'; sleep 20 ) > "$QFIFO" &
2453 QWPID=$! 2182 QWPID=$!
2183 defer_kill "$QWPID"
2454 QT0=$(date +%s%N) 2184 QT0=$(date +%s%N)
2455 set +e 2185 set +e
2456 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY.wrong" \ 2186 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY.wrong" \
@@ -2534,16 +2264,15 @@ PAINTS_BEFORE=$(repaints "$OUT.qk")
2534 hardkill "$D4PID" 2264 hardkill "$D4PID"
2535 D4PID="" 2265 D4PID=""
2536 sleep 0.5 2266 sleep 0.5
2537 "$MUXD" run --sock "$SOCK4" --shell /bin/sh \ 2267 start_daemon "$SOCK4" "$OUT.q" "restarted --quic daemon never rebound its session socket" --shell /bin/sh \
2538 --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 15000 > "$OUT.q" 2>&1 & 2268 --quic "127.0.0.1:$QPORT" --key "$QKEY" --quic-idle-ms 15000
2539 D4PID=$! 2269 D4PID=$DPID
2540 # Nearly vacuous, and kept only because a MISSING file would still be worth 2270 # Nearly vacuous, and kept only because a MISSING file would still be worth
2541 # saying out loud: kill -9 leaves the old socket file behind, so this 2271 # saying out loud: kill -9 leaves the old socket file behind, so this
2542 # passes on the dead daemon's leavings. What proves the unix socket serves 2272 # passes on the dead daemon's leavings. What proves the unix socket serves
2543 # again is further down, where `muxd dump` and `muxd stats` answer on it; 2273 # again is further down, where `muxd dump` and `muxd stats` answer on it;
2544 # what proves the daemon serves SESSIONS again is the post-restart marker 2274 # what proves the daemon serves SESSIONS again is the post-restart marker
2545 # the resumed client gets back. 2275 # the resumed client gets back.
2546 wait_sock "$SOCK4" "$OUT.q" "restarted --quic daemon never rebound its session socket"
2547 # The UDP port really came back, and to THIS daemon. Without SO_REUSEADDR a 2276 # The UDP port really came back, and to THIS daemon. Without SO_REUSEADDR a
2548 # bind that collided would have failed loudly instead. 2277 # bind that collided would have failed loudly instead.
2549 # 2278 #
@@ -2609,6 +2338,7 @@ D4PID=""
2609 env MUX_KEY_FILE="$QKEY" "$MUXD" run --sock "$SOCK9" --shell /bin/sh \ 2338 env MUX_KEY_FILE="$QKEY" "$MUXD" run --sock "$SOCK9" --shell /bin/sh \
2610 --quic "127.0.0.1:$QPORT2" --quic-idle-ms 3000 > "$OUT.d9.d" 2>&1 & 2339 --quic "127.0.0.1:$QPORT2" --quic-idle-ms 3000 > "$OUT.d9.d" 2>&1 &
2611 D9PID=$! 2340 D9PID=$!
2341 defer_kill "$D9PID"
2612 wait_sock "$SOCK9" "$OUT.d9.d" "MUX_KEY_FILE daemon never bound its session socket" 2342 wait_sock "$SOCK9" "$OUT.d9.d" "MUX_KEY_FILE daemon never bound its session socket"
2613 2343
2614 pipe_mux "$OUT.env1" "$OUT.env1.err" timeout 30 "$MUX" "quic://127.0.0.1:$QPORT2" --key "$QKEY" --quic-idle-ms 15000 2344 pipe_mux "$OUT.env1" "$OUT.env1.err" timeout 30 "$MUX" "quic://127.0.0.1:$QPORT2" --key "$QKEY" --quic-idle-ms 15000
@@ -2630,6 +2360,7 @@ D9PID=""
2630 env MUX_KEY_FILE="$QKEY.wrong" "$MUXD" run --sock "$SOCK10" --shell /bin/sh \ 2360 env MUX_KEY_FILE="$QKEY.wrong" "$MUXD" run --sock "$SOCK10" --shell /bin/sh \
2631 --quic "127.0.0.1:$QPORT3" --key "$QKEY" --quic-idle-ms 3000 > "$OUT.d10.d" 2>&1 & 2361 --quic "127.0.0.1:$QPORT3" --key "$QKEY" --quic-idle-ms 3000 > "$OUT.d10.d" 2>&1 &
2632 D10PID=$! 2362 D10PID=$!
2363 defer_kill "$D10PID"
2633 wait_sock "$SOCK10" "$OUT.d10.d" "--key-beats-env daemon never bound its session socket" 2364 wait_sock "$SOCK10" "$OUT.d10.d" "--key-beats-env daemon never bound its session socket"
2634 2365
2635 pipe_mux "$OUT.env2" "$OUT.env2.err" timeout 30 "$MUX" "quic://127.0.0.1:$QPORT3" --key "$QKEY" --quic-idle-ms 15000 2366 pipe_mux "$OUT.env2" "$OUT.env2.err" timeout 30 "$MUX" "quic://127.0.0.1:$QPORT3" --key "$QKEY" --quic-idle-ms 15000
@@ -2664,6 +2395,7 @@ grep -q '^up (' "$OUT.start" || {
2664 # run, and closing it would mean parsing the pid before asserting the lines 2395 # run, and closing it would mean parsing the pid before asserting the lines
2665 # that prove the pid is there. 2396 # that prove the pid is there.
2666 SPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start") 2397 SPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start")
2398 defer_kill "$SPID"
2667 [ -n "$SPID" ] || { echo "e2e FAIL: up line carries no pid"; exit 1; } 2399 [ -n "$SPID" ] || { echo "e2e FAIL: up line carries no pid"; exit 1; }
2668 kill -0 "$SPID" || { echo "e2e FAIL: started daemon not alive"; exit 1; } 2400 kill -0 "$SPID" || { echo "e2e FAIL: started daemon not alive"; exit 1; }
2669 # Non-tty stderr: exactly two lines, no dots. 2401 # Non-tty stderr: exactly two lines, no dots.
@@ -2701,6 +2433,7 @@ dd if=/dev/zero bs=1024 count=8 2>/dev/null >> "$MUXLOG"
2701 PADDED=$(stat -c %s "$MUXLOG") 2433 PADDED=$(stat -c %s "$MUXLOG")
2702 "$MUXD" start --sock "$SOCK8T" 2> "$OUT.start" 2434 "$MUXD" start --sock "$SOCK8T" 2> "$OUT.start"
2703 TPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start") 2435 TPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.start")
2436 defer_kill "$TPID"
2704 [ -n "$TPID" ] || { echo "e2e FAIL: truncation spawn reported no pid"; exit 1; } 2437 [ -n "$TPID" ] || { echo "e2e FAIL: truncation spawn reported no pid"; exit 1; }
2705 SHRUNK=$(stat -c %s "$MUXLOG") 2438 SHRUNK=$(stat -c %s "$MUXLOG")
2706 [ "$SHRUNK" -lt "$PADDED" ] || { 2439 [ "$SHRUNK" -lt "$PADDED" ] || {
@@ -2712,8 +2445,8 @@ TPID=""
2712 # survives — a second daemon on the path would have started a fresh shell). 2445 # survives — a second daemon on the path would have started a fresh shell).
2713 softkill "$SPID" && wait_gone "$SOCK8" 2446 softkill "$SPID" && wait_gone "$SOCK8"
2714 SPID="" 2447 SPID=""
2715 "$MUXD" start --sock "$SOCK8" 2> "$OUT.ra" & RA=$! 2448 "$MUXD" start --sock "$SOCK8" 2> "$OUT.ra" & RA=$!; defer_kill "$RA"
2716 "$MUXD" start --sock "$SOCK8" 2> "$OUT.rb" & RB=$! 2449 "$MUXD" start --sock "$SOCK8" 2> "$OUT.rb" & RB=$!; defer_kill "$RB"
2717 set +e 2450 set +e
2718 wait "$RA"; RCA=$? 2451 wait "$RA"; RCA=$?
2719 wait "$RB"; RCB=$? 2452 wait "$RB"; RCB=$?
@@ -2728,6 +2461,7 @@ pipe_detach
2728 echo "e2e FAIL: race: session unusable"; exit 1; } 2461 echo "e2e FAIL: race: session unusable"; exit 1; }
2729 SPID=$(cat "$OUT.ra" "$OUT.rb" | sed -n 's/.* pid=\([0-9]*\).*/\1/p' | while read -r p; do 2462 SPID=$(cat "$OUT.ra" "$OUT.rb" | sed -n 's/.* pid=\([0-9]*\).*/\1/p' | while read -r p; do
2730 kill -0 "$p" 2>/dev/null && echo "$p"; done | head -1) 2463 kill -0 "$p" 2>/dev/null && echo "$p"; done | head -1)
2464 defer_kill "$SPID"
2731 2465
2732 # What actually makes two concurrent starts safe is that the LOSER exits 2466 # What actually makes two concurrent starts safe is that the LOSER exits
2733 # instead of unlinking the winner's socket and binding over it. The race 2467 # instead of unlinking the winner's socket and binding over it. The race
@@ -2762,6 +2496,7 @@ ok "muxd start — spawn, no-op rerun, log truncation, race"
2762 # stdlib answers with `unreachable`, i.e. exit 134 and a stack trace. 2496 # stdlib answers with `unreachable`, i.e. exit 134 and a stack trace.
2763 # No convergence: the daemon under test is the one that died, so nothing to dump. 2497 # No convergence: the daemon under test is the one that died, so nothing to dump.
2764 DEADCFG="${TMPDIR:-/tmp}/mux-e2e-deadchild-$$" 2498 DEADCFG="${TMPDIR:-/tmp}/mux-e2e-deadchild-$$"
2499 defer_rm "$DEADCFG"
2765 set +e 2500 set +e
2766 env XDG_CONFIG_HOME="$DEADCFG" timeout 30 "$MUXD" start --sock "$SOCK8T" \ 2501 env XDG_CONFIG_HOME="$DEADCFG" timeout 30 "$MUXD" start --sock "$SOCK8T" \
2767 --quic "127.0.0.1:1" --key /nonexistent > "$OUT.dead" 2>&1 2502 --quic "127.0.0.1:1" --key /nonexistent > "$OUT.dead" 2>&1
@@ -2793,6 +2528,7 @@ ok "a start whose daemon dies young says so, with the log path"
2793 # hermetic default path near the top of this suite. 2528 # hermetic default path near the top of this suite.
2794 "$MUXD" start --sock "$SOCK11" --quic "127.0.0.1:$QPORT4" 2> "$OUT.goal" 2529 "$MUXD" start --sock "$SOCK11" --quic "127.0.0.1:$QPORT4" 2> "$OUT.goal"
2795 GPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.goal") 2530 GPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.goal")
2531 defer_kill "$GPID"
2796 [ -n "$GPID" ] || { echo "e2e FAIL: goal start reported no pid"; cat "$OUT.goal"; exit 1; } 2532 [ -n "$GPID" ] || { echo "e2e FAIL: goal start reported no pid"; cat "$OUT.goal"; exit 1; }
2797 pipe_mux "$OUT.g9" "$OUT.g9.err" timeout 30 "$MUX" "quic://127.0.0.1:$QPORT4" 2533 pipe_mux "$OUT.g9" "$OUT.g9.err" timeout 30 "$MUX" "quic://127.0.0.1:$QPORT4"
2798 pipe_send 'printf "goal-%%s\\n" quic\n' 2534 pipe_send 'printf "goal-%%s\\n" quic\n'
@@ -2841,9 +2577,8 @@ ok "keygen + start --quic + mux quic:// with no --key anywhere"
2841 PDELAY=300 2577 PDELAY=300
2842 2578
2843 # Own file, not the client capture: the daemon's fd survives the client's truncation and would write into the replayed stream. 2579 # Own file, not the client capture: the daemon's fd survives the client's truncation and would write into the replayed stream.
2844 "$MUXD" run --sock "$SOCK5" --shell /bin/cat > "$OUT.p1.d" 2>&1 & 2580 start_daemon "$SOCK5" "$OUT.p1.d" "prediction daemon never bound" --shell /bin/cat
2845 D5PID=$! 2581 D5PID=$DPID
2846 wait_sock "$SOCK5" "$OUT.p1.d" "prediction daemon never bound"
2847 2582
2848 # 1. Line mode: the glyph is on screen before the round trip could have 2583 # 1. Line mode: the glyph is on screen before the round trip could have
2849 # delivered it. 2584 # delivered it.
@@ -2853,6 +2588,7 @@ set +e
2853 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \ 2588 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \
2854 > "$OUT.p1" 2> "$OUT.p1.err" & 2589 > "$OUT.p1" 2> "$OUT.p1.err" &
2855 P1PID=$! 2590 P1PID=$!
2591 defer_kill "$P1PID"
2856 set -e 2592 set -e
2857 # Snapshot the client output 450ms after the keystroke: well 2593 # Snapshot the client output 450ms after the keystroke: well
2858 # before this 600ms round trip could bring the pty own echo back, and well 2594 # before this 600ms round trip could bring the pty own echo back, and well
@@ -2970,9 +2706,8 @@ read _hold
2970 PWEOF 2706 PWEOF
2971 chmod +x "$PWSH" 2707 chmod +x "$PWSH"
2972 2708
2973 "$MUXD" run --sock "$SOCK6" --shell "$PWSH" > "$OUT.pw.d" 2>&1 & 2709 start_daemon "$SOCK6" "$OUT.pw.d" "password daemon never bound" --shell "$PWSH"
2974 D6PID=$! 2710 D6PID=$DPID
2975 wait_sock "$SOCK6" "$OUT.pw.d" "password daemon never bound"
2976 2711
2977 # One character per write, and NOT as a single `printf hunter2`. A whole 2712 # One character per write, and NOT as a single `printf hunter2`. A whole
2978 # word in one write reaches the client as a multi-byte chunk, which is 2713 # word in one write reaches the client as a multi-byte chunk, which is
@@ -3024,9 +2759,8 @@ D6PID=""
3024 # earn/lose policy, which only a remote path lets through to the screen. 2759 # earn/lose policy, which only a remote path lets through to the screen.
3025 # Keys are 1s apart so each confirm (600ms) lands before the next key — 2760 # Keys are 1s apart so each confirm (600ms) lands before the next key —
3026 # at 0.5s `c` would find only `a` confirmed and stay hidden. 2761 # at 0.5s `c` would find only `a` confirmed and stay hidden.
3027 "$MUXD" run --sock "$SOCK7" --shell "$RAWMODE" > "$OUT.rw.d" 2>&1 & 2762 start_daemon "$SOCK7" "$OUT.rw.d" "rawmode daemon never bound" --shell "$RAWMODE"
3028 D7PID=$! 2763 D7PID=$DPID
3029 wait_sock "$SOCK7" "$OUT.rw.d" "rawmode daemon never bound"
3030 2764
3031 set +e 2765 set +e
3032 { sleep 2; for c in a b c; do printf '%s' "$c"; sleep 1; done; \ 2766 { sleep 2; for c in a b c; do printf '%s' "$c"; sleep 1; done; \
@@ -3083,9 +2817,8 @@ assert_converged "$OUT.rw" "$SOCK7" "raw mode"
3083 softkill "$D7PID" || true 2817 softkill "$D7PID" || true
3084 wait "$D7PID" 2>/dev/null || true 2818 wait "$D7PID" 2>/dev/null || true
3085 rm -f "$SOCK7" 2819 rm -f "$SOCK7"
3086 "$MUXD" run --sock "$SOCK7" --shell "$RAWMODE" > "$OUT.rwl.d" 2>&1 & 2820 start_daemon "$SOCK7" "$OUT.rwl.d" "rawmode local daemon never bound" --shell "$RAWMODE"
3087 D7PID=$! 2821 D7PID=$DPID
3088 wait_sock "$SOCK7" "$OUT.rwl.d" "rawmode local daemon never bound"
3089 2822
3090 set +e 2823 set +e
3091 { sleep 2; for c in a b c; do printf '%s' "$c"; sleep 0.5; done; \ 2824 { sleep 2; for c in a b c; do printf '%s' "$c"; sleep 0.5; done; \
@@ -3113,9 +2846,8 @@ D7PID=""
3113 # 5. A transport torn down with predictions outstanding. The overlay must 2846 # 5. A transport torn down with predictions outstanding. The overlay must
3114 # come back empty: what was queued was predicted against a connection 2847 # come back empty: what was queued was predicted against a connection
3115 # that no longer exists. 2848 # that no longer exists.
3116 "$MUXD" run --sock "$SOCK5" --shell /bin/cat > "$OUT.pr.d" 2>&1 & 2849 start_daemon "$SOCK5" "$OUT.pr.d" "reconnect daemon never bound" --shell /bin/cat
3117 D5PID=$! 2850 D5PID=$DPID
3118 wait_sock "$SOCK5" "$OUT.pr.d" "reconnect daemon never bound"
3119 2851
3120 set +e 2852 set +e
3121 { sleep 2; printf 'p'; sleep 5; printf 'q'; sleep 4; printf '\034\034'; } | \ 2853 { sleep 2; printf 'p'; sleep 5; printf 'q'; sleep 4; printf '\034\034'; } | \
@@ -3123,6 +2855,7 @@ set +e
3123 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \ 2855 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \
3124 > "$OUT.pr" 2> "$OUT.pr.err" & 2856 > "$OUT.pr" 2> "$OUT.pr.err" &
3125 PRPID=$! 2857 PRPID=$!
2858 defer_kill "$PRPID"
3126 set -e 2859 set -e
3127 # Kill the transport a tenth of a second after the keystroke, while its 2860 # Kill the transport a tenth of a second after the keystroke, while its
3128 # prediction is still outstanding — the round trip is 600ms, so it cannot 2861 # prediction is still outstanding — the round trip is 600ms, so it cannot
@@ -3262,9 +2995,8 @@ ok "ptyclient controls: pty echo roundtrips, impossible expect fails loudly, std
3262 # Hence: fill the screen, put a 95-wide row on it, then winch UP to 2995 # Hence: fill the screen, put a 95-wide row on it, then winch UP to
3263 # 100x30. With the prefix ignored the replica stays 90x28 and the repaint 2996 # 100x30. With the prefix ignored the replica stays 90x28 and the repaint
3264 # answering the resize reproduces the OLD geometry. 2997 # answering the resize reproduces the OLD geometry.
3265 "$MUXD" run --sock "$SOCK12" --shell /bin/sh > "$OUT.tp2.d" 2>&1 & 2998 start_daemon "$SOCK12" "$OUT.tp2.d" "tp2 daemon never bound" --shell /bin/sh
3266 D12PID=$! 2999 D12PID=$DPID
3267 wait_sock "$SOCK12" "$OUT.tp2.d" "tp2 daemon never bound"
3268 3000
3269 # Two rules govern every script below, and breaking either one produces a 3001 # Two rules govern every script below, and breaking either one produces a
3270 # capture that is honestly SHORT rather than wrong — a divergence whose 3002 # capture that is honestly SHORT rather than wrong — a divergence whose
@@ -3424,9 +3156,8 @@ seq 1 100
3424 exec /bin/cat 3156 exec /bin/cat
3425 EOF 3157 EOF
3426 chmod +x "$TP1SH" 3158 chmod +x "$TP1SH"
3427 "$MUXD" run --sock "$SOCK13" --shell "$TP1SH" > "$OUT.tp1.d" 2>&1 & 3159 start_daemon "$SOCK13" "$OUT.tp1.d" "tp1 daemon never bound" --shell "$TP1SH"
3428 D13PID=$! 3160 D13PID=$DPID
3429 wait_sock "$SOCK13" "$OUT.tp1.d" "tp1 daemon never bound"
3430 # Attach only after seq finished: the client must be served a snapshot of 3161 # Attach only after seq finished: the client must be served a snapshot of
3431 # the TAIL, so the scrollback page it fetches later is content it has never 3162 # the TAIL, so the scrollback page it fetches later is content it has never
3432 # been sent. Observed at 80x24: the attach snapshot carries rows 78..100 and 3163 # been sent. Observed at 80x24: the attach snapshot carries rows 78..100 and
@@ -3479,6 +3210,7 @@ send \x1c\x1c
3479 waitexit 15000 3210 waitexit 15000
3480 EOF 3211 EOF
3481 TP1PID=$! 3212 TP1PID=$!
3213 defer_kill "$TP1PID"
3482 3214
3483 # The tear goes between verb 3 (the scroll view is on screen) and verb 4; 3215 # The tear goes between verb 3 (the scroll view is on screen) and verb 4;
3484 # the fixture's "done 3" line is the barrier. Every needle above is unique 3216 # the fixture's "done 3" line is the barrier. Every needle above is unique
@@ -3612,6 +3344,7 @@ grep -q '^muxd proxy: starting' "$OUT.as.err" || {
3612 # it is covered anyway: the trap also stops SOCK14/SOCK15 by PATH, which 3344 # it is covered anyway: the trap also stops SOCK14/SOCK15 by PATH, which
3613 # needs no pid. 3345 # needs no pid.
3614 APID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.as.err" | head -1) 3346 APID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.as.err" | head -1)
3347 defer_kill "$APID"
3615 [ -n "$APID" ] || { echo "e2e FAIL: proxy up-line carries no pid"; cat "$OUT.as.err"; exit 1; } 3348 [ -n "$APID" ] || { echo "e2e FAIL: proxy up-line carries no pid"; cat "$OUT.as.err"; exit 1; }
3616 kill -0 "$APID" || { echo "e2e FAIL: auto-started daemon not alive"; exit 1; } 3349 kill -0 "$APID" || { echo "e2e FAIL: auto-started daemon not alive"; exit 1; }
3617 assert_converged "$OUT.as" "$SOCK14" "auto-start via proxy" 3350 assert_converged "$OUT.as" "$SOCK14" "auto-start via proxy"
@@ -3710,6 +3443,7 @@ grep -q '^mux: starting' "$OUT.pa.err" || {
3710 echo "e2e FAIL: local auto-start printed no mux-prefixed starting line" 3443 echo "e2e FAIL: local auto-start printed no mux-prefixed starting line"
3711 cat "$OUT.pa.err"; exit 1; } 3444 cat "$OUT.pa.err"; exit 1; }
3712 PAPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.pa.err" | head -1) 3445 PAPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.pa.err" | head -1)
3446 defer_kill "$PAPID"
3713 [ -n "$PAPID" ] || { echo "e2e FAIL: mux up-line carries no pid"; cat "$OUT.pa.err"; exit 1; } 3447 [ -n "$PAPID" ] || { echo "e2e FAIL: mux up-line carries no pid"; cat "$OUT.pa.err"; exit 1; }
3714 assert_converged "$OUT.pa" "$SOCK15" "local mux auto-start" 3448 assert_converged "$OUT.pa" "$SOCK15" "local mux auto-start"
3715 # Same teardown, same reasons — and the stderr is captured rather than 3449 # Same teardown, same reasons — and the stderr is captured rather than
@@ -4059,9 +3793,8 @@ ok "a paste into nvim keeps its indentation"
4059 # than storing it, and this session paints a prompt, an echo and BELLDONE — 3793 # than storing it, and this session paints a prompt, an echo and BELLDONE —
4060 # but the two checks above are what make the count unambiguous, not a proof 3794 # but the two checks above are what make the count unambiguous, not a proof
4061 # that no other path to a BEL exists. 3795 # that no other path to a BEL exists.
4062 "$MUXD" run --sock "$SOCK24" --shell /bin/sh > "$OUT.d21.d" 2>&1 & 3796 start_daemon "$SOCK24" "$OUT.d21.d" "bell daemon socket never appeared" --shell /bin/sh
4063 D21PID=$! 3797 D21PID=$DPID
4064 wait_sock "$SOCK24" "$OUT.d21.d" "bell daemon socket never appeared"
4065 3798
4066 # Five rings in ONE printf, which is one write and so in practice one pty read 3799 # Five rings in ONE printf, which is one write and so in practice one pty read
4067 # and one drain — the line discipline is not going to split five bytes short of 3800 # and one drain — the line discipline is not going to split five bytes short of
@@ -4222,6 +3955,7 @@ grep -q '^muxd endpoint: starting' "$OUT.h1.err" || {
4222 echo "e2e FAIL: cold handoff printed no endpoint starting line" 3955 echo "e2e FAIL: cold handoff printed no endpoint starting line"
4223 cat "$OUT.h1.err"; exit 1; } 3956 cat "$OUT.h1.err"; exit 1; }
4224 HAPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.h1.err" | head -1) 3957 HAPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.h1.err" | head -1)
3958 defer_kill "$HAPID"
4225 [ -n "$HAPID" ] || { 3959 [ -n "$HAPID" ] || {
4226 echo "e2e FAIL: endpoint up-line carries no pid"; cat "$OUT.h1.err"; exit 1; } 3960 echo "e2e FAIL: endpoint up-line carries no pid"; cat "$OUT.h1.err"; exit 1; }
4227 kill -0 "$HAPID" 2>/dev/null || { 3961 kill -0 "$HAPID" 2>/dev/null || {
@@ -4383,10 +4117,9 @@ chmod 600 "$HKEY"
4383 # it here would suggest the ceiling's reasoning below depends on this 4117 # it here would suggest the ceiling's reasoning below depends on this
4384 # flag. It does not — the 15000 that bounds an overrunning dial is the 4118 # flag. It does not — the 15000 that bounds an overrunning dial is the
4385 # CLIENT's `quic_idle_ms_default`, which this daemon cannot influence. 4119 # CLIENT's `quic_idle_ms_default`, which this daemon cannot influence.
4386 "$MUXD" run --sock "$SOCK17" --shell /bin/sh \ 4120 start_daemon "$SOCK17" "$OUT.d17.d" "key-mismatch daemon never bound" --shell /bin/sh \
4387 --quic "127.0.0.1:$HQPORT" --key "$HKEY" > "$OUT.d17.d" 2>&1 & 4121 --quic "127.0.0.1:$HQPORT" --key "$HKEY"
4388 HDPID=$! 4122 HDPID=$DPID
4389 wait_sock "$SOCK17" "$OUT.d17.d" "key-mismatch daemon never bound"
4390 HSHIMS_D=$(wc -l < "$SSHIM_PIDLOG") 4123 HSHIMS_D=$(wc -l < "$SSHIM_PIDLOG")
4391 HT0=$(date +%s%N) 4124 HT0=$(date +%s%N)
4392 pipe_mux "$OUT.h4" "$OUT.h4.err" env SHELL=/bin/sh XDG_RUNTIME_DIR="$HRUN2" PATH="$HPATH" timeout 40 \ 4125 pipe_mux "$OUT.h4" "$OUT.h4.err" env SHELL=/bin/sh XDG_RUNTIME_DIR="$HRUN2" PATH="$HPATH" timeout 40 \
@@ -4540,9 +4273,8 @@ rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2"
4540 # would hang one of them with nothing printed — fifteen seconds into a 4273 # would hang one of them with nothing printed — fifteen seconds into a
4541 # scenario whose label says "WebSocket". This block reaches the same 4274 # scenario whose label says "WebSocket". This block reaches the same
4542 # resolver with a CLI in front of it, and fails there first. 4275 # resolver with a CLI in front of it, and fails there first.
4543 "$MUXD" run --sock "$SOCK21" --shell /bin/sh > "$OUT.m18.d" 2>&1 & 4276 start_daemon "$SOCK21" "$OUT.m18.d" "M18 multi-session daemon never bound" --shell /bin/sh
4544 D18PID=$! 4277 D18PID=$DPID
4545 wait_sock "$SOCK21" "$OUT.m18.d" "M18 multi-session daemon never bound"
4546 4278
4547 # Two clients, two names, one socket. Each plants a marker its own shell 4279 # Two clients, two names, one socket. Each plants a marker its own shell
4548 # has to EXPAND — the typed line reads `printf "m18a-%s\n" pin` and only 4280 # has to EXPAND — the typed line reads `printf "m18a-%s\n" pin` and only
@@ -4670,11 +4402,11 @@ ok "two sessions on one daemon are two shells; one dies without the other"
4670 # `#NAME` suffix, so the hub dials it twice and the two connections are 4402 # `#NAME` suffix, so the hub dials it twice and the two connections are
4671 # two sessions — the browser's spelling of everything the block above 4403 # two sessions — the browser's spelling of everything the block above
4672 # proved with a CLI. 4404 # proved with a CLI.
4673 "$MUXD" run --sock "$SOCK22" --shell /bin/sh > "$OUT.m18w.d" 2>&1 & 4405 start_daemon "$SOCK22" "$OUT.m18w.d" "M18 wall daemon never bound" --shell /bin/sh
4674 D19PID=$! 4406 D19PID=$DPID
4675 wait_sock "$SOCK22" "$OUT.m18w.d" "M18 wall daemon never bound"
4676 "$MUXWEB" --sock "$SOCK22#a" --sock "$SOCK22#b" --port "$WPORT3" > "$OUT.m18wh" 2>&1 & 4407 "$MUXWEB" --sock "$SOCK22#a" --sock "$SOCK22#b" --port "$WPORT3" > "$OUT.m18wh" 2>&1 &
4677 W3PID=$! 4408 W3PID=$!
4409 defer_kill "$W3PID"
4678 wait_for "$OUT.m18wh" "serving" 10 || { 4410 wait_for "$OUT.m18wh" "serving" 10 || {
4679 echo "e2e FAIL: M18 wall hub never reported serving"; cat "$OUT.m18wh"; exit 1; } 4411 echo "e2e FAIL: M18 wall hub never reported serving"; cat "$OUT.m18wh"; exit 1; }
4680 4412
@@ -4766,6 +4498,7 @@ while : ; do
4766 "$MUXD" run --sock "$SOCK23" --quic "127.0.0.1:$QPORT5" --key "$M18KEY" \ 4498 "$MUXD" run --sock "$SOCK23" --quic "127.0.0.1:$QPORT5" --key "$M18KEY" \
4767 --quic-idle-ms 15000 --shell /bin/sh > "$OUT.m18q.d" 2>&1 & 4499 --quic-idle-ms 15000 --shell /bin/sh > "$OUT.m18q.d" 2>&1 &
4768 D20PID=$! 4500 D20PID=$!
4501 defer_kill "$D20PID"
4769 _i=0 4502 _i=0
4770 while [ ! -S "$SOCK23" ] && kill -0 "$D20PID" 2>/dev/null && [ "$_i" -lt 60 ]; do 4503 while [ ! -S "$SOCK23" ] && kill -0 "$D20PID" 2>/dev/null && [ "$_i" -lt 60 ]; do
4771 sleep 0.1; _i=$((_i + 1)) 4504 sleep 0.1; _i=$((_i + 1))
@@ -4847,9 +4580,8 @@ ok "quic: two dials are two sessions, and one name is one session on either tran
4847 # string typed at 80 wide can only appear CONTIGUOUS in `muxd dump` if 4580 # string typed at 80 wide can only appear CONTIGUOUS in `muxd dump` if
4848 # the grid is still 80 wide — a 1-column grid puts every glyph on its 4581 # the grid is still 80 wide — a 1-column grid puts every glyph on its
4849 # own row, so grep itself is the geometry assertion. 4582 # own row, so grep itself is the geometry assertion.
4850 "$MUXD" run --sock "$SOCK18" --shell /bin/sh > "$OUT.weba.d" 2>&1 & 4583 start_daemon "$SOCK18" "$OUT.weba.d" "tiny attach daemon never bound" --shell /bin/sh
4851 D14PID=$! 4584 D14PID=$DPID
4852 wait_sock "$SOCK18" "$OUT.weba.d" "tiny attach daemon never bound"
4853 4585
4854 # The 80x24 client: types the first marker, holds the session open while 4586 # The 80x24 client: types the first marker, holds the session open while
4855 # the 1x1 attacher comes and goes, types the second marker (the geometry 4587 # the 1x1 attacher comes and goes, types the second marker (the geometry
@@ -4875,6 +4607,7 @@ send \x1c\x1c
4875 waitexit 10000 4607 waitexit 10000
4876 EOF 4608 EOF
4877 W1X1PID=$! 4609 W1X1PID=$!
4610 defer_kill "$W1X1PID"
4878 wait_for "$OUT.web1x1.log" "ptyclient: done 2" 20 || { 4611 wait_for "$OUT.web1x1.log" "ptyclient: done 2" 20 || {
4879 echo "e2e FAIL: tiny attach: the 1x1 client never got as far as its claim" 4612 echo "e2e FAIL: tiny attach: the 1x1 client never got as far as its claim"
4880 cat "$OUT.web1x1.log"; cat -v "$OUT.web1x1.err" 2>/dev/null; exit 1; } 4613 cat "$OUT.web1x1.log"; cat -v "$OUT.web1x1.err" 2>/dev/null; exit 1; }
@@ -4905,11 +4638,11 @@ D14PID=""
4905 ok "a 1x1 attach is refused the grid and can never claim it" 4638 ok "a 1x1 attach is refused the grid and can never claim it"
4906 4639
4907 # --- M-web (b): the hub pumps a real session; wrong Origin refused. 4640 # --- M-web (b): the hub pumps a real session; wrong Origin refused.
4908 "$MUXD" run --sock "$SOCK19" --shell /bin/sh > "$OUT.webb.d" 2>&1 & 4641 start_daemon "$SOCK19" "$OUT.webb.d" "web hub daemon never bound" --shell /bin/sh
4909 D15PID=$! 4642 D15PID=$DPID
4910 wait_sock "$SOCK19" "$OUT.webb.d" "web hub daemon never bound"
4911 "$MUXWEB" --sock "$SOCK19" --port "$WPORT" > "$OUT.webh" 2>&1 & 4643 "$MUXWEB" --sock "$SOCK19" --port "$WPORT" > "$OUT.webh" 2>&1 &
4912 W1PID=$! 4644 W1PID=$!
4645 defer_kill "$W1PID"
4913 wait_for "$OUT.webh" "serving" 10 || { 4646 wait_for "$OUT.webh" "serving" 10 || {
4914 echo "e2e FAIL: hub never reported serving"; cat "$OUT.webh"; exit 1; } 4647 echo "e2e FAIL: hub never reported serving"; cat "$OUT.webh"; exit 1; }
4915 4648
@@ -4972,11 +4705,11 @@ ok "hub pumps a real session; wrong origin refused"
4972 # epoch), say `up`, and the stand-in's re-attach — quoting coordinates 4705 # epoch), say `up`, and the stand-in's re-attach — quoting coordinates
4973 # the NEW daemon has never issued — must be answered with a snapshot 4706 # the NEW daemon has never issued — must be answered with a snapshot
4974 # that converges on the new session's content. 4707 # that converges on the new session's content.
4975 "$MUXD" run --sock "$SOCK20" --shell /bin/sh > "$OUT.webc.d" 2>&1 & 4708 start_daemon "$SOCK20" "$OUT.webc.d" "web tear daemon never bound" --shell /bin/sh
4976 D16PID=$! 4709 D16PID=$DPID
4977 wait_sock "$SOCK20" "$OUT.webc.d" "web tear daemon never bound"
4978 "$MUXWEB" --sock "$SOCK20" --port "$WPORT2" > "$OUT.webh2" 2>&1 & 4710 "$MUXWEB" --sock "$SOCK20" --port "$WPORT2" > "$OUT.webh2" 2>&1 &
4979 W2PID=$! 4711 W2PID=$!
4712 defer_kill "$W2PID"
4980 wait_for "$OUT.webh2" "serving" 10 || { 4713 wait_for "$OUT.webh2" "serving" 10 || {
4981 echo "e2e FAIL: tear hub never reported serving"; cat "$OUT.webh2"; exit 1; } 4714 echo "e2e FAIL: tear hub never reported serving"; cat "$OUT.webh2"; exit 1; }
4982 4715
@@ -4998,6 +4731,7 @@ settle 500 10000
4998 dumpexit 4731 dumpexit
4999 EOF 4732 EOF
5000 WCLIPID=$! 4733 WCLIPID=$!
4734 defer_kill "$WCLIPID"
5001 # Let the stand-in reach its first expectgrid before the tear; the marker 4735 # Let the stand-in reach its first expectgrid before the tear; the marker
5002 # it waits for is already on the grid, so one settle-length is plenty. 4736 # it waits for is already on the grid, so one settle-length is plenty.
5003 sleep 1 4737 sleep 1
@@ -5005,9 +4739,8 @@ sleep 1
5005 assert_stopped "$SOCK20" "$D16PID" "web tear: the first daemon, under a live tile" "$OUT.webstop3" 4739 assert_stopped "$SOCK20" "$D16PID" "web tear: the first daemon, under a live tile" "$OUT.webstop3"
5006 D16PID="" 4740 D16PID=""
5007 4741
5008 "$MUXD" run --sock "$SOCK20" --shell /bin/sh > "$OUT.webc2.d" 2>&1 & 4742 start_daemon "$SOCK20" "$OUT.webc2.d" "web tear restart never bound" --shell /bin/sh
5009 D17PID=$! 4743 D17PID=$DPID
5010 wait_sock "$SOCK20" "$OUT.webc2.d" "web tear restart never bound"
5011 4744
5012 pipe_mux "$OUT.webc2" "$OUT.webc2.err" timeout 30 "$MUX" --sock "$SOCK20" 4745 pipe_mux "$OUT.webc2" "$OUT.webc2.err" timeout 30 "$MUX" --sock "$SOCK20"
5013 pipe_send 'printf "web-%%s\\n" c2\n' 4746 pipe_send 'printf "web-%%s\\n" c2\n'
@@ -5040,12 +4773,12 @@ ok "hub narrates the tear; the replica re-attaches across an epoch"
5040 # restart. Every muxweb here runs with a state home of its own (see 4773 # restart. Every muxweb here runs with a state home of its own (see
5041 # $DWSTATE), so what this leg reads back is the wall this leg wrote — and 4774 # $DWSTATE), so what this leg reads back is the wall this leg wrote — and
5042 # never the developer's real one. 4775 # never the developer's real one.
5043 "$MUXD" run --sock "$SOCK25" --shell /bin/sh > "$OUT.dw.d" 2>&1 & 4776 start_daemon "$SOCK25" "$OUT.dw.d" "dyn wall daemon never bound" --shell /bin/sh
5044 D22PID=$! 4777 D22PID=$DPID
5045 wait_sock "$SOCK25" "$OUT.dw.d" "dyn wall daemon never bound"
5046 4778
5047 XDG_STATE_HOME="$DWSTATE" "$MUXWEB" --port "$WPORT4" > "$OUT.dwh" 2>&1 & 4779 XDG_STATE_HOME="$DWSTATE" "$MUXWEB" --port "$WPORT4" > "$OUT.dwh" 2>&1 &
5048 W4PID=$! 4780 W4PID=$!
4781 defer_kill "$W4PID"
5049 wait_for "$OUT.dwh" "serving" 10 || { 4782 wait_for "$OUT.dwh" "serving" 10 || {
5050 echo "e2e FAIL: dyn wall: hub never reported serving"; cat "$OUT.dwh"; exit 1; } 4783 echo "e2e FAIL: dyn wall: hub never reported serving"; cat "$OUT.dwh"; exit 1; }
5051 DWORIG="http://127.0.0.1:$WPORT4" 4784 DWORIG="http://127.0.0.1:$WPORT4"
@@ -5184,6 +4917,7 @@ softkill "$W4PID" || true
5184 wait_pid_gone "$W4PID" "dyn wall: first hub killed by tracked pid" 4917 wait_pid_gone "$W4PID" "dyn wall: first hub killed by tracked pid"
5185 XDG_STATE_HOME="$DWSTATE" "$MUXWEB" --port "$WPORT4" > "$OUT.dwh2" 2>&1 & 4918 XDG_STATE_HOME="$DWSTATE" "$MUXWEB" --port "$WPORT4" > "$OUT.dwh2" 2>&1 &
5186 W4PID=$! 4919 W4PID=$!
4920 defer_kill "$W4PID"
5187 wait_for "$OUT.dwh2" "serving" 10 || { 4921 wait_for "$OUT.dwh2" "serving" 10 || {
5188 echo "e2e FAIL: dyn wall: restored hub never reported serving"; cat "$OUT.dwh2"; exit 1; } 4922 echo "e2e FAIL: dyn wall: restored hub never reported serving"; cat "$OUT.dwh2"; exit 1; }
5189 curl -s "$DWORIG/tiles" | grep -q "^\[{\"id\":0,\"label\":\"--sock $SOCK25#b\"" || { 4923 curl -s "$DWORIG/tiles" | grep -q "^\[{\"id\":0,\"label\":\"--sock $SOCK25#b\"" || {
@@ -5204,6 +4938,7 @@ softkill "$W4PID" || true
5204 wait_pid_gone "$W4PID" "dyn wall: restored hub killed by tracked pid" 4938 wait_pid_gone "$W4PID" "dyn wall: restored hub killed by tracked pid"
5205 XDG_STATE_HOME="$DWSTATE" "$MUXWEB" "--sock $SOCK25" --port "$WPORT4" > "$OUT.dwh3" 2>&1 & 4939 XDG_STATE_HOME="$DWSTATE" "$MUXWEB" "--sock $SOCK25" --port "$WPORT4" > "$OUT.dwh3" 2>&1 &
5206 W4PID=$! 4940 W4PID=$!
4941 defer_kill "$W4PID"
5207 wait_for "$OUT.dwh3" "serving" 10 || { 4942 wait_for "$OUT.dwh3" "serving" 10 || {
5208 echo "e2e FAIL: dyn wall: overriding hub never reported serving"; cat "$OUT.dwh3"; exit 1; } 4943 echo "e2e FAIL: dyn wall: overriding hub never reported serving"; cat "$OUT.dwh3"; exit 1; }
5209 grep -qxF -- "--sock $SOCK25" "$DWSTATE/mux/wall" || { 4944 grep -qxF -- "--sock $SOCK25" "$DWSTATE/mux/wall" || {
@@ -5247,9 +4982,8 @@ ok "the wall is runtime state: add, remove, reorder, restore, argv adds"
5247 # The two tiles are spelled DIFFERENTLY on purpose: `--sock PATH` as two 4982 # The two tiles are spelled DIFFERENTLY on purpose: `--sock PATH` as two
5248 # arguments (muxweb's dialect) and as one quoted spelling (the wall file's). 4983 # arguments (muxweb's dialect) and as one quoted spelling (the wall file's).
5249 # Both must reach the same tile, so both are pinned by this one leg. 4984 # Both must reach the same tile, so both are pinned by this one leg.
5250 "$MUXD" run --sock "$SOCK26" --shell /bin/sh > "$OUT.cwall.d" 2>&1 & 4985 start_daemon "$SOCK26" "$OUT.cwall.d" "CLI wall daemon never bound" --shell /bin/sh
5251 D23PID=$! 4986 D23PID=$DPID
5252 wait_sock "$SOCK26" "$OUT.cwall.d" "CLI wall daemon never bound"
5253 4987
5254 pipe_mux "$OUT.cwa" "$OUT.cwa.err" timeout 40 "$MUX" --sock "$SOCK26" --session a 4988 pipe_mux "$OUT.cwa" "$OUT.cwa.err" timeout 40 "$MUX" --sock "$SOCK26" --session a
5255 pipe_send 'printf "cwa-%%s\\n" pin\n' 4989 pipe_send 'printf "cwa-%%s\\n" pin\n'
@@ -5265,6 +4999,7 @@ wait_grid "$SOCK26" "cwb-pin" "CLI wall: session b's marker" b
5265 ( sleep 5; "$MUXA" send 'printf "cwlive-%s\n" pin\n' \ 4999 ( sleep 5; "$MUXA" send 'printf "cwlive-%s\n" pin\n' \
5266 --sock "$SOCK26" --session b > "$OUT.cwinj" 2>&1 ) & 5000 --sock "$SOCK26" --session b > "$OUT.cwinj" 2>&1 ) &
5267 CWINJPID=$! 5001 CWINJPID=$!
5002 defer_kill "$CWINJPID"
5268 set +e 5003 set +e
5269 timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cwcap" --err "$OUT.cwcap.err" -- \ 5004 timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cwcap" --err "$OUT.cwcap.err" -- \
5270 "$MUX" wall --sock "$SOCK26#a" "--sock $SOCK26#b" > "$OUT.cwpc" 2>&1 <<'EOF' 5005 "$MUX" wall --sock "$SOCK26#a" "--sock $SOCK26#b" > "$OUT.cwpc" 2>&1 <<'EOF'
@@ -5312,9 +5047,8 @@ ok "mux wall: two sessions and a live delta on one terminal"
5312 # A real pty (ptyclient) rather than a pipeline: the shell only echoes what 5047 # A real pty (ptyclient) rather than a pipeline: the shell only echoes what
5313 # it was really given when there is a terminal to echo to, and the echo is 5048 # it was really given when there is a terminal to echo to, and the echo is
5314 # what carries the stray `z` if the layer leaks one. 5049 # what carries the stray `z` if the layer leaks one.
5315 "$MUXD" run --sock "$SOCK27" --shell /bin/sh > "$OUT.pfx.d" 2>&1 & 5050 start_daemon "$SOCK27" "$OUT.pfx.d" "prefix daemon never bound" --shell /bin/sh
5316 D24PID=$! 5051 D24PID=$DPID
5317 wait_sock "$SOCK27" "$OUT.pfx.d" "prefix daemon never bound"
5318 5052
5319 set +e 5053 set +e
5320 timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pfx" --err "$OUT.pfx.err" \ 5054 timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pfx" --err "$OUT.pfx.err" \
@@ -5382,9 +5116,8 @@ ok "Ctrl-\\ prefix: an unknown chord is swallowed, Ctrl-\\ d detaches"
5382 # would go to the mailbox of the tile the user is leaving, which is the 5116 # would go to the mailbox of the tile the user is leaving, which is the
5383 # session they were typing in a moment ago and would silently swallow the 5117 # session they were typing in a moment ago and would silently swallow the
5384 # marker. 5118 # marker.
5385 "$MUXD" run --sock "$SOCK28" --shell /bin/sh > "$OUT.nsw.d" 2>&1 & 5119 start_daemon "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound" --shell /bin/sh
5386 D25PID=$! 5120 D25PID=$DPID
5387 wait_sock "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound"
5388 # The refusal this leg's fourth chord asserts needs a FULL table. The 5121 # The refusal this leg's fourth chord asserts needs a FULL table. The
5389 # chords make four (0, 1, 2, 3); the other 28 are filled here. `fillN` is 5122 # chords make four (0, 1, 2, 3); the other 28 are filled here. `fillN` is
5390 # not an integer, so nextFreeName still hands the chords 1, 2, 3 and 5123 # not an integer, so nextFreeName still hands the chords 1, 2, 3 and
@@ -5514,9 +5247,8 @@ ok "Ctrl-\\ c: a new session is created and switched to, the old one intact; a f
5514 # The captures afterwards pin which marker lives where — three sessions 5247 # The captures afterwards pin which marker lives where — three sessions
5515 # that each kept their own line, so the stepping moved the CLIENT and left 5248 # that each kept their own line, so the stepping moved the CLIENT and left
5516 # the sessions where they were. 5249 # the sessions where they were.
5517 "$MUXD" run --sock "$SOCK29" --shell /bin/sh > "$OUT.ring.d" 2>&1 & 5250 start_daemon "$SOCK29" "$OUT.ring.d" "session-ring daemon never bound" --shell /bin/sh
5518 D26PID=$! 5251 D26PID=$DPID
5519 wait_sock "$SOCK29" "$OUT.ring.d" "session-ring daemon never bound"
5520 5252
5521 set +e 5253 set +e
5522 # tall: Ctrl-\ c adds tiles at 80x24 (80 >= 48) 5254 # tall: Ctrl-\ c adds tiles at 80x24 (80 >= 48)
@@ -5606,9 +5338,8 @@ ok "Ctrl-\\ n / Ctrl-\\ p: the ring steps both ways and wraps at both ends"
5606 # mid-sentence and a correct refusal read as a missing one. Width is the fix 5338 # mid-sentence and a correct refusal read as a missing one. Width is the fix
5607 # that does not make the needle shorter than the claim. 5339 # that does not make the needle shorter than the claim.
5608 MUXABS=$(cd "$(dirname "$MUX")" && pwd)/$(basename "$MUX") 5340 MUXABS=$(cd "$(dirname "$MUX")" && pwd)/$(basename "$MUX")
5609 "$MUXD" run --sock "$SOCK31" --shell /bin/sh --cols 200 > "$OUT.sa.d" 2>&1 & 5341 start_daemon "$SOCK31" "$OUT.sa.d" "self-attach daemon never bound" --shell /bin/sh --cols 200
5610 D28PID=$! 5342 D28PID=$DPID
5611 wait_sock "$SOCK31" "$OUT.sa.d" "self-attach daemon never bound"
5612 5343
5613 timeout 20 "$MUXA" send \ 5344 timeout 20 "$MUXA" send \
5614 "[ \"\$MUX_SOCK\" = \"$SOCK31\" ] && printf 'ENV%s-%s' OK \"\$MUX_SESSION\"; echo\n" \ 5345 "[ \"\$MUX_SOCK\" = \"$SOCK31\" ] && printf 'ENV%s-%s' OK \"\$MUX_SESSION\"; echo\n" \
@@ -5682,9 +5413,8 @@ seq 1 100
5682 exec /bin/cat 5413 exec /bin/cat
5683 EOF 5414 EOF
5684 chmod +x "$WHEELSH" 5415 chmod +x "$WHEELSH"
5685 "$MUXD" run --sock "$SOCK33" --shell "$WHEELSH" > "$OUT.whl.d" 2>&1 & 5416 start_daemon "$SOCK33" "$OUT.whl.d" "wheel daemon never bound" --shell "$WHEELSH"
5686 D30PID=$! 5417 D30PID=$DPID
5687 wait_sock "$SOCK33" "$OUT.whl.d" "wheel daemon never bound"
5688 # Attach only once seq has finished, for tp1's reason: the page the wheel 5418 # Attach only once seq has finished, for tp1's reason: the page the wheel
5689 # fetches has to be content this client was never sent. 5419 # fetches has to be content this client was never sent.
5690 i=0 5420 i=0
@@ -5754,9 +5484,8 @@ printf 'app-holds-the-mouse\n'
5754 exec /bin/cat 5484 exec /bin/cat
5755 EOF 5485 EOF
5756 chmod +x "$MOUSESH" 5486 chmod +x "$MOUSESH"
5757 "$MUXD" run --sock "$SOCK34" --shell "$MOUSESH" > "$OUT.mse.d" 2>&1 & 5487 start_daemon "$SOCK34" "$OUT.mse.d" "app-mouse daemon never bound" --shell "$MOUSESH"
5758 D31PID=$! 5488 D31PID=$DPID
5759 wait_sock "$SOCK34" "$OUT.mse.d" "app-mouse daemon never bound"
5760 i=0 5489 i=0
5761 until "$MUXD" dump --sock "$SOCK34" | grep -q "app-holds-the-mouse"; do 5490 until "$MUXD" dump --sock "$SOCK34" | grep -q "app-holds-the-mouse"; do
5762 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: app-mouse session never armed"; exit 1; } 5491 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: app-mouse session never armed"; exit 1; }
@@ -5823,9 +5552,8 @@ export LESS
5823 exec less +G $LESSDATA 5552 exec less +G $LESSDATA
5824 EOF 5553 EOF
5825 chmod +x "$LESSSH" 5554 chmod +x "$LESSSH"
5826 "$MUXD" run --sock "$SOCK35" --shell "$LESSSH" > "$OUT.pgr.d" 2>&1 & 5555 start_daemon "$SOCK35" "$OUT.pgr.d" "pager daemon never bound" --shell "$LESSSH"
5827 D32PID=$! 5556 D32PID=$DPID
5828 wait_sock "$SOCK35" "$OUT.pgr.d" "pager daemon never bound"
5829 i=0 5557 i=0
5830 until "$MUXD" dump --sock "$SOCK35" | grep -qx "200"; do 5558 until "$MUXD" dump --sock "$SOCK35" | grep -qx "200"; do
5831 i=$((i+1)); [ "$i" -lt 100 ] || { 5559 i=$((i+1)); [ "$i" -lt 100 ] || {
@@ -5880,9 +5608,8 @@ ok "the wheel scrolls a pager on the alternate screen, as arrow keys"
5880 # 5608 #
5881 # `cat` as the session, so what arrives is echoed back and the grid IS the 5609 # `cat` as the session, so what arrives is echoed back and the grid IS the
5882 # assertion: the bytes reached the pty or they did not. 5610 # assertion: the bytes reached the pty or they did not.
5883 "$MUXD" run --sock "$SOCK36" --shell /bin/cat > "$OUT.pipe.d" 2>&1 & 5611 start_daemon "$SOCK36" "$OUT.pipe.d" "pipe-stdin daemon never bound" --shell /bin/cat
5884 D33PID=$! 5612 D33PID=$DPID
5885 wait_sock "$SOCK36" "$OUT.pipe.d" "pipe-stdin daemon never bound"
5886 # The detach chord is a SEPARATE write behind a sleep, and both halves of 5613 # The detach chord is a SEPARATE write behind a sleep, and both halves of
5887 # that are load-bearing. Without it the client stays attached after stdin 5614 # that are load-bearing. Without it the client stays attached after stdin
5888 # ends — a pipe closing is not a session ending — and the leg would prove 5615 # ends — a pipe closing is not a session ending — and the leg would prove
@@ -5938,9 +5665,8 @@ ok "a client with no terminal of its own forwards SGR-shaped bytes untouched"
5938 # tile, at wall startup. A `Ctrl-\ n` that re-dialled would be three or 5665 # tile, at wall startup. A `Ctrl-\ n` that re-dialled would be three or
5939 # more, and the cumulative counter says so however briefly the extra 5666 # more, and the cumulative counter says so however briefly the extra
5940 # connection lived. Focus is client-local and sends nothing on the wire. 5667 # connection lived. Focus is client-local and sends nothing on the wire.
5941 "$MUXD" run --sock "$SOCK37" --shell /bin/sh > "$OUT.zs.d" 2>&1 & 5668 start_daemon "$SOCK37" "$OUT.zs.d" "focus-skip daemon never bound" --shell /bin/sh
5942 D34PID=$! 5669 D34PID=$DPID
5943 wait_sock "$SOCK37" "$OUT.zs.d" "focus-skip daemon never bound"
5944 5670
5945 # Both sessions exist before the wall does, each with a marker so the wall 5671 # Both sessions exist before the wall does, each with a marker so the wall
5946 # has something to paint and this leg has an anchor to enter on. 5672 # has something to paint and this leg has an anchor to enter on.
@@ -6060,9 +5786,8 @@ seq 1 100 | sed 's/^/wln/'
6060 exec /bin/cat 5786 exec /bin/cat
6061 EOF 5787 EOF
6062 chmod +x "$ZWHEELSH" 5788 chmod +x "$ZWHEELSH"
6063 "$MUXD" run --sock "$SOCK41" --shell "$ZWHEELSH" > "$OUT.zw.d" 2>&1 & 5789 start_daemon "$SOCK41" "$OUT.zw.d" "wheel daemon never bound" --shell "$ZWHEELSH"
6064 D38PID=$! 5790 D38PID=$DPID
6065 wait_sock "$SOCK41" "$OUT.zw.d" "wheel daemon never bound"
6066 # The wall attaches only once seq has finished, scenario 42's reason: the 5791 # The wall attaches only once seq has finished, scenario 42's reason: the
6067 # page the wheel fetches has to be content this tile was never sent. 5792 # page the wheel fetches has to be content this tile was never sent.
6068 i=0 5793 i=0
@@ -6131,9 +5856,8 @@ printf 'mapp-holds-the-mouse\n'
6131 exec /bin/cat 5856 exec /bin/cat
6132 EOF 5857 EOF
6133 chmod +x "$ZMOUSESH" 5858 chmod +x "$ZMOUSESH"
6134 "$MUXD" run --sock "$SOCK42" --shell "$ZMOUSESH" > "$OUT.zm2.d" 2>&1 & 5859 start_daemon "$SOCK42" "$OUT.zm2.d" "app-mouse daemon never bound" --shell "$ZMOUSESH"
6135 D39PID=$! 5860 D39PID=$DPID
6136 wait_sock "$SOCK42" "$OUT.zm2.d" "app-mouse daemon never bound"
6137 i=0 5861 i=0
6138 until "$MUXD" dump --sock "$SOCK42" | grep -q "mapp-holds-the-mouse"; do 5862 until "$MUXD" dump --sock "$SOCK42" | grep -q "mapp-holds-the-mouse"; do
6139 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: app mouse: session never armed"; exit 1; } 5863 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: app mouse: session never armed"; exit 1; }
@@ -6197,9 +5921,8 @@ ok "an application in the focused tile gets the wheel, and the tile does not"
6197 # treatment. It must not — it still has a thread that will repaint its hot 5921 # treatment. It must not — it still has a thread that will repaint its hot
6198 # replica within a poll timeout, and the keyboard drawing over that would 5922 # replica within a poll timeout, and the keyboard drawing over that would
6199 # replace something true with something stale. 5923 # replace something true with something stale.
6200 "$MUXD" run --sock "$SOCK39" --shell /bin/sh > "$OUT.zd.d" 2>&1 & 5924 start_daemon "$SOCK39" "$OUT.zd.d" "dead-tile daemon never bound" --shell /bin/sh
6201 D36PID=$! 5925 D36PID=$DPID
6202 wait_sock "$SOCK39" "$OUT.zd.d" "dead-tile daemon never bound"
6203 pipe_mux "$OUT.zda" "$OUT.zda.err" timeout 40 "$MUX" --sock "$SOCK39" --session a 5926 pipe_mux "$OUT.zda" "$OUT.zda.err" timeout 40 "$MUX" --sock "$SOCK39" --session a
6204 pipe_send 'printf "zdlive-%%s\\n" pin\n' 5927 pipe_send 'printf "zdlive-%%s\\n" pin\n'
6205 await_out "$OUT.zda" "zdlive-pin" "zdlive-pin never reached the client" 5928 await_out "$OUT.zda" "zdlive-pin" "zdlive-pin never reached the client"
@@ -6298,9 +6021,8 @@ ok "a tile whose pump has died narrates on its bar and on its prompt, and the fo
6298 # Then the two failure faces: an unwritable wall file warns and does NOT 6021 # Then the two failure faces: an unwritable wall file warns and does NOT
6299 # stop the attach, and 'mux wall add' refuses a bad spelling by name 6022 # stop the attach, and 'mux wall add' refuses a bad spelling by name
6300 # without touching the file. 6023 # without touching the file.
6301 "$MUXD" run --sock "$SOCK40" --shell /bin/sh > "$OUT.wh.d" 2>&1 & 6024 start_daemon "$SOCK40" "$OUT.wh.d" "attach-history daemon never bound" --shell /bin/sh
6302 D37PID=$! 6025 D37PID=$DPID
6303 wait_sock "$SOCK40" "$OUT.wh.d" "attach-history daemon never bound"
6304 6026
6305 pipe_mux "$OUT.wh1" "$OUT.wh1.err" env XDG_STATE_HOME="$WHSTATE" timeout 40 "$MUX" --sock "$SOCK40" 6027 pipe_mux "$OUT.wh1" "$OUT.wh1.err" env XDG_STATE_HOME="$WHSTATE" timeout 40 "$MUX" --sock "$SOCK40"
6306 pipe_send 'printf "wh1-%%s\\n" pin\n' 6028 pipe_send 'printf "wh1-%%s\\n" pin\n'
@@ -6584,9 +6306,8 @@ ok "\\x1cx forgets a tile and leaves its session running; a refusal records noth
6584 # A state home of its own: the wall `Ctrl-\ w` folds in is the saved one, 6306 # A state home of its own: the wall `Ctrl-\ w` folds in is the saved one,
6585 # and the shared $XDG_STATE_HOME is every socket the suite has ever 6307 # and the shared $XDG_STATE_HOME is every socket the suite has ever
6586 # attached to. 6308 # attached to.
6587 "$MUXD" run --sock "$SOCK45" --shell /bin/sh > "$OUT.cv.d" 2>&1 & 6309 start_daemon "$SOCK45" "$OUT.cv.d" "convergence daemon never bound" --shell /bin/sh
6588 D45PID=$! 6310 D45PID=$DPID
6589 wait_sock "$SOCK45" "$OUT.cv.d" "convergence daemon never bound"
6590 6311
6591 # The tile that is already on the wall before this leg's client starts. A 6312 # The tile that is already on the wall before this leg's client starts. A
6592 # piped `mux`, so its attach writes the line and then goes: what the leg 6313 # piped `mux`, so its attach writes the line and then goes: what the leg
@@ -6611,6 +6332,7 @@ CVATT_BEFORE=$(attaches_now "$SOCK45")
6611 ( sleep 6; "$MUXA" send 'printf "cvret-%s\n" pin\n' \ 6332 ( sleep 6; "$MUXA" send 'printf "cvret-%s\n" pin\n' \
6612 --sock "$SOCK45" --session 0 > "$OUT.cvinj" 2>&1 ) & 6333 --sock "$SOCK45" --session 0 > "$OUT.cvinj" 2>&1 ) &
6613 CVINJPID=$! 6334 CVINJPID=$!
6335 defer_kill "$CVINJPID"
6614 set +e 6336 set +e
6615 # tall: Ctrl-\ w folds in a saved tile, making two at 100x30 (100 >= 60); 6337 # tall: Ctrl-\ w folds in a saved tile, making two at 100x30 (100 >= 60);
6616 # 70x36 stays stacked (70 < 72) and fits the bar label the assertion greps 6338 # 70x36 stays stacked (70 < 72) and fits the bar label the assertion greps
@@ -6687,9 +6409,8 @@ ok "mux is the wall: Ctrl-\\ w folds in the saved tiles, the focus stays put, Ct
6687 # free. Sampled around the whole ptyclient run rather than around each 6409 # free. Sampled around the whole ptyclient run rather than around each
6688 # step, because a step that dialled and closed between two samples would 6410 # step, because a step that dialled and closed between two samples would
6689 # hide inside a gauge — the counter is cumulative and cannot. 6411 # hide inside a gauge — the counter is cumulative and cannot.
6690 "$MUXD" run --sock "$SOCK46" --shell /bin/sh > "$OUT.rg.d" 2>&1 & 6412 start_daemon "$SOCK46" "$OUT.rg.d" "ring-grow daemon never bound" --shell /bin/sh
6691 D46PID=$! 6413 D46PID=$DPID
6692 wait_sock "$SOCK46" "$OUT.rg.d" "ring-grow daemon never bound"
6693 6414
6694 # `two` exists on the DAEMON but not in this leg's wall: a separate state 6415 # `two` exists on the DAEMON but not in this leg's wall: a separate state
6695 # home is what makes the setup client's own tile invisible here, so the 6416 # home is what makes the setup client's own tile invisible here, so the
@@ -6787,9 +6508,8 @@ ok "Ctrl-\\ n grows the wall for a sibling with no tile, and is free for one tha
6787 # * two tiles and a PIPED client whose stdin has already gone. The case 6508 # * two tiles and a PIPED client whose stdin has already gone. The case
6788 # review reproduced as a hang, and the reason the rule's second half is 6509 # review reproduced as a hang, and the reason the rule's second half is
6789 # stated in `endAction` rather than at the read that notices EOF. 6510 # stated in `endAction` rather than at the read that notices EOF.
6790 "$MUXD" run --sock "$SOCK47" --shell /bin/sh > "$OUT.xe.d" 2>&1 & 6511 start_daemon "$SOCK47" "$OUT.xe.d" "exit-semantics daemon never bound" --shell /bin/sh
6791 D47PID=$! 6512 D47PID=$DPID
6792 wait_sock "$SOCK47" "$OUT.xe.d" "exit-semantics daemon never bound"
6793 6513
6794 # A wall it dropped to instead would read as 124: it never leaves, because 6514 # A wall it dropped to instead would read as 124: it never leaves, because
6795 # nothing closes its stdin under it. 6515 # nothing closes its stdin under it.
@@ -6885,9 +6605,8 @@ ok "a session ending in the focused tile exits mux alone, and leaves the focus t
6885 # fingerprint. What puts those 50 bytes on the terminal is the agent on this 6605 # fingerprint. What puts those 50 bytes on the terminal is the agent on this
6886 # machine answering a request that left the session, crossed the daemon, 6606 # machine answering a request that left the session, crossed the daemon,
6887 # reached the client, and came back. 6607 # reached the client, and came back.
6888 "$MUXD" run --sock "$SOCK48" --shell /bin/sh > "$OUT.agt.d" 2>&1 & 6608 start_daemon "$SOCK48" "$OUT.agt.d" "agent daemon never bound" --shell /bin/sh
6889 D42PID=$! 6609 D42PID=$DPID
6890 wait_sock "$SOCK48" "$OUT.agt.d" "agent daemon never bound"
6891 6610
6892 # The agent is started AFTER the daemon and its path is never exported, so 6611 # The agent is started AFTER the daemon and its path is never exported, so
6893 # the daemon cannot have inherited it: the only SSH_AUTH_SOCK a session on 6612 # the daemon cannot have inherited it: the only SSH_AUTH_SOCK a session on
@@ -6895,6 +6614,7 @@ wait_sock "$SOCK48" "$OUT.agt.d" "agent daemon never bound"
6895 # makes the fingerprint below evidence of a forward rather than of a leak. 6614 # makes the fingerprint below evidence of a forward rather than of a leak.
6896 ssh-agent -a "$AGENT48" > "$OUT.agt.env" 2>&1 6615 ssh-agent -a "$AGENT48" > "$OUT.agt.env" 2>&1
6897 AGENT48PID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.agt.env") 6616 AGENT48PID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.agt.env")
6617 defer_kill "$AGENT48PID"
6898 [ -n "$AGENT48PID" ] || { 6618 [ -n "$AGENT48PID" ] || {
6899 echo "e2e FAIL: agent: ssh-agent printed no pid for the trap to hold it by:" 6619 echo "e2e FAIL: agent: ssh-agent printed no pid for the trap to hold it by:"
6900 cat "$OUT.agt.env"; exit 1; } 6620 cat "$OUT.agt.env"; exit 1; }
@@ -7010,9 +6730,8 @@ ok "agent forwarding: -A with no agent is a usage error, not a silent no-op"
7010 # falls straight through to its other methods, where a connection accepted 6730 # falls straight through to its other methods, where a connection accepted
7011 # and left silent would make it wait out a timeout on every dial. 6731 # and left silent would make it wait out a timeout on every dial.
7012 wait_pid_gone "$D42PID" "agent forwarding: the positive leg's daemon outlived its last session" 6732 wait_pid_gone "$D42PID" "agent forwarding: the positive leg's daemon outlived its last session"
7013 "$MUXD" run --sock "$SOCK48" --shell /bin/sh > "$OUT.agtn.d" 2>&1 & 6733 start_daemon "$SOCK48" "$OUT.agtn.d" "agent-refusal daemon never bound" --shell /bin/sh
7014 D42PID=$! 6734 D42PID=$DPID
7015 wait_sock "$SOCK48" "$OUT.agtn.d" "agent-refusal daemon never bound"
7016 AR0=$(date +%s%N) 6735 AR0=$(date +%s%N)
7017 set +e 6736 set +e
7018 timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.agtn" --err "$OUT.agtn.err" \ 6737 timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.agtn" --err "$OUT.agtn.err" \
@@ -7145,17 +6864,18 @@ ok "agent forwarding: no offerer means a fast refusal, not a hang (${AMS}ms)"
7145 # OWN key. `ssh-add -l` typed at one keyboard and then at the other has to 6864 # OWN key. `ssh-add -l` typed at one keyboard and then at the other has to
7146 # come back with two DIFFERENT fingerprints — that is the spec's "latest 6865 # come back with two DIFFERENT fingerprints — that is the spec's "latest
7147 # wins, applied to keys", and it is a claim only two agents can make. 6866 # wins, applied to keys", and it is a claim only two agents can make.
7148 "$MUXD" run --sock "$SOCK49" --shell /bin/sh > "$OUT.flip.d" 2>&1 & 6867 start_daemon "$SOCK49" "$OUT.flip.d" "agent-flip daemon never bound" --shell /bin/sh
7149 D43PID=$! 6868 D43PID=$DPID
7150 wait_sock "$SOCK49" "$OUT.flip.d" "agent-flip daemon never bound"
7151 6869
7152 # Both agents after the daemon, for the ordering reason the leg above gives: 6870 # Both agents after the daemon, for the ordering reason the leg above gives:
7153 # a path the daemon cannot have inherited is what makes a fingerprint 6871 # a path the daemon cannot have inherited is what makes a fingerprint
7154 # evidence of a forward rather than of a leak. 6872 # evidence of a forward rather than of a leak.
7155 ssh-agent -a "$AGENT49A" > "$OUT.flipa.env" 2>&1 6873 ssh-agent -a "$AGENT49A" > "$OUT.flipa.env" 2>&1
7156 AGENT49APID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.flipa.env") 6874 AGENT49APID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.flipa.env")
6875 defer_kill "$AGENT49APID"
7157 ssh-agent -a "$AGENT49B" > "$OUT.flipb.env" 2>&1 6876 ssh-agent -a "$AGENT49B" > "$OUT.flipb.env" 2>&1
7158 AGENT49BPID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.flipb.env") 6877 AGENT49BPID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.flipb.env")
6878 defer_kill "$AGENT49BPID"
7159 [ -n "$AGENT49APID" ] && [ -n "$AGENT49BPID" ] || { 6879 [ -n "$AGENT49APID" ] && [ -n "$AGENT49BPID" ] || {
7160 echo "e2e FAIL: agent-flip: an ssh-agent printed no pid for the trap to hold" 6880 echo "e2e FAIL: agent-flip: an ssh-agent printed no pid for the trap to hold"
7161 cat "$OUT.flipa.env" "$OUT.flipb.env"; exit 1; } 6881 cat "$OUT.flipa.env" "$OUT.flipb.env"; exit 1; }
@@ -7230,6 +6950,7 @@ send exit\n
7230 waitexit 20000 6950 waitexit 20000
7231 EOF 6951 EOF
7232 FLIPAPID=$! 6952 FLIPAPID=$!
6953 defer_kill "$FLIPAPID"
7233 # B settles before its first keystroke and A does not need to: a shell still 6954 # B settles before its first keystroke and A does not need to: a shell still
7234 # setting up its terminal can flush what was typed at it, and B's marker is 6955 # setting up its terminal can flush what was typed at it, and B's marker is
7235 # the only line typed before any shell has spoken. Every later send waits on 6956 # the only line typed before any shell has spoken. Every later send waits on
@@ -7298,9 +7019,8 @@ ok "agent forwarding: the agent that answers is whoever typed last"
7298 # with is gone — every click and drag routes to the focused tile's own 7019 # with is gone — every click and drag routes to the focused tile's own
7299 # Core, hit-tested by rect — so what remains here is the setup: a daemon, 7020 # Core, hit-tested by rect — so what remains here is the setup: a daemon,
7300 # a marker per session, and the tiles a `mux wall` over them paints. 7021 # a marker per session, and the tiles a `mux wall` over them paints.
7301 "$MUXD" run --sock "$SOCK50" --shell /bin/sh > "$OUT.wmse.d" 2>&1 & 7022 start_daemon "$SOCK50" "$OUT.wmse.d" "wall-cluster daemon never bound" --shell /bin/sh
7302 D48PID=$! 7023 D48PID=$DPID
7303 wait_sock "$SOCK50" "$OUT.wmse.d" "wall-cluster daemon never bound"
7304 { printf 'printf "wma-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \ 7024 { printf 'printf "wma-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
7305 timeout 40 "$MUX" --sock "$SOCK50" --session a > "$OUT.wmsa" 2> "$OUT.wmsa.err" 7025 timeout 40 "$MUX" --sock "$SOCK50" --session a > "$OUT.wmsa" 2> "$OUT.wmsa.err"
7306 wait_grid "$SOCK50" "wma-pin" "wall cluster: session a's marker" a 7026 wait_grid "$SOCK50" "wma-pin" "wall cluster: session a's marker" a
@@ -7533,9 +7253,8 @@ ok "a drag copies on release, and a click copies nothing"
7533 # so the report — INPUT to the app — lands on the grid as text. Two reports 7253 # so the report — INPUT to the app — lands on the grid as text. Two reports
7534 # are asserted, because they are two code paths: the one the mode-set 7254 # are asserted, because they are two code paths: the one the mode-set
7535 # itself owes (the app sizes itself from it), and the one each resize owes. 7255 # itself owes (the app sizes itself from it), and the one each resize owes.
7536 "$MUXD" run --sock "$SOCK51" --shell /bin/sh > "$OUT.inband.d" 2>&1 & 7256 start_daemon "$SOCK51" "$OUT.inband.d" "in-band daemon never bound" --shell /bin/sh
7537 D51PID=$! 7257 D51PID=$DPID
7538 wait_sock "$SOCK51" "$OUT.inband.d" "in-band daemon never bound"
7539 set +e 7258 set +e
7540 timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.inband" --err "$OUT.inband.err" -- \ 7259 timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.inband" --err "$OUT.inband.err" -- \
7541 "$MUX" --sock "$SOCK51" > "$OUT.inband.log" 2>&1 <<'EOF' 7260 "$MUX" --sock "$SOCK51" > "$OUT.inband.log" 2>&1 <<'EOF'
@@ -7606,11 +7325,11 @@ ok "no XDG_RUNTIME_DIR: the default path is refused by name, --version is not"
7606 # channel for the client that would not), and the SECOND is refused in 7325 # channel for the client that would not), and the SECOND is refused in
7607 # milliseconds (the offer was taken away, so nobody routes to the mute 7326 # milliseconds (the offer was taken away, so nobody routes to the mute
7608 # client again and ssh pays nothing). 7327 # client again and ssh pays nothing).
7609 "$MUXD" run --sock "$SOCK48" --shell /bin/sh > "$OUT.agtmute.d" 2>&1 & 7328 start_daemon "$SOCK48" "$OUT.agtmute.d" "agent-mute daemon never bound" --shell /bin/sh
7610 D42PID=$! 7329 D42PID=$DPID
7611 wait_sock "$SOCK48" "$OUT.agtmute.d" "agent-mute daemon never bound"
7612 ssh-agent -a "$AGENT48" > "$OUT.agtmute.env" 2>&1 7330 ssh-agent -a "$AGENT48" > "$OUT.agtmute.env" 2>&1
7613 AGENT48PID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.agtmute.env") 7331 AGENT48PID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.agtmute.env")
7332 defer_kill "$AGENT48PID"
7614 [ -n "$AGENT48PID" ] || { 7333 [ -n "$AGENT48PID" ] || {
7615 echo "e2e FAIL: agent-mute: ssh-agent printed no pid to stop:" 7334 echo "e2e FAIL: agent-mute: ssh-agent printed no pid to stop:"
7616 cat "$OUT.agtmute.env"; exit 1; } 7335 cat "$OUT.agtmute.env"; exit 1; }
@@ -7683,9 +7402,8 @@ ok "agent forwarding: a mute offerer is hung up on, then no longer offered (${MU
7683 # never an echo of anything typed here). The ptyclient wall focuses tile 3, 7402 # never an echo of anything typed here). The ptyclient wall focuses tile 3,
7684 # settles, and detaches; the capture's last cursor-position escape is the 7403 # settles, and detaches; the capture's last cursor-position escape is the
7685 # witness. 7404 # witness.
7686 "$MUXD" run --sock "$SOCK52" --shell /bin/sh > "$OUT.cu.d" 2>&1 & 7405 start_daemon "$SOCK52" "$OUT.cu.d" "cursor-ownership daemon never bound" --shell /bin/sh
7687 D52PID=$! 7406 D52PID=$DPID
7688 wait_sock "$SOCK52" "$OUT.cu.d" "cursor-ownership daemon never bound"
7689 7407
7690 pipe_mux "$OUT.cua" "$OUT.cua.err" timeout 40 "$MUX" --sock "$SOCK52" --session a 7408 pipe_mux "$OUT.cua" "$OUT.cua.err" timeout 40 "$MUX" --sock "$SOCK52" --session a
7691 pipe_send 'printf "ma-%%s\\n" pin\n' 7409 pipe_send 'printf "ma-%%s\\n" pin\n'
@@ -7754,9 +7472,8 @@ ok "the cursor rests in the focused tile, whichever pump painted last"
7754 # pin and is never touched again. Both shells are /bin/sh and the content 7472 # pin and is never touched again. Both shells are /bin/sh and the content
7755 # is typed, the focus-skip leg's pattern — the neighbour's pin is the 7473 # is typed, the focus-skip leg's pattern — the neighbour's pin is the
7756 # session's own output, not an echo of the command that made it. 7474 # session's own output, not an echo of the command that made it.
7757 "$MUXD" run --sock "$SOCK53" --shell /bin/sh > "$OUT.sb2.d" 2>&1 & 7475 start_daemon "$SOCK53" "$OUT.sb2.d" "scrollback-rect daemon never bound" --shell /bin/sh
7758 D54PID=$! 7476 D54PID=$DPID
7759 wait_sock "$SOCK53" "$OUT.sb2.d" "scrollback-rect daemon never bound"
7760 7477
7761 # Session a: 100 lines of history, then cat. The wall attaches only once 7478 # Session a: 100 lines of history, then cat. The wall attaches only once
7762 # seq has finished, so the page the wheel fetches is content this tile was 7479 # seq has finished, so the page the wheel fetches is content this tile was
@@ -7817,9 +7534,9 @@ ok "a focused tile's scrollback page owns only its sub-rect, and the neighbour's
7817 # is only reached by a run that passed, and a leak deserves reporting on the 7534 # is only reached by a run that passed, and a leak deserves reporting on the
7818 # runs that did not (see leak_sweep). Everything below it is a pin, and the 7535 # runs that did not (see leak_sweep). Everything below it is a pin, and the
7819 # trap fires after all of them either way. 7536 # trap fires after all of them either way.
7820 softkill "$DPID" || true 7537 softkill "$D1PID" || true
7821 wait "$DPID" 2>/dev/null || true 7538 wait "$D1PID" 2>/dev/null || true
7822 DPID="" 7539 D1PID=""
7823 7540
7824 # The pins. Literals, not variables set from counting something else — 7541 # The pins. Literals, not variables set from counting something else —
7825 # "assert the literal, never the constant the code under test reads" 7542 # "assert the literal, never the constant the code under test reads"
@@ -7854,9 +7571,8 @@ DPID=""
7854 # 7571 #
7855 # No convergence point: `muxa capture` reads the daemon's grid, not the 7572 # No convergence point: `muxa capture` reads the daemon's grid, not the
7856 # terminal's, and the daemon's grid is the side channel this asserts on. 7573 # terminal's, and the daemon's grid is the side channel this asserts on.
7857 "$MUXD" run --sock "$SOCK54" --shell /bin/sh > "$OUT.fs.d" 2>&1 & 7574 start_daemon "$SOCK54" "$OUT.fs.d" "fullscreen daemon never bound" --shell /bin/sh
7858 D55PID=$! 7575 D55PID=$DPID
7859 wait_sock "$SOCK54" "$OUT.fs.d" "fullscreen daemon never bound"
7860 7576
7861 pipe_mux "$OUT.fsa" "$OUT.fsa.err" timeout 40 "$MUX" --sock "$SOCK54" --session a 7577 pipe_mux "$OUT.fsa" "$OUT.fsa.err" timeout 40 "$MUX" --sock "$SOCK54" --session a
7862 pipe_send 'printf "fs-%%s\\n" init-a\n' 7578 pipe_send 'printf "fs-%%s\\n" init-a\n'
@@ -7940,9 +7656,8 @@ ok "fullscreen gives the focused tile the terminal; focus follows; f restores"
7940 # 7656 #
7941 # No convergence point: the rail column is a terminal-capture assertion, 7657 # No convergence point: the rail column is a terminal-capture assertion,
7942 # not a daemon-grid comparison. 7658 # not a daemon-grid comparison.
7943 "$MUXD" run --sock "$SOCK55" --shell /bin/sh > "$OUT.rsz.d" 2>&1 & 7659 start_daemon "$SOCK55" "$OUT.rsz.d" "resize daemon never bound" --shell /bin/sh
7944 D56PID=$! 7660 D56PID=$DPID
7945 wait_sock "$SOCK55" "$OUT.rsz.d" "resize daemon never bound"
7946 7661
7947 pipe_mux "$OUT.rsza" "$OUT.rsza.err" timeout 40 "$MUX" --sock "$SOCK55" --session a 7662 pipe_mux "$OUT.rsza" "$OUT.rsza.err" timeout 40 "$MUX" --sock "$SOCK55" --session a
7948 pipe_send 'printf "rsz-%%s\\n" init-a\n' 7663 pipe_send 'printf "rsz-%%s\\n" init-a\n'
@@ -8020,9 +7735,8 @@ ok "Ctrl-\\ r trades cells between panes; Esc returns to prose"
8020 # still passes a byte grep, so the assertion replays the wall client's 7735 # still passes a byte grep, so the assertion replays the wall client's
8021 # FULL captured stdout through the render fixture (an engine, not a grep) 7736 # FULL captured stdout through the render fixture (an engine, not a grep)
8022 # and checks the right pane's marker text survives on the replayed grid. 7737 # and checks the right pane's marker text survives on the replayed grid.
8023 "$MUXD" run --sock "$SOCK56" --shell /bin/sh > "$OUT.spc.d" 2>&1 & 7738 start_daemon "$SOCK56" "$OUT.spc.d" "span-clear daemon never bound" --shell /bin/sh
8024 D57PID=$! 7739 D57PID=$DPID
8025 wait_sock "$SOCK56" "$OUT.spc.d" "span-clear daemon never bound"
8026 7740
8027 pipe_mux "$OUT.spca" "$OUT.spca.err" timeout 40 "$MUX" --sock "$SOCK56" --session a 7741 pipe_mux "$OUT.spca" "$OUT.spca.err" timeout 40 "$MUX" --sock "$SOCK56" --session a
8028 pipe_send 'printf "spc-%%s\\n" left-flood\n' 7742 pipe_send 'printf "spc-%%s\\n" left-flood\n'
@@ -8078,9 +7792,8 @@ ok "a span-clear flood in the left pane cannot blank the right pane's grid"
8078 # 7792 #
8079 # No convergence point: the assertion is per-session muxa capture, not a 7793 # No convergence point: the assertion is per-session muxa capture, not a
8080 # daemon-grid diff. 7794 # daemon-grid diff.
8081 "$MUXD" run --sock "$SOCK57" --shell /bin/sh > "$OUT.hjk.d" 2>&1 & 7795 start_daemon "$SOCK57" "$OUT.hjk.d" "hjkl daemon never bound" --shell /bin/sh
8082 D58PID=$! 7796 D58PID=$DPID
8083 wait_sock "$SOCK57" "$OUT.hjk.d" "hjkl daemon never bound"
8084 7797
8085 pipe_mux "$OUT.hjkcap" "$OUT.hjkcap.err" timeout 40 "$MUX" --sock "$SOCK57" --session a 7798 pipe_mux "$OUT.hjkcap" "$OUT.hjkcap.err" timeout 40 "$MUX" --sock "$SOCK57" --session a
8086 pipe_send 'printf "hjk-%%s\\n" origin\n' 7799 pipe_send 'printf "hjk-%%s\\n" origin\n'
@@ -8149,9 +7862,8 @@ ok "hjkl walks focus across panes in both axes"
8149 # daemon's session count rises to two, and the new pane's label bar sits 7862 # daemon's session count rises to two, and the new pane's label bar sits
8150 # at a column to the right of the rail (the rail paints as ESC[r;cH 7863 # at a column to the right of the rail (the rail paints as ESC[r;cH
8151 # ESC[7m — the same idiom the resize leg uses). 7864 # ESC[7m — the same idiom the resize leg uses).
8152 "$MUXD" run --sock "$SOCK58" --shell /bin/sh > "$OUT.spl.d" 2>&1 & 7865 start_daemon "$SOCK58" "$OUT.spl.d" "split-birth daemon never bound" --shell /bin/sh
8153 D59PID=$! 7866 D59PID=$DPID
8154 wait_sock "$SOCK58" "$OUT.spl.d" "split-birth daemon never bound"
8155 7867
8156 pipe_mux "$OUT.splcap" "$OUT.splcap.err" timeout 40 "$MUX" --sock "$SOCK58" --session a 7868 pipe_mux "$OUT.splcap" "$OUT.splcap.err" timeout 40 "$MUX" --sock "$SOCK58" --session a
8157 pipe_send 'printf "spl-%%s\\n" origin\n' 7869 pipe_send 'printf "spl-%%s\\n" origin\n'
@@ -8210,11 +7922,11 @@ ok "Ctrl-\\ | births a session beside; its label bar sits right of the rail"
8210 # right-resizes from a 40|40 split), and a marker typed into the restored 7922 # right-resizes from a 40|40 split), and a marker typed into the restored
8211 # wall lands in the focused session. 7923 # wall lands in the focused session.
8212 LPSTATE="${TMPDIR:-/tmp}/mux-e2e-lprrestore-state-$$" 7924 LPSTATE="${TMPDIR:-/tmp}/mux-e2e-lprrestore-state-$$"
7925 defer_rm "$LPSTATE"
8213 LPWALL="$LPSTATE/mux/wall" 7926 LPWALL="$LPSTATE/mux/wall"
8214 LPLAYOUT="$LPSTATE/mux/layout" 7927 LPLAYOUT="$LPSTATE/mux/layout"
8215 "$MUXD" run --sock "$SOCK59" --shell /bin/sh > "$OUT.lpr.d" 2>&1 & 7928 start_daemon "$SOCK59" "$OUT.lpr.d" "layout-restore daemon never bound" --shell /bin/sh
8216 D60PID=$! 7929 D60PID=$DPID
8217 wait_sock "$SOCK59" "$OUT.lpr.d" "layout-restore daemon never bound"
8218 7930
8219 pipe_mux "$OUT.lpra" "$OUT.lpra.err" timeout 40 "$MUX" --sock "$SOCK59" --session a 7931 pipe_mux "$OUT.lpra" "$OUT.lpra.err" timeout 40 "$MUX" --sock "$SOCK59" --session a
8220 pipe_send 'printf "lpr-%%s\\n" origin-a\n' 7932 pipe_send 'printf "lpr-%%s\\n" origin-a\n'
@@ -8335,11 +8047,11 @@ ok "a resized layout survives a detach/reattach round trip via the sidecar"
8335 # and the unmatched saved leaf collapses out. Two tiles, not three, and 8047 # and the unmatched saved leaf collapses out. Two tiles, not three, and
8336 # the wall did not refuse. 8048 # the wall did not refuse.
8337 LPHSTATE="${TMPDIR:-/tmp}/mux-e2e-lpheal-state-$$" 8049 LPHSTATE="${TMPDIR:-/tmp}/mux-e2e-lpheal-state-$$"
8050 defer_rm "$LPHSTATE"
8338 LPHWALL="$LPHSTATE/mux/wall" 8051 LPHWALL="$LPHSTATE/mux/wall"
8339 LPHLAYOUT="$LPHSTATE/mux/layout" 8052 LPHLAYOUT="$LPHSTATE/mux/layout"
8340 "$MUXD" run --sock "$SOCK60" --shell /bin/sh > "$OUT.lph.d" 2>&1 & 8053 start_daemon "$SOCK60" "$OUT.lph.d" "layout-heal daemon never bound" --shell /bin/sh
8341 D61PID=$! 8054 D61PID=$DPID
8342 wait_sock "$SOCK60" "$OUT.lph.d" "layout-heal daemon never bound"
8343 8055
8344 pipe_mux "$OUT.lpha" "$OUT.lpha.err" timeout 40 "$MUX" --sock "$SOCK60" --session a 8056 pipe_mux "$OUT.lpha" "$OUT.lpha.err" timeout 40 "$MUX" --sock "$SOCK60" --session a
8345 pipe_send 'printf "lph-%%s\\n" survivor-a\n' 8057 pipe_send 'printf "lph-%%s\\n" survivor-a\n'
@@ -8430,11 +8142,11 @@ ok "the layout heals on wall-file drift: survivor keeps its session, newcomer jo
8430 # session (the wall works), and the client's stderr never mentions 8142 # session (the wall works), and the client's stderr never mentions
8431 # "layout" (the degrade is silent). 8143 # "layout" (the degrade is silent).
8432 LPDSTATE="${TMPDIR:-/tmp}/mux-e2e-lpdegrade-state-$$" 8144 LPDSTATE="${TMPDIR:-/tmp}/mux-e2e-lpdegrade-state-$$"
8145 defer_rm "$LPDSTATE"
8433 LPDWALL="$LPDSTATE/mux/wall" 8146 LPDWALL="$LPDSTATE/mux/wall"
8434 LPDLAYOUT="$LPDSTATE/mux/layout" 8147 LPDLAYOUT="$LPDSTATE/mux/layout"
8435 "$MUXD" run --sock "$SOCK61" --shell /bin/sh > "$OUT.lpd.d" 2>&1 & 8148 start_daemon "$SOCK61" "$OUT.lpd.d" "layout-degrade daemon never bound" --shell /bin/sh
8436 D62PID=$! 8149 D62PID=$DPID
8437 wait_sock "$SOCK61" "$OUT.lpd.d" "layout-degrade daemon never bound"
8438 8150
8439 pipe_mux "$OUT.lpda" "$OUT.lpda.err" timeout 40 "$MUX" --sock "$SOCK61" --session a 8151 pipe_mux "$OUT.lpda" "$OUT.lpda.err" timeout 40 "$MUX" --sock "$SOCK61" --session a
8440 pipe_send 'printf "lpd-%%s\\n" origin\n' 8152 pipe_send 'printf "lpd-%%s\\n" origin\n'
@@ -8492,13 +8204,12 @@ ok "a corrupted sidecar degrades silently to the default layout"
8492 # label on the capture (the echo would match) but a marker typed AFTER 8204 # label on the capture (the echo would match) but a marker typed AFTER
8493 # the birth landing in daemon B's session b — a hit is B's shell's work. 8205 # the birth landing in daemon B's session b — a hit is B's shell's work.
8494 PRSTATE="${TMPDIR:-/tmp}/mux-e2e-prompt-state-$$" 8206 PRSTATE="${TMPDIR:-/tmp}/mux-e2e-prompt-state-$$"
8207 defer_rm "$PRSTATE"
8495 PRWALL="$PRSTATE/mux/wall" 8208 PRWALL="$PRSTATE/mux/wall"
8496 "$MUXD" run --sock "$SOCK62" --shell /bin/sh > "$OUT.pra.d" 2>&1 & 8209 start_daemon "$SOCK62" "$OUT.pra.d" "prompt daemon A never bound" --shell /bin/sh
8497 D63PID=$! 8210 D63PID=$DPID
8498 wait_sock "$SOCK62" "$OUT.pra.d" "prompt daemon A never bound" 8211 start_daemon "$SOCK63" "$OUT.prb.d" "prompt daemon B never bound" --shell /bin/sh
8499 "$MUXD" run --sock "$SOCK63" --shell /bin/sh > "$OUT.prb.d" 2>&1 & 8212 D64PID=$DPID
8500 D64PID=$!
8501 wait_sock "$SOCK63" "$OUT.prb.d" "prompt daemon B never bound"
8502 8213
8503 pipe_mux "$OUT.pra" "$OUT.pra.err" timeout 40 "$MUX" --sock "$SOCK62" --session a 8214 pipe_mux "$OUT.pra" "$OUT.pra.err" timeout 40 "$MUX" --sock "$SOCK62" --session a
8504 pipe_send 'printf "pr-%%s\\n" origin\n' 8215 pipe_send 'printf "pr-%%s\\n" origin\n'
@@ -8620,10 +8331,9 @@ ok "Ctrl-\\ : adds a tile by spelling: born on another daemon, recorded, refusal
8620 # loop, and the `Ctrl-\ w` half at the end of this leg runs `hydrate()`. 8331 # loop, and the `Ctrl-\ w` half at the end of this leg runs `hydrate()`.
8621 head -c 32 /dev/urandom > "$HYKEY" 8332 head -c 32 /dev/urandom > "$HYKEY"
8622 chmod 600 "$HYKEY" 8333 chmod 600 "$HYKEY"
8623 "$MUXD" run --sock "$SOCK64" --shell /bin/sh \ 8334 start_daemon "$SOCK64" "$OUT.hyd.d" "hydrate-create daemon never bound" --shell /bin/sh \
8624 --quic "127.0.0.1:$HYPORT" --key "$HYKEY" --quic-idle-ms 15000 > "$OUT.hyd.d" 2>&1 & 8335 --quic "127.0.0.1:$HYPORT" --key "$HYKEY" --quic-idle-ms 15000
8625 D65PID=$! 8336 D65PID=$DPID
8626 wait_sock "$SOCK64" "$OUT.hyd.d" "hydrate-create daemon never bound"
8627 8337
8628 # The wall file names three sessions this daemon does not have. Written by 8338 # The wall file names three sessions this daemon does not have. Written by
8629 # hand rather than earned by an attach, because an attach that earned the 8339 # hand rather than earned by an attach, because an attach that earned the
@@ -8781,6 +8491,7 @@ ok "a saved local line comes back as a fresh session, by wall and by fold; a rem
8781 mkdir -p "$REFSTATE" 8491 mkdir -p "$REFSTATE"
8782 XDG_STATE_HOME="$REFSTATE" "$MUXD" run --sock "$SOCK66" --shell /bin/sh > "$OUT.ref.d" 2>&1 & 8492 XDG_STATE_HOME="$REFSTATE" "$MUXD" run --sock "$SOCK66" --shell /bin/sh > "$OUT.ref.d" 2>&1 &
8783 D66PID=$! 8493 D66PID=$!
8494 defer_kill "$D66PID"
8784 wait_sock "$SOCK66" "$OUT.ref.d" "refused-attach daemon never bound" 8495 wait_sock "$SOCK66" "$OUT.ref.d" "refused-attach daemon never bound"
8785 8496
8786 # Each verb twice over: the words it must say, and the words it must not. 8497 # Each verb twice over: the words it must say, and the words it must not.
@@ -8868,10 +8579,9 @@ ok "muxa names a refused attach instead of a shell that never ran"
8868 # other half: one birth, not two, and nothing born for the remote line. 8579 # other half: one birth, not two, and nothing born for the remote line.
8869 head -c 32 /dev/urandom > "$WGKEY" 8580 head -c 32 /dev/urandom > "$WGKEY"
8870 chmod 600 "$WGKEY" 8581 chmod 600 "$WGKEY"
8871 "$MUXD" run --sock "$SOCK67" --shell /bin/sh \ 8582 start_daemon "$SOCK67" "$OUT.wg.d" "web-restore daemon never bound" --shell /bin/sh \
8872 --quic "127.0.0.1:$WGQPORT" --key "$WGKEY" --quic-idle-ms 15000 > "$OUT.wg.d" 2>&1 & 8583 --quic "127.0.0.1:$WGQPORT" --key "$WGKEY" --quic-idle-ms 15000
8873 D67PID=$! 8584 D67PID=$DPID
8874 wait_sock "$SOCK67" "$OUT.wg.d" "web-restore daemon never bound"
8875 8585
8876 # Written by hand for the hydrate leg's reason: a line the hub had earned 8586 # Written by hand for the hydrate leg's reason: a line the hub had earned
8877 # by attaching would name a session that already exists, and there would be 8587 # by attaching would name a session that already exists, and there would be
@@ -8892,6 +8602,7 @@ done
8892 XDG_STATE_HOME="$WGSTATE" "$MUXWEB" --port "$WGPORT" --key "$WGKEY" \ 8602 XDG_STATE_HOME="$WGSTATE" "$MUXWEB" --port "$WGPORT" --key "$WGKEY" \
8893 --quic-idle-ms 15000 > "$OUT.wgh" 2>&1 & 8603 --quic-idle-ms 15000 > "$OUT.wgh" 2>&1 &
8894 W5PID=$! 8604 W5PID=$!
8605 defer_kill "$W5PID"
8895 wait_for "$OUT.wgh" "serving" 10 || { 8606 wait_for "$OUT.wgh" "serving" 10 || {
8896 echo "e2e FAIL: web-restore: hub never reported serving"; cat "$OUT.wgh"; exit 1; } 8607 echo "e2e FAIL: web-restore: hub never reported serving"; cat "$OUT.wgh"; exit 1; }
8897 8608
@@ -9056,9 +8767,8 @@ ok "the browser wall restores a saved local line too, and still joins a remote o
9056 # its own ssh, and the shim logs its pid before exec, so `wc -l` is a 8767 # its own ssh, and the shim logs its pid before exec, so `wc -l` is a
9057 # count of PROCESSES the OS made. A hub counter would be the code under 8768 # count of PROCESSES the OS made. A hub counter would be the code under
9058 # test grading its own homework. 8769 # test grading its own homework.
9059 "$MUXD" run --sock "$SOCK68" --shell /bin/sh > "$OUT.sp.d" 2>&1 & 8770 start_daemon "$SOCK68" "$OUT.sp.d" "refusal-spin daemon never bound" --shell /bin/sh
9060 D68PID=$! 8771 D68PID=$DPID
9061 wait_sock "$SOCK68" "$OUT.sp.d" "refusal-spin daemon never bound"
9062 8772
9063 # This shim ignores the remote command on purpose — it is not modelling 8773 # This shim ignores the remote command on purpose — it is not modelling
9064 # ssh (the M14 shim above does that), it is the dial's process signature 8774 # ssh (the M14 shim above does that), it is the dial's process signature
@@ -9084,6 +8794,7 @@ printf 'mux-spin@127.0.0.1#spinghost\n' > "$SPSTATE/mux/wall"
9084 XDG_STATE_HOME="$SPSTATE" XDG_CACHE_HOME="$SPSTATE/cache" PATH="$SPDIR:$PATH" \ 8794 XDG_STATE_HOME="$SPSTATE" XDG_CACHE_HOME="$SPSTATE/cache" PATH="$SPDIR:$PATH" \
9085 "$MUXWEB" --port "$SPPORT" > "$OUT.sph" 2>&1 & 8795 "$MUXWEB" --port "$SPPORT" > "$OUT.sph" 2>&1 &
9086 W6PID=$! 8796 W6PID=$!
8797 defer_kill "$W6PID"
9087 wait_for "$OUT.sph" "serving" 10 || { 8798 wait_for "$OUT.sph" "serving" 10 || {
9088 echo "e2e FAIL: refusal-spin: hub never reported serving"; cat "$OUT.sph"; exit 1; } 8799 echo "e2e FAIL: refusal-spin: hub never reported serving"; cat "$OUT.sph"; exit 1; }
9089 8800
@@ -9163,6 +8874,7 @@ mkdir -p "$UPHOME"
9163 MUX_SHELL_INTEGRATION=1 HOME="$UPHOME" "$MUXD" run --sock "$SOCK69" \ 8874 MUX_SHELL_INTEGRATION=1 HOME="$UPHOME" "$MUXD" run --sock "$SOCK69" \
9164 --shell /bin/bash > "$OUT.up.d" 2>&1 & 8875 --shell /bin/bash > "$OUT.up.d" 2>&1 &
9165 D69PID=$! 8876 D69PID=$!
8877 defer_kill "$D69PID"
9166 wait_sock "$SOCK69" "$OUT.up.d" "upgrade daemon never bound" 8878 wait_sock "$SOCK69" "$OUT.up.d" "upgrade daemon never bound"
9167 8879
9168 # Marks BEFORE the exec, so "marks after" is a comparison rather than a 8880 # Marks BEFORE the exec, so "marks after" is a comparison rather than a
@@ -9210,6 +8922,7 @@ send \x1cd
9210 waitexit 15000 8922 waitexit 15000
9211 EOF 8923 EOF
9212 UPPCPID=$! 8924 UPPCPID=$!
8925 defer_kill "$UPPCPID"
9213 8926
9214 # Daemon truth for the pid, and the number this leg is built around. The 8927 # Daemon truth for the pid, and the number this leg is built around. The
9215 # echoed command line spells `$$`, so only the shell's own answer has 8928 # echoed command line spells `$$`, so only the shell's own answer has
@@ -9254,6 +8967,7 @@ chmod 755 "$UPBIN"
9254 kill -STOP "$D69PID" 8967 kill -STOP "$D69PID"
9255 "$UPBIN" upgrade --sock "$SOCK69" --allow-same-version > "$OUT.upref2" 2>&1 & 8968 "$UPBIN" upgrade --sock "$SOCK69" --allow-same-version > "$OUT.upref2" 2>&1 &
9256 UPXPID=$! 8969 UPXPID=$!
8970 defer_kill "$UPXPID"
9257 _i=0 8971 _i=0
9258 while [ "$(readlink "/proc/$UPXPID/exe" 2>/dev/null)" != "$UPBIN" ]; do 8972 while [ "$(readlink "/proc/$UPXPID/exe" 2>/dev/null)" != "$UPBIN" ]; do
9259 _i=$((_i + 1)) 8973 _i=$((_i + 1))
@@ -9439,9 +9153,8 @@ ok "muxd upgrade keeps the shell, its pid, its title, its marks and its counters
9439 # assertion below would pass just as well on an upgrade that simply worked. 9153 # assertion below would pass just as well on an upgrade that simply worked.
9440 mkdir -p "$UPHOME" 9154 mkdir -p "$UPHOME"
9441 MUX_SHELL_INTEGRATION=1 MUX_RESUME_FAIL_AT=session HOME="$UPHOME" \ 9155 MUX_SHELL_INTEGRATION=1 MUX_RESUME_FAIL_AT=session HOME="$UPHOME" \
9442 "$MUXD" run --sock "$SOCK70" --shell /bin/bash > "$OUT.url.d" 2>&1 & 9156 start_daemon "$SOCK70" "$OUT.url.d" "rollback daemon never bound" --shell /bin/bash
9443 D70PID=$! 9157 D70PID=$DPID
9444 wait_sock "$SOCK70" "$OUT.url.d" "rollback daemon never bound"
9445 9158
9446 timeout 20 "$MUXA" run --sock "$SOCK70" --timeout 8000 'echo rollpid=$$' > "$OUT.urlm1" 2>&1 || { 9159 timeout 20 "$MUXA" run --sock "$SOCK70" --timeout 8000 'echo rollpid=$$' > "$OUT.urlm1" 2>&1 || {
9447 echo "e2e FAIL: rollback: muxa run failed before the upgrade:" 9160 echo "e2e FAIL: rollback: muxa run failed before the upgrade:"
@@ -9497,12 +9210,12 @@ ok "a candidate that cannot adopt execs the old binary back, session and marks i
9497 # Its own agent rather than the forwarding legs' AGENT48: that one is left 9210 # Its own agent rather than the forwarding legs' AGENT48: that one is left
9498 # under SIGSTOP by the mute-offerer leg, and a mute agent is exactly what 9211 # under SIGSTOP by the mute-offerer leg, and a mute agent is exactly what
9499 # this leg cannot tell from a broken listener. 9212 # this leg cannot tell from a broken listener.
9500 "$MUXD" run --sock "$SOCK71" --shell /bin/sh > "$OUT.uag.d" 2>&1 & 9213 start_daemon "$SOCK71" "$OUT.uag.d" "agent-upgrade daemon never bound" --shell /bin/sh
9501 D71PID=$! 9214 D71PID=$DPID
9502 wait_sock "$SOCK71" "$OUT.uag.d" "agent-upgrade daemon never bound"
9503 9215
9504 ssh-agent -a "$UPAGENT" > "$OUT.uagenv" 2>&1 9216 ssh-agent -a "$UPAGENT" > "$OUT.uagenv" 2>&1
9505 UPAGPID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.uagenv") 9217 UPAGPID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.uagenv")
9218 defer_kill "$UPAGPID"
9506 [ -n "$UPAGPID" ] || { 9219 [ -n "$UPAGPID" ] || {
9507 echo "e2e FAIL: agent-upgrade: ssh-agent printed no pid for the trap to hold it by:" 9220 echo "e2e FAIL: agent-upgrade: ssh-agent printed no pid for the trap to hold it by:"
9508 cat "$OUT.uagenv"; exit 1; } 9221 cat "$OUT.uagenv"; exit 1; }
@@ -9581,10 +9294,9 @@ ok "the session's agent socket crosses the exec: ssh-add -l answers again"
9581 # here: back in service in a fraction of `--quic-idle-ms`, not after it. 9294 # here: back in service in a fraction of `--quic-idle-ms`, not after it.
9582 head -c 32 /dev/urandom > "$UPKEY" 9295 head -c 32 /dev/urandom > "$UPKEY"
9583 chmod 600 "$UPKEY" 9296 chmod 600 "$UPKEY"
9584 "$MUXD" run --sock "$SOCK72" --shell /bin/sh \ 9297 start_daemon "$SOCK72" "$OUT.uqc.d" "quic-upgrade daemon never bound" --shell /bin/sh \
9585 --quic "127.0.0.1:$UPQPORT" --key "$UPKEY" --quic-idle-ms 15000 > "$OUT.uqc.d" 2>&1 & 9298 --quic "127.0.0.1:$UPQPORT" --key "$UPKEY" --quic-idle-ms 15000
9586 D72PID=$! 9299 D72PID=$DPID
9587 wait_sock "$SOCK72" "$OUT.uqc.d" "quic-upgrade daemon never bound"
9588 9300
9589 pipe_mux "$OUT.uqc" "$OUT.uqc.err" timeout 90 "$MUX" "quic://127.0.0.1:$UPQPORT" \ 9301 pipe_mux "$OUT.uqc" "$OUT.uqc.err" timeout 90 "$MUX" "quic://127.0.0.1:$UPQPORT" \
9590 --key "$UPKEY" --quic-idle-ms 15000 9302 --key "$UPKEY" --quic-idle-ms 15000
@@ -9646,9 +9358,8 @@ ok "a QUIC client is served again within a breath of the exec, not after the idl
9646 # sessions make it a table's grace, not one shell's. 9358 # sessions make it a table's grace, not one shell's.
9647 printf '#!/bin/sh\ntrap "" HUP TERM\nwhile :; do sleep 1; done\n' > "$OUT.sg.sh" 9359 printf '#!/bin/sh\ntrap "" HUP TERM\nwhile :; do sleep 1; done\n' > "$OUT.sg.sh"
9648 chmod +x "$OUT.sg.sh" 9360 chmod +x "$OUT.sg.sh"
9649 "$MUXD" run --sock "$SOCK73" --shell "$OUT.sg.sh" > "$OUT.sg.d" 2>&1 & 9361 start_daemon "$SOCK73" "$OUT.sg.d" "stop-gone daemon never bound" --shell "$OUT.sg.sh"
9650 D73PID=$! 9362 D73PID=$DPID
9651 wait_sock "$SOCK73" "$OUT.sg.d" "stop-gone daemon never bound"
9652 pipe_mux "$OUT.sg" "" timeout 60 "$MUX" --sock "$SOCK73" --session second 9363 pipe_mux "$OUT.sg" "" timeout 60 "$MUX" --sock "$SOCK73" --session second
9653 sleep 0.5 9364 sleep 0.5
9654 pipe_detach "stop-gone: the second session's client" 9365 pipe_detach "stop-gone: the second session's client"