#!/usr/bin/env bash # FED R7 / #1802. Scratch acceptance with labelled ancestry stand-in (sage-3625). # No systemd units. All children run in owned process groups, removed on exit. set -euo pipefail R7_INTERRUPT=0 case "${1:-}" in '') ;; --kill-after-start) R7_INTERRUPT=1 ;; *) echo 'usage: scripts/test-federation-acceptance.sh [--kill-after-start]' >&2; exit 2 ;; esac [ "$#" -le 1 ] || exit 2 for R7_TOOL in ss rg timeout node ps awk; do command -v "$R7_TOOL" >/dev/null || { echo "R7 requires $R7_TOOL" >&2; exit 1; } done cd "$(dirname "${BASH_SOURCE[0]}")/.." R7_ROOT="$(mktemp -d /mnt/storage/scratch/tmp/r7.XXXXXXXX)" mkdir -p "$R7_ROOT/home" "$R7_ROOT/tmp" touch "$R7_ROOT/groups" "$R7_ROOT/ports" "$R7_ROOT/sockets" cleanup() { local result=$? pid start raw port socket listeners cleanup_failed=0 attempt local -a fields trap - EXIT INT TERM if [ -n "${R7_DRIVER:-}" ]; then kill -TERM "$R7_DRIVER" 2>/dev/null || true wait "$R7_DRIVER" 2>/dev/null || true fi # A detached child is its group's leader. Confirm pid/start time/group before # signalling only groups in this run's ledger; never a live stack process. while read -r pid start; do [ -n "$pid" ] || continue if [ -r "/proc/$pid/stat" ]; then raw="$(cat "/proc/$pid/stat")" read -r -a fields <<< "${raw##*) }" if [ "${fields[19]}" != "$start" ] || [ "${fields[2]}" != "$pid" ]; then echo "R7 cleanup failed: group identity changed: $pid" >&2 result=1; cleanup_failed=1 continue fi kill -TERM -- "-$pid" 2>/dev/null || true for ((attempt=0; attempt<800; attempt++)); do ps -eo pgid=,stat= | awk -v g="$pid" '$1==g && $2 !~ /^Z/ {found=1} END {exit !found}' || break sleep 0.05 done kill -KILL -- "-$pid" 2>/dev/null || true for ((attempt=0; attempt<40; attempt++)); do ps -eo pgid=,stat= | awk -v g="$pid" '$1==g && $2 !~ /^Z/ {found=1} END {exit !found}' || break sleep 0.05 done fi # Zombies hold no tools/listeners and are reaped by their parent. The test # requires no live member of an owned group, including orphan descendants. if ps -eo pgid=,stat= | awk -v g="$pid" '$1==g && $2 !~ /^Z/ {found=1} END {exit !found}'; then echo "R7 cleanup failed: group $pid still has a live process" >&2 result=1; cleanup_failed=1 fi done < "$R7_ROOT/groups" while IFS= read -r port; do [ -n "$port" ] || continue if ! listeners="$(ss -H -ltn "sport = :$port")"; then echo "R7 cleanup failed: cannot inspect port $port" >&2 result=1; cleanup_failed=1 elif [ -n "$listeners" ]; then echo "R7 cleanup failed: port $port still listens" >&2 result=1; cleanup_failed=1 fi done < "$R7_ROOT/ports" while IFS= read -r socket; do [ -n "$socket" ] || continue if ! listeners="$(ss -H -lx)"; then echo "R7 cleanup failed: cannot inspect socket $socket" >&2 result=1; cleanup_failed=1 elif printf '%s\n' "$listeners" | rg -F -- "$socket" >/dev/null; then echo "R7 cleanup failed: socket $socket still listens" >&2 result=1; cleanup_failed=1 fi done < "$R7_ROOT/sockets" if listeners="$(ss -H -lx)"; then if printf '%s\n' "$listeners" | rg -F -- "$R7_ROOT/" >/dev/null; then echo 'R7 cleanup failed: another scratch Unix listener remains' >&2 result=1; cleanup_failed=1 fi else result=1; cleanup_failed=1 fi if [ "$result" -ne 0 ]; then for raw in "$R7_ROOT"/*.log; do [ -f "$raw" ] || continue echo "R7 diagnostic: ${raw##*/}" >&2 tail -40 "$raw" >&2 done fi rm -rf -- "$R7_ROOT" if [ -e "$R7_ROOT" ]; then echo 'R7 cleanup failed: scratch directory remains' >&2 result=1; cleanup_failed=1 fi if [ "$cleanup_failed" -eq 0 ]; then echo 'R7 cleanup: no child groups, ports or sockets remain; no units created'; fi exit "$result" } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM # No host configuration, preload hooks, credentials or agent env in fixtures. # The ancestry stand-in lives only in federation-host.mjs, per sage-3625. timeout -k 10 210 env -i PATH="$PATH" HOME="$R7_ROOT/home" TMPDIR="$R7_ROOT/tmp" \ MOSAIC_R7_ROOT="$R7_ROOT" MOSAIC_R7_INTERRUPT="$R7_INTERRUPT" \ node --test --test-concurrency=1 packages/cli/tests/federation-acceptance.test.mjs & R7_DRIVER=$! wait "$R7_DRIVER" R7_DRIVER=