writeonce/scripts/subprocess-accept.sh
shoney.arickathil c91027bcc6 feat(lang42): subprocess example + gate
- docs/examples/subprocess: line-oriented TCP service, one Handler actor
  per request — ping/run/slow/deadline/cap/long exercise the whole
  bounded surface from .wo, traps caught with try/catch in the language
- scripts/subprocess-accept.sh + `just subprocess`: 12 checks, 0 failures
  first run — deadline and cap messages verbatim, ping answered in 2 ms
  while a sleep-2 child was parked, SIGTERM exit 0 with the sleep-30
  child verifiably gone (pid checked from outside)
- service logs to /tmp/subprocess.log, banner-separated per run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 22:19:25 +02:00

127 lines
4.3 KiB
Bash
Executable file

#!/usr/bin/env bash
# scripts/subprocess-accept.sh — iteration 42's gate: bounded subprocess.
# The runtime suite (test_proc) proves the mechanics in-process; this
# proves the half only a real program shows: proc.run called FROM .wo
# through the compiler, bounds caught with try/catch in the language, a
# parked run blocking no other request, and the SIGTERM drain leaving no
# child behind. Service log: /tmp/subprocess.log (tail -F it live).
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WOC="$ROOT/compiler/_build/default/bin/woc"
WOVM="$ROOT/runtime/wovm"
APP="$ROOT/docs/examples/subprocess/main.wo"
PORT="${SUBPROCESS_PORT:-18971}"
LOG=/tmp/subprocess.log
pass=0; fail=0
ok() { echo "ok $1"; pass=$((pass + 1)); }
bad() { echo "FAIL $1"; fail=$((fail + 1)); }
WORK="$(mktemp -d)"
APP_PID=""
cleanup() {
[[ -n "$APP_PID" ]] && kill -KILL "$APP_PID" 2>/dev/null
rm -rf "$WORK"
}
trap cleanup EXIT
now_ms() { echo $(( $(date +%s%N) / 1000000 )); }
# one request, one line back (the service closes after answering)
req() {
local line=""
if exec 3<>"/dev/tcp/127.0.0.1/$PORT" 2>/dev/null; then
printf '%s\n' "$1" >&3
IFS= read -r -t "${2:-8}" line <&3 || true
exec 3>&- 2>/dev/null
fi
echo "$line"
}
# ---- 0. build ------------------------------------------------------------
if "$WOC" --emit "$APP" -o "$WORK/subprocess.wob" 2>"$WORK/cerr"; then
ok "build: main.wo compiles"
else
bad "build: $(head -3 "$WORK/cerr")"
echo "subprocess-accept: $((pass + fail)) checks, $fail failures"
exit 1
fi
# ---- 1. start, banner-separated log --------------------------------------
{ echo; echo "== subprocess-accept $(date -Is) port $PORT =="; } >>"$LOG"
"$WOVM" "$WORK/subprocess.wob" "$PORT" >>"$LOG" 2>&1 &
APP_PID=$!
up=0
for _ in $(seq 1 50); do
if (exec 3<>"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then exec 3>&-; up=1; break; fi
sleep 0.1
done
[[ $up == 1 ]] && ok "service answers on $PORT (log: $LOG)" \
|| bad "service never came up (see $LOG)"
# ---- 2. the default run --------------------------------------------------
got="$(req run)"
[[ "$got" == "code=0 out=hello" ]] && ok "proc.run: code=0 out=hello" \
|| bad "proc.run answered '$got'"
# ---- 3. deadline caught IN the language ----------------------------------
got="$(req deadline)"
if [[ "$got" == caught:*deadline* ]]; then
ok "deadline trap caught in .wo: '$got'"
else
bad "deadline leg answered '$got'"
fi
# ---- 4. stdout cap caught IN the language --------------------------------
got="$(req cap)"
if [[ "$got" == caught:*"stdout cap 1000"* ]]; then
ok "cap trap caught in .wo: '$got'"
else
bad "cap leg answered '$got'"
fi
# ---- 5. a parked run blocks nobody ---------------------------------------
slow_out="$WORK/slow"
( req slow 10 >"$slow_out" ) &
SLOW_JOB=$!
sleep 0.4 # the slow child (sleep 2) is now in flight
t0=$(now_ms)
got="$(req ping)"
t1=$(now_ms)
[[ "$got" == "pong" ]] && ok "ping answered while slow runs" \
|| bad "ping answered '$got' while slow runs"
(( t1 - t0 < 1500 )) && ok "ping took $((t1 - t0)) ms (not slow's 2 s)" \
|| bad "ping took $((t1 - t0)) ms — the shard was blocked"
wait "$SLOW_JOB"
got="$(cat "$slow_out")"
[[ "$got" == "slow-done code=0" ]] && ok "slow completed: '$got'" \
|| bad "slow answered '$got'"
# ---- 6. SIGTERM drain: no child survives ---------------------------------
got="$(req long)"
[[ "$got" == "long-started" ]] && ok "long child started" \
|| bad "long answered '$got'"
CHILD=""
for _ in $(seq 1 30); do
CHILD="$(pgrep -P "$APP_PID" -x sleep | head -1 || true)"
[[ -n "$CHILD" ]] && break
sleep 0.1
done
[[ -n "$CHILD" ]] && ok "live child found (sleep 30, pid $CHILD)" \
|| bad "no sleep child appeared under the service"
kill -TERM "$APP_PID"
rc=-1
for _ in $(seq 1 50); do
if ! kill -0 "$APP_PID" 2>/dev/null; then wait "$APP_PID"; rc=$?; break; fi
sleep 0.1
done
[[ $rc == 0 ]] && ok "SIGTERM: clean exit 0" || bad "SIGTERM exit was $rc"
if [[ -n "$CHILD" ]]; then
kill -0 "$CHILD" 2>/dev/null && bad "child $CHILD SURVIVED the stop" \
|| ok "child $CHILD is gone with the service"
fi
APP_PID=""
echo "subprocess-accept: $((pass + fail)) checks, $fail failures"
[[ $fail == 0 ]]