summaryrefslogtreecommitdiff
path: root/9ns/test
diff options
context:
space:
mode:
Diffstat (limited to '9ns/test')
-rwxr-xr-x9ns/test/adv_bridge_hostile.py11
-rwxr-xr-x9ns/test/adv_bridge_hostile.sh6
-rwxr-xr-x9ns/test/adv_bridge_interrupt.sh136
-rwxr-xr-x9ns/test/adv_bridge_semantics.sh7
-rwxr-xr-x9ns/test/adv_bridge_stress.sh2
-rwxr-xr-x9ns/test/adversarial.sh2
-rwxr-xr-x9ns/test/integration.sh27
7 files changed, 180 insertions, 11 deletions
diff --git a/9ns/test/adv_bridge_hostile.py b/9ns/test/adv_bridge_hostile.py
index d353541..f676d2f 100755
--- a/9ns/test/adv_bridge_hostile.py
+++ b/9ns/test/adv_bridge_hostile.py
@@ -62,7 +62,8 @@ name_huge / has an entry with a 60000-byte name
name_dots / lists "." and ".." too
rerror_big Twalk to nope: Rerror with 65535 bytes of text
extra_reply Rread on /f: an unsolicited Rclunk (tag 9) precedes the real reply
-never Tread on /f: never reply (hang)
+never Tread on /f: never reply (hang); the server stops reading, so a Tflush is never seen
+never_flush Tread on /f: never reply, but keep serving: log every Tflush and answer Rflush
close_mid Tread on /f: close the socket without replying
renegotiate Tread on /f: an unsolicited Rversion precedes the real reply
length_max Tstat on /f: length = 2**64-1
@@ -142,6 +143,9 @@ class Server:
parent.children[name] = n
return n
+ def log(self, text):
+ print(text, flush=True)
+
# -- framing ---------------------------------------------------------
def frame(self, typ, tag, body):
return s32(7 + len(body)) + s8(typ) + s16(tag) + body
@@ -196,6 +200,8 @@ class Server:
self.fids[fid] = [self.root, False]
return self.frame(Rattach, tag, self.root.qid(self))
if typ == Tflush:
+ oldtag = r.u16()
+ self.log('Tflush tag=%d oldtag=%d' % (tag, oldtag))
return self.frame(Rflush, tag, b'')
if typ == Twalk:
fid, newfid, nw = r.u32(), r.u32(), r.u16()
@@ -292,6 +298,9 @@ class Server:
if m == 'never':
time.sleep(3600)
return None
+ if m == 'never_flush':
+ self.log('Tread tag=%d fid=%d hang' % (tag, fid))
+ return b''
if m == 'close_mid':
return None
if m == 'renegotiate':
diff --git a/9ns/test/adv_bridge_hostile.sh b/9ns/test/adv_bridge_hostile.sh
index f5ba871..20c05bf 100755
--- a/9ns/test/adv_bridge_hostile.sh
+++ b/9ns/test/adv_bridge_hostile.sh
@@ -37,7 +37,7 @@ start_server() { # mode
run() {
local mode=$1 script=$2; shift 2
start_server "$mode"
- OUT=$(timeout 30 "$NS" --unix "$SOCK" "$@" -- sh -c "$script" 2>"$TMP/stderr")
+ OUT=$(timeout 30 "$NS" --unix "$SOCK" --mount "$M" "$@" -- sh -c "$script" 2>"$TMP/stderr")
RC=$?
STDERR=$(cat "$TMP/stderr")
}
@@ -146,7 +146,7 @@ echo "# a server that never replies"
start_server never
# SIGTERM is forwarded to the child; once the child is gone 9ns must leave the
# pending 9P reply behind and exit even though the server stays silent.
-timeout -s TERM 3 "$NS" --unix "$SOCK" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" &
+timeout -s TERM 3 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" &
TPID=$!
sleep 4
if kill -0 "$TPID" 2>/dev/null; then
@@ -156,7 +156,7 @@ else
fi
wait "$TPID" 2>/dev/null
# Without a signal the mount hangs (documented v1 limitation) until the server dies.
-timeout 30 "$NS" --unix "$SOCK" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" &
+timeout 30 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" &
TPID=$!
sleep 1.5
if kill -0 "$TPID" 2>/dev/null; then
diff --git a/9ns/test/adv_bridge_interrupt.sh b/9ns/test/adv_bridge_interrupt.sh
new file mode 100755
index 0000000..e364143
--- /dev/null
+++ b/9ns/test/adv_bridge_interrupt.sh
@@ -0,0 +1,136 @@
+#!/usr/bin/env bash
+# Interrupt tests: a reader blocked in a 9P read the server never answers must
+# be releasable. Killing or Ctrl-C-ing it makes the kernel send FUSE_INTERRUPT,
+# which the bridge turns into a Tflush; a server that answers Rflush (the
+# hostile server's `never_flush` mode) unblocks the reader with EINTR and the
+# mount stays usable; a server that ignores everything (`never`) still blocks
+# the mount, but SIGTERM to 9ns ends the session as before.
+# Usage: bash 9ns/test/adv_bridge_interrupt.sh <9ns> <9proc-demo> (9proc unused; part of zig build 9ns-adv)
+set -u
+NS=$(realpath "${1:?path to 9ns}")
+HERE=$(cd "$(dirname "$0")" && pwd)
+SRV=$HERE/adv_bridge_hostile.py
+TMP=$(mktemp -d "${TMPDIR:-/tmp}/9ns-int.XXXXXX")
+M=/mnt/9p
+FAILED=0
+PASSED=0
+SRVPID=
+
+cleanup() { [ -n "$SRVPID" ] && kill "$SRVPID" 2>/dev/null; pkill -f "adv_bridge_hostile.py $TMP" 2>/dev/null; rm -rf "$TMP"; }
+trap cleanup EXIT
+
+if ! unshare -Urm true 2>/dev/null || [ ! -c /dev/fuse ]; then echo "SKIP: no user namespaces or /dev/fuse"; exit 0; fi
+
+pass() { PASSED=$((PASSED + 1)); echo "ok - $1"; }
+fail() { FAILED=$((FAILED + 1)); echo "FAIL - $1"; shift; [ $# -gt 0 ] && printf ' %s\n' "$@"; }
+expect_eq() { if [ "$2" = "$3" ]; then pass "$1"; else fail "$1" "expected: $(printf %q "$2")" "actual: $(printf %q "$3")"; fi; }
+expect_contains() { case "$3" in *"$2"*) pass "$1" ;; *) fail "$1" "missing: $(printf %q "$2")" "in: $(printf %q "$3")" ;; esac; }
+# expect_lt NAME ACTUAL LIMIT
+expect_lt() { if [ "$2" -lt "$3" ]; then pass "$1 ($2 < $3)"; else fail "$1" "expected < $3, got $2"; fi; }
+
+start_server() { # mode
+ [ -n "$SRVPID" ] && { kill "$SRVPID" 2>/dev/null; wait "$SRVPID" 2>/dev/null; }
+ SOCK=$TMP/$1.sock
+ SRVLOG=$TMP/$1.srv.out
+ rm -f "$SOCK"
+ python3 "$SRV" "$SOCK" "$1" >"$SRVLOG" 2>&1 </dev/null &
+ SRVPID=$!
+ for _ in $(seq 1 100); do [ -S "$SOCK" ] && return 0; sleep 0.02; done
+ echo "server for $1 did not start"; cat "$SRVLOG"; exit 1
+}
+
+# run MODE SCRIPT [extra 9ns args...]: starts the server, runs 9ns; sets OUT, RC, STDERR.
+run() {
+ local mode=$1 script=$2; shift 2
+ start_server "$mode"
+ OUT=$(timeout 60 "$NS" --unix "$SOCK" --mount "$M" "$@" -- sh -c "$script" 2>"$TMP/stderr")
+ RC=$?
+ STDERR=$(cat "$TMP/stderr")
+}
+
+no_crash() { # name
+ if [ "$RC" -ge 128 ] || [ "$RC" -eq 124 ]; then fail "$1: 9ns exit $RC" "$STDERR"; return; fi
+ case "$STDERR" in *panic*|*"Segmentation"*|*"integer overflow"*|*"reached unreachable"*|*"index out of bounds"*) fail "$1: crash text in stderr" "$STDERR";; *) pass "$1: no crash (exit $RC)";; esac
+}
+
+# The shell snippet that times a command: prints "rc=N" and "ms=N".
+timed() { # command...
+ printf 's=$(date +%%s%%N); %s; echo rc=$?; e=$(date +%%s%%N); echo ms=$(( (e - s) / 1000000 ))' "$*"
+}
+field() { printf '%s\n' "$2" | sed -n "s/^$1=//p" | tail -1; }
+# Server-side fid count before and after the interrupted operation, measured in
+# the same run. Everything the scripts touch is looked up first, so the inode
+# fids the kernel keeps for f, d and g are in both samples and the comparison
+# is exact: any difference is a fid an interrupted operation left behind.
+BEFORE="stat $M/f $M/d/g >/dev/null; ls $M >/dev/null; echo before=\$(cat $M/fids)"
+AFTER="sleep 0.2; echo after=\$(cat $M/fids)"
+fids_same() { # name
+ local b a; b=$(field before "$OUT"); a=$(field after "$OUT")
+ case "$b" in ''|*[!0-9]*) fail "$1: fid count before is not numeric: '$b'"; return;; esac
+ expect_eq "$1: server-side fid count unchanged ($b)" "$b" "$a"
+}
+# Same for the bridge's own counter (--debug prints fids=N per request): the
+# first and the last READ traced are the two `cat fids`.
+bridge_fids_same() { # name
+ local reads; reads=$(printf '%s\n' "$STDERR" | sed -n 's/^9ns: <- read .*(fids=\([0-9]*\) .*/\1/p')
+ expect_eq "$1: bridge fid counter unchanged ($(printf '%s\n' "$reads" | head -1))" "$(printf '%s\n' "$reads" | head -1)" "$(printf '%s\n' "$reads" | tail -1)"
+}
+
+echo "# (a) SIGINT to a reader blocked in a read the server never answers"
+run never_flush "$BEFORE; $(timed "timeout -s INT 2 cat $M/f"); ls $M | tr '\n' ' '; echo; cat $M/d/g; $AFTER" --debug
+no_crash "never_flush/SIGINT"
+expect_eq "never_flush/SIGINT: cat was killed by the signal (timeout reports 124)" "124" "$(field rc "$OUT")"
+expect_lt "never_flush/SIGINT: the reader was released promptly" "$(field ms "$OUT")" 2500
+expect_contains "never_flush/SIGINT: the mount is still usable (ls)" "big d f fids" "$OUT"
+expect_contains "never_flush/SIGINT: the mount is still usable (read another file)" "in d" "$OUT"
+fids_same "never_flush/SIGINT"
+hung_tag=$(sed -n 's/^Tread tag=\([0-9]*\) .*hang$/\1/p' "$SRVLOG" | head -1)
+flush_oldtag=$(sed -n 's/^Tflush tag=[0-9]* oldtag=\([0-9]*\)$/\1/p' "$SRVLOG" | head -1)
+expect_eq "never_flush/SIGINT: exactly one Tflush reached the server" "1" "$(grep -c '^Tflush ' "$SRVLOG")"
+expect_eq "never_flush/SIGINT: Tflush.oldtag is the hung Tread's tag ($hung_tag)" "$hung_tag" "$flush_oldtag"
+expect_contains "never_flush/SIGINT: --debug shows the interrupt being forwarded" "sending Tflush" "$STDERR"
+expect_contains "never_flush/SIGINT: --debug shows EINTR going back to the kernel" "error EINTR" "$STDERR"
+bridge_fids_same "never_flush/SIGINT"
+
+echo "# (c) SIGKILL to the blocked reader"
+run never_flush "$BEFORE; $(timed "cat $M/f & p=\$!; sleep 0.5; kill -9 \$p; wait \$p"); cat $M/d/g; $AFTER"
+no_crash "never_flush/SIGKILL"
+expect_eq "never_flush/SIGKILL: reader died of SIGKILL (137)" "137" "$(field rc "$OUT")"
+expect_lt "never_flush/SIGKILL: released promptly" "$(field ms "$OUT")" 2000
+expect_contains "never_flush/SIGKILL: mount still usable" "in d" "$OUT"
+fids_same "never_flush/SIGKILL"
+expect_eq "never_flush/SIGKILL: one Tflush" "1" "$(grep -c '^Tflush ' "$SRVLOG")"
+
+echo "# a second blocked read after the first was interrupted"
+run never_flush "$BEFORE; $(timed "timeout -s INT 1 cat $M/f"); $(timed "timeout -s INT 1 cat $M/f"); cat $M/d/g; $AFTER"
+no_crash "never_flush/twice"
+expect_eq "never_flush/twice: both readers killed" "124 124" "$(printf '%s\n' "$OUT" | sed -n 's/^rc=//p' | tr '\n' ' ' | sed 's/ $//')"
+expect_eq "never_flush/twice: two Tflush, no tag confusion" "2" "$(grep -c '^Tflush ' "$SRVLOG")"
+expect_contains "never_flush/twice: mount still usable" "in d" "$OUT"
+fids_same "never_flush/twice"
+
+echo "# an interrupted open+read through a lookup (walk+stat) leaves no fid behind"
+# `f` is looked up fresh each time (cache 0), so the LOOKUP's walk+stat and the
+# OPEN's clone+open all run before the read blocks; all their fids must go.
+run never_flush "$BEFORE; $(timed "timeout -s INT 1 cat $M/f"); $AFTER" --cache 0 --debug
+no_crash "never_flush/cache0"
+expect_eq "never_flush/cache0: reader killed" "124" "$(field rc "$OUT")"
+fids_same "never_flush/cache0"
+bridge_fids_same "never_flush/cache0"
+
+echo "# (b) a server that ignores Tflush too: the reader stays blocked, SIGTERM to 9ns still ends the session"
+start_server never
+timeout -s TERM 3 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "timeout -s INT 1 cat $M/f; echo unreachable-rc=\$?" >"$TMP/never.out" 2>"$TMP/never.err" &
+TPID=$!
+sleep 4
+if kill -0 "$TPID" 2>/dev/null; then
+ fail "never: SIGTERM did not end 9ns while the reader was stuck"; kill -9 "$TPID"
+else
+ pass "never: SIGTERM ends 9ns even though the server ignores the Tflush"
+fi
+wait "$TPID" 2>/dev/null
+expect_eq "never: the reader never came back (server ignores Tflush)" "" "$(grep unreachable "$TMP/never.out")"
+
+echo
+echo "passed=$PASSED failed=$FAILED"
+[ "$FAILED" -eq 0 ]
diff --git a/9ns/test/adv_bridge_semantics.sh b/9ns/test/adv_bridge_semantics.sh
index b38ce9c..526ebc8 100755
--- a/9ns/test/adv_bridge_semantics.sh
+++ b/9ns/test/adv_bridge_semantics.sh
@@ -24,7 +24,7 @@ SOCK=$TMP/i.sock
SRVPID=$!
for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; sleep 0.02; done
# Each run is a fresh session; state persists in the server, so tests clean up after themselves.
-run() { OUT=$(timeout 120 "$NS" --unix "$SOCK" "${EXTRA[@]}" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); }
+run() { OUT=$(timeout 120 "$NS" --unix "$SOCK" --mount "$M" "${EXTRA[@]}" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); }
EXTRA=()
py() { run "python3 - <<'PYEOF'
$1
@@ -141,8 +141,9 @@ for n in os.listdir(d): os.unlink(f'{d}/{n}')
os.rmdir(d)
"
expect_eq "readdir of a directory that changes mid-iteration" "a True True" "$OUT"
-run "ls $M/.. > /dev/null && echo ok; stat -c %i $M $M/. $M/scratch/..; cd $M/scratch && ls .. | grep -c scratch"
-expect_eq ".. of the root and of a subdir" $'ok\n1\n1\n1\n1' "$OUT"
+# The root's inode number is its 9P qid.path (not a fixed 1): all three views must agree.
+run "ls $M/.. > /dev/null && echo ok; stat -c %i $M $M/. $M/scratch/.. | sort -u | wc -l; cd $M/scratch && ls .. | grep -c scratch"
+expect_eq ".. of the root and of a subdir" $'ok\n1\n1' "$OUT"
run "cd $M && find . -type d | wc -l && find . -type f | head -1 && find $S -type f | wc -l"
expect_contains "find -type works" "./README" "$OUT"
run "stat -f -c '%T %S %l' $M; df -P $M | tail -1 | awk '{print \$1}'; sync -f $M && echo synced; sync && echo synced2"
diff --git a/9ns/test/adv_bridge_stress.sh b/9ns/test/adv_bridge_stress.sh
index b306b28..d49f453 100755
--- a/9ns/test/adv_bridge_stress.sh
+++ b/9ns/test/adv_bridge_stress.sh
@@ -21,7 +21,7 @@ SOCK=$TMP/i.sock
"$PROC" --unix "$SOCK" >"$TMP/srv.out" 2>&1 &
SRVPID=$!
for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; sleep 0.02; done
-run() { OUT=$(timeout 600 "$NS" --unix "$SOCK" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); }
+run() { OUT=$(timeout 600 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); }
echo "# 100k+ 9P operations in one session; RSS must plateau"
# Each iteration: create+write+close, open+read+close, unlink, plus a failing lookup: ~15 RPCs.
diff --git a/9ns/test/adversarial.sh b/9ns/test/adversarial.sh
index 71c6f44..8575dcf 100755
--- a/9ns/test/adversarial.sh
+++ b/9ns/test/adversarial.sh
@@ -8,7 +8,7 @@ NS=${1:?path to 9ns}
PROC=${2:?path to 9proc-demo}
HERE=$(cd "$(dirname "$0")" && pwd)
status=0
-for suite in adv_ns_process adv_bridge_hostile adv_bridge_semantics adv_bridge_stress; do
+for suite in adv_ns_process adv_bridge_hostile adv_bridge_interrupt adv_bridge_semantics adv_bridge_stress; do
echo "### $suite"
if bash "$HERE/$suite.sh" "$NS" "$PROC"; then echo "### $suite: ok"; else echo "### $suite: FAILED"; status=1; fi
done
diff --git a/9ns/test/integration.sh b/9ns/test/integration.sh
index 5e0ad13..32d132a 100755
--- a/9ns/test/integration.sh
+++ b/9ns/test/integration.sh
@@ -10,6 +10,8 @@ TMP=$(mktemp -d "${TMPDIR:-/tmp}/9ns-itest.XXXXXX")
PIDS=()
FAILED=0
PASSED=0
+# The suites pin the mountpoint with --mount; the default is /mnt/9p/<name>
+# (see the "# mount names" section below).
M=/mnt/9p
cleanup() {
@@ -39,8 +41,8 @@ wait_socket() { # path
return 1
}
-# run_in "<shell script>" — run inside a namespace with the current transport ($TRANSPORT array).
-run_in() { timeout 60 "$NS" "${TRANSPORT[@]}" -- sh -c "$1" 2>"$TMP/stderr"; }
+# run_in "<shell script>" — run inside a namespace with the current transport ($TRANSPORT array), mounted on $M.
+run_in() { timeout 60 "$NS" "${TRANSPORT[@]}" --mount "$M" -- sh -c "$1" 2>"$TMP/stderr"; }
# --- scratch battery: works against any writable 9P tree rooted at $1 (relative to mount) ---
scratch_battery() { # label scratchdir
@@ -111,6 +113,26 @@ mkdir -p "$TMP/mnt"
expect_eq "--mount existing dir" "ok" "$(timeout 60 "$NS" --unix "$SOCK" --mount "$TMP/mnt" -- sh -c "[ -f $TMP/mnt/README ] && echo ok")"
expect_eq "--mount relative" "ok" "$(cd "$TMP" && timeout 60 "$NS" --unix "$SOCK" --mount rel -- sh -c "[ -f $TMP/rel/README ] && echo ok")"
expect_eq "--mount missing under /" "125" "$(timeout 60 "$NS" --unix "$SOCK" --mount /nonexistent-9ns-dir -- true 2>/dev/null; echo $?)"
+expect_eq "--mount beats --name" "$TMP/mnt" "$(timeout 60 "$NS" --unix "$SOCK" --name zz --mount "$TMP/mnt" -- sh -c 'echo $NINE_MOUNT')"
+
+echo "# mount names (default mountpoint /mnt/9p/<name>)"
+mkdir -p "$TMP/n"
+"$PROC" --unix "$TMP/n/foo.sock" &
+PIDS+=($!)
+wait_socket "$TMP/n/foo.sock" || echo "foo.sock missing"
+named() { timeout 60 "$NS" --unix "$TMP/n/foo.sock" "$@"; }
+expect_eq "unix socket foo.sock -> /mnt/9p/foo" "/mnt/9p/foo" "$(named -- sh -c 'echo $NINE_MOUNT')"
+expect_eq "default mount is served" "ok" "$(named -- sh -c '[ -f /mnt/9p/foo/README ] && grep -q "^9ns /mnt/9p/foo fuse" /proc/self/mounts && echo ok')"
+expect_eq "default mount keeps /mnt entries" "$(ls -A /mnt | sort | tr '\n' ' ')" "$(named -- sh -c "ls -A /mnt | grep -v '^9p\$' | sort | tr '\n' ' '")"
+expect_eq "--name bar -> /mnt/9p/bar" "/mnt/9p/bar ok" "$(named --name bar -- sh -c 'echo $NINE_MOUNT; [ -f /mnt/9p/bar/README ] && echo ok' | tr '\n' ' ' | sed 's/ $//')"
+expect_eq "--name=bar form" "/mnt/9p/bar" "$(named --name=bar -- sh -c 'echo $NINE_MOUNT')"
+expect_eq "--name a/b is a usage error" "125" "$(named --name a/b -- true 2>/dev/null; echo $?)"
+expect_eq "--name . is a usage error" "125" "$(named --name . -- true 2>/dev/null; echo $?)"
+expect_eq "--name '' is a usage error" "125" "$(named --name '' -- true 2>/dev/null; echo $?)"
+expect_eq "nested, two names: both under /mnt/9p" "a b both /mnt/9p/b" "$(named --name a -- "$NS" --unix "$TMP/n/foo.sock" --name b -- sh -c 'ls /mnt/9p | tr "\n" " "; [ -f /mnt/9p/a/README ] && [ -f /mnt/9p/b/README ] && echo both; echo $NINE_MOUNT' 2>/dev/null | tr '\n' ' ' | sed 's/ $//')"
+expect_eq "nested, same name: inner shadows outer" "2 /mnt/9p/a" "$(named --name a -- "$NS" --unix "$TMP/n/foo.sock" --name a -- sh -c 'grep -c "^9ns /mnt/9p/a fuse" /proc/self/mounts; [ -f /mnt/9p/a/README ] && echo $NINE_MOUNT' 2>/dev/null | tr '\n' ' ' | sed 's/ $//')"
+expect_eq "--spawn default name is the command basename" "/mnt/9p/9proc-demo" "$(timeout 60 "$NS" --spawn "$PROC --stdio" -- sh -c 'echo $NINE_MOUNT')"
+expect_eq "host mount table untouched by named mounts" "no" "$(grep -q " /mnt/9p" /proc/self/mountinfo && echo yes || echo no)"
echo "# lifecycle"
START=$(date +%s)
@@ -131,6 +153,7 @@ PIDS+=($!)
sleep 0.3
TRANSPORT=(--tcp "127.0.0.1:$PORT")
expect_eq "tcp: zig_version" "$(zig version)" "$(run_in "cat $M/build/zig_version")"
+expect_eq "tcp: default name" "/mnt/9p/tcp-127.0.0.1-$PORT" "$(timeout 60 "$NS" --tcp "127.0.0.1:$PORT" -- sh -c 'echo $NINE_MOUNT')"
expect_eq "tcp: scratch" "tcp" "$(run_in "echo tcp > $M/scratch/t && cat $M/scratch/t && rm $M/scratch/t")"
echo "# server death"