320 lines
15 KiB
Bash
Executable File
320 lines
15 KiB
Bash
Executable File
#!/bin/sh
|
|
# Production-shaped Linux server/nginx -> native Windows client test lane.
|
|
set -eu
|
|
|
|
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
usage: scripts/windows/native-test run|recover|clean --run-id ID [options]
|
|
|
|
run options:
|
|
--port PORT Current-controller TLS port (default RVBOX_NATIVE_PORT or 16899)
|
|
--keep retain the stack/VM lease for inspection
|
|
|
|
recover reports the exact VM lease and Compose resources.
|
|
clean stops only the matching stack and resets the matching VM run.
|
|
--purge --yes also delete only the matching local and Helium run files
|
|
|
|
run uses a separately tagged disposable fixture binary to prove every Windows
|
|
execution context through SCM, nginx WSS, the Linux server, and rvc. Release
|
|
binaries reject the fixture-only pre-launch failure switch.
|
|
EOF
|
|
}
|
|
|
|
fail() { printf '%s\n' "native-test: $*" >&2; exit 2; }
|
|
|
|
safe_id() {
|
|
case $1 in [a-z0-9]* ) ;; *) fail "run ID must start with lowercase alphanumeric" ;; esac
|
|
case $1 in ''|*[!a-z0-9-]*|????????????????????????????????????????????????????????????????*) fail "run ID must match [a-z0-9][a-z0-9-]{0,63}" ;; esac
|
|
}
|
|
|
|
action=${1-}
|
|
[ -n "$action" ] || { usage >&2; exit 2; }
|
|
shift
|
|
case $action in run|recover|clean|--help|-h) ;; *) usage >&2; fail "unknown action $action" ;; esac
|
|
[ "$action" != --help ] && [ "$action" != -h ] || { usage; exit 0; }
|
|
|
|
run_id=
|
|
port=${RVBOX_NATIVE_PORT:-16899}
|
|
keep=no
|
|
purge=no
|
|
yes=no
|
|
while [ "$#" -gt 0 ]; do
|
|
case $1 in
|
|
--run-id) [ "$#" -ge 2 ] || fail "--run-id needs a value"; run_id=$2; shift 2 ;;
|
|
--port) [ "$#" -ge 2 ] || fail "--port needs a value"; port=$2; shift 2 ;;
|
|
--keep) keep=yes; shift ;;
|
|
--purge) purge=yes; shift ;;
|
|
--yes) yes=yes; shift ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*) fail "unknown argument $1" ;;
|
|
esac
|
|
done
|
|
[ -n "$run_id" ] || fail "$action requires --run-id"
|
|
safe_id "$run_id"
|
|
case $port in *[!0-9]*|'') fail "--port must be an integer" ;; esac
|
|
[ "$port" -ge 1024 ] && [ "$port" -le 65535 ] || fail "--port must be 1024..65535"
|
|
[ "$purge" = no ] || [ "$action" = clean ] || fail "--purge is only valid with clean"
|
|
[ "$yes" = no ] || [ "$action" = clean ] || fail "--yes is only valid with clean"
|
|
[ "$purge" = no ] || [ "$yes" = yes ] || fail "--purge requires --yes"
|
|
|
|
: "${RVBOX_TEST_VBOX_HOST:=helium-remote}"
|
|
case $RVBOX_TEST_VBOX_HOST in ''|*[!A-Za-z0-9._:@-]*) fail "RVBOX_TEST_VBOX_HOST contains unsupported characters" ;; esac
|
|
endpoint_host=${RVBOX_NATIVE_ENDPOINT_HOST:-x1.xcel.me}
|
|
case $endpoint_host in ''|*[!A-Za-z0-9.-]*) fail "RVBOX_NATIVE_ENDPOINT_HOST contains unsupported characters" ;; esac
|
|
project=rvbox-native-$run_id
|
|
run_root=$repo_root/.test-runs/$run_id
|
|
fixture_dir=$run_root/native-windows
|
|
client_id=native-$run_id
|
|
client_config=$fixture_dir/client.toml
|
|
server_config=$fixture_dir/server.toml
|
|
vm_prepared=no
|
|
|
|
compose() {
|
|
RVBOX_NATIVE_RUN_ID="$run_id" RVBOX_NATIVE_PORT="$port" \
|
|
RVBOX_NATIVE_BIND=0.0.0.0 RVBOX_NATIVE_UID="$(id -u)" RVBOX_NATIVE_GID="$(id -g)" \
|
|
RVBOX_NATIVE_RUNTIME_DIR="$fixture_dir" RVBOX_NATIVE_ENDPOINT_HOST="$endpoint_host" \
|
|
docker compose -p "$project" -f "$repo_root/test/linux-server/compose.yaml" "$@"
|
|
}
|
|
|
|
remove_project_networks() {
|
|
# The project label is Docker Compose's exact resource-ownership key. This
|
|
# also reclaims legacy certgen default networks left by fixtures created
|
|
# before certgen joined the labeled native network.
|
|
for network_id in $(docker network ls --filter "label=com.docker.compose.project=$project" --quiet); do
|
|
docker network rm "$network_id" >/dev/null || fail "could not remove owned Docker network $network_id"
|
|
done
|
|
}
|
|
|
|
rvc() {
|
|
compose exec -T server /opt/rvbox/rvc --socket /run/rvbox/server.sock "$@"
|
|
}
|
|
|
|
prepare_files() {
|
|
umask 077
|
|
mkdir -p "$fixture_dir"
|
|
[ ! -e "$server_config" ] || fail "refusing to overwrite existing $server_config"
|
|
[ ! -e "$client_config" ] || fail "refusing to overwrite existing $client_config"
|
|
printf '%s\n' '[server]' 'data_dir = "/state/data"' 'agent_listen = "0.0.0.0:6899"' 'control_socket = "/run/rvbox/server.sock"' '' '[observability]' 'listen = "0.0.0.0:6901"' >"$server_config"
|
|
printf '%s\n' \
|
|
'[client]' \
|
|
"server_url = \"wss://$endpoint_host:$port/v1/agent\"" \
|
|
'state_dir = "C:\\ProgramData\\RVBox\\test-state"' \
|
|
"client_id = \"$client_id\"" \
|
|
'daemon_cwd = "C:\\ProgramData\\RVBox\\test-work"' \
|
|
'' '[tls]' \
|
|
'# Empty intentionally exercises v1 matching-host self-signed TLS.' \
|
|
'ca_file = ""' \
|
|
"server_name = \"$endpoint_host\"" \
|
|
'' '[observability]' \
|
|
'listen = "127.0.0.1:6902"' \
|
|
'log_file = "C:\\ProgramData\\RVBox\\test-logs\\rvbox.log"' >"$client_config"
|
|
chmod 600 "$server_config" "$client_config"
|
|
}
|
|
|
|
stage_stack() {
|
|
# scripts/build intentionally execs its Docker command. Keep that process
|
|
# replacement inside a subshell so this lifecycle controller continues.
|
|
("$repo_root/scripts/build" build)
|
|
install -d -m 700 "$fixture_dir/pki" "$fixture_dir/state/data" "$fixture_dir/state/control"
|
|
compose --profile tools run --rm certgen
|
|
compose up -d server nginx
|
|
attempt=0
|
|
while [ "$attempt" -lt 30 ]; do
|
|
if rvc stat >/dev/null 2>&1; then return 0; fi
|
|
attempt=$((attempt + 1)); sleep 1
|
|
done
|
|
compose logs --tail=200
|
|
fail "server control socket did not become ready"
|
|
}
|
|
|
|
wait_client() {
|
|
attempt=0
|
|
while [ "$attempt" -lt 45 ]; do
|
|
state=$(rvc stat "$client_id" 2>/dev/null || true)
|
|
if printf '%s\n' "$state" | grep -q "client $client_id connected=true"; then return 0; fi
|
|
attempt=$((attempt + 1)); sleep 1
|
|
done
|
|
compose logs --tail=200
|
|
fail "native Windows client did not connect through nginx WSS"
|
|
}
|
|
|
|
assert_context() {
|
|
label=$1
|
|
elevated=$2
|
|
want=$3
|
|
if [ "$elevated" = yes ]; then
|
|
issued=$(rvc run --background --shell cmd --elevated "$client_id" "echo RVBOX_NATIVE_$label" 2>&1) || fail "$label admission failed: $issued"
|
|
else
|
|
issued=$(rvc run --background --shell cmd "$client_id" "echo RVBOX_NATIVE_$label" 2>&1) || fail "$label admission failed: $issued"
|
|
fi
|
|
issue=$(printf '%s\n' "$issued" | awk 'NR == 1 { print $1 }')
|
|
case $issue in ????????-????-7???-????-????????????) ;; *) fail "$label returned invalid issue UUID: $issued" ;; esac
|
|
attempt=0
|
|
while [ "$attempt" -lt 45 ]; do
|
|
result=$(rvc stat "$client_id" "$issue" 2>/dev/null || true)
|
|
if printf '%s\n' "$result" | grep -q 'lifecycle=COMMAND_SUCCEEDED'; then
|
|
printf '%s\n' "$result" | grep -q "windows_effective_context=$want" || fail "$label effective context mismatch: $result"
|
|
printf '%s\n' "$result" >"$fixture_dir/$label.stat"
|
|
printf 'passed %s issue=%s context=%s\n' "$label" "$issue" "$want"
|
|
return 0
|
|
fi
|
|
case $result in *'lifecycle=COMMAND_FAILED'*|*'lifecycle=COMMAND_REJECTED'*|*'lifecycle=COMMAND_TERMINATED'*) fail "$label did not succeed: $result" ;; esac
|
|
attempt=$((attempt + 1)); sleep 1
|
|
done
|
|
fail "$label did not reach terminal success"
|
|
}
|
|
|
|
assert_stdin_close() {
|
|
issued=$(rvc run --background --shell cmd "$client_id" 'set /p RVBOX_STDIN= & echo RVBOX_STDIN_%RVBOX_STDIN%' 2>&1) || fail "stdin command admission failed: $issued"
|
|
issue=$(printf '%s\n' "$issued" | awk 'NR == 1 { print $1 }')
|
|
case $issue in ????????-????-7???-????-????????????) ;; *) fail "stdin command returned invalid issue UUID: $issued" ;; esac
|
|
# The command remains running until the durable StdinWrite reaches the
|
|
# native service. Use the public rvc controls, not guest-side injection.
|
|
rvc append "$client_id" "$issue" RVBOX_NATIVE_INPUT >/dev/null || fail "stdin append failed"
|
|
rvc close-stdin "$client_id" "$issue" >/dev/null || fail "stdin close failed"
|
|
attempt=0
|
|
while [ "$attempt" -lt 45 ]; do
|
|
result=$(rvc stat "$client_id" "$issue" 2>/dev/null || true)
|
|
if printf '%s\n' "$result" | grep -q 'lifecycle=COMMAND_SUCCEEDED'; then
|
|
printf '%s\n' "$result" >"$fixture_dir/stdin-close.stat"
|
|
printf 'passed stdin-close issue=%s\n' "$issue"
|
|
return 0
|
|
fi
|
|
case $result in *'lifecycle=COMMAND_FAILED'*|*'lifecycle=COMMAND_REJECTED'*|*'lifecycle=COMMAND_TERMINATED'*) fail "stdin command did not succeed: $result" ;; esac
|
|
attempt=$((attempt + 1)); sleep 1
|
|
done
|
|
fail "stdin command did not reach terminal success"
|
|
}
|
|
|
|
assert_signal_term() {
|
|
issued=$(rvc run --background --shell cmd "$client_id" 'ping -t 127.0.0.1 >NUL' 2>&1) || fail "signal command admission failed: $issued"
|
|
issue=$(printf '%s\n' "$issued" | awk 'NR == 1 { print $1 }')
|
|
case $issue in ????????-????-7???-????-????????????) ;; *) fail "signal command returned invalid issue UUID: $issued" ;; esac
|
|
attempt=0
|
|
while [ "$attempt" -lt 30 ]; do
|
|
result=$(rvc stat "$client_id" "$issue" 2>/dev/null || true)
|
|
if printf '%s\n' "$result" | grep -q 'lifecycle=COMMAND_RUNNING'; then break; fi
|
|
attempt=$((attempt + 1)); sleep 1
|
|
done
|
|
[ "$attempt" -lt 30 ] || fail "signal command did not reach running state"
|
|
rvc kill TERM "$client_id" "$issue" >/dev/null || fail "TERM request failed"
|
|
attempt=0
|
|
while [ "$attempt" -lt 45 ]; do
|
|
result=$(rvc stat "$client_id" "$issue" 2>/dev/null || true)
|
|
if printf '%s\n' "$result" | grep -q 'lifecycle=COMMAND_TERMINATED'; then
|
|
printf '%s\n' "$result" >"$fixture_dir/signal-term.stat"
|
|
printf 'passed signal-term issue=%s\n' "$issue"
|
|
return 0
|
|
fi
|
|
case $result in *'lifecycle=COMMAND_FAILED'*|*'lifecycle=COMMAND_REJECTED'*|*'lifecycle=COMMAND_SUCCEEDED'*) fail "TERM command reached wrong terminal state: $result" ;; esac
|
|
attempt=$((attempt + 1)); sleep 1
|
|
done
|
|
fail "TERM command did not reach terminal state"
|
|
}
|
|
|
|
assert_server_restart_reconnect() {
|
|
issued=$(rvc run --background --shell cmd "$client_id" 'ping -t 127.0.0.1 >NUL' 2>&1) || fail "restart command admission failed: $issued"
|
|
issue=$(printf '%s\n' "$issued" | awk 'NR == 1 { print $1 }')
|
|
case $issue in ????????-????-7???-????-????????????) ;; *) fail "restart command returned invalid issue UUID: $issued" ;; esac
|
|
attempt=0
|
|
while [ "$attempt" -lt 30 ]; do
|
|
result=$(rvc stat "$client_id" "$issue" 2>/dev/null || true)
|
|
if printf '%s\n' "$result" | grep -q 'lifecycle=COMMAND_RUNNING'; then break; fi
|
|
attempt=$((attempt + 1)); sleep 1
|
|
done
|
|
[ "$attempt" -lt 30 ] || fail "restart command did not reach running state"
|
|
# This preserves the server state volume while replacing the actual server
|
|
# process behind nginx. A later TERM terminal result can only arrive after
|
|
# the Windows daemon has established a fresh reconciled WSS session.
|
|
compose restart server
|
|
attempt=0
|
|
while [ "$attempt" -lt 45 ]; do
|
|
if rvc stat "$client_id" >/dev/null 2>&1; then break; fi
|
|
attempt=$((attempt + 1)); sleep 1
|
|
done
|
|
[ "$attempt" -lt 45 ] || fail "server control socket did not recover after restart"
|
|
wait_client
|
|
rvc kill TERM "$client_id" "$issue" >/dev/null || fail "TERM after server restart failed"
|
|
attempt=0
|
|
while [ "$attempt" -lt 45 ]; do
|
|
result=$(rvc stat "$client_id" "$issue" 2>/dev/null || true)
|
|
if printf '%s\n' "$result" | grep -q 'lifecycle=COMMAND_TERMINATED'; then
|
|
printf '%s\n' "$result" >"$fixture_dir/server-restart-reconnect.stat"
|
|
printf 'passed server-restart-reconnect issue=%s\n' "$issue"
|
|
return 0
|
|
fi
|
|
case $result in *'lifecycle=COMMAND_FAILED'*|*'lifecycle=COMMAND_REJECTED'*|*'lifecycle=COMMAND_SUCCEEDED'*) fail "restart TERM command reached wrong terminal state: $result" ;; esac
|
|
attempt=$((attempt + 1)); sleep 1
|
|
done
|
|
fail "restart TERM command did not reach terminal state"
|
|
}
|
|
|
|
collect() {
|
|
if [ "$vm_prepared" = yes ]; then
|
|
"$repo_root/scripts/windows/test-host" collect --run-id "$run_id" || true
|
|
fi
|
|
if [ -d "$fixture_dir" ]; then
|
|
compose logs --no-color --tail=500 >"$fixture_dir/server-proxy.log" 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
clean() {
|
|
compose down --volumes --remove-orphans || true
|
|
remove_project_networks
|
|
# A reset is the isolation boundary for the next run. Do not conceal a
|
|
# failed shutdown/snapshot restore behind a successful-looking `clean`:
|
|
# callers must repair or explicitly inspect the retained VM lease first.
|
|
"$repo_root/scripts/windows/test-host" reset --run-id "$run_id"
|
|
if [ "$purge" = yes ]; then
|
|
[ -L "$run_root" ] && fail "refusing symlink run root $run_root"
|
|
rm -rf "$run_root"
|
|
fi
|
|
}
|
|
|
|
case $action in
|
|
recover)
|
|
"$repo_root/scripts/windows/test-host" recover --run-id "$run_id"
|
|
compose ps
|
|
printf 'run_root=%s\n' "$run_root"
|
|
;;
|
|
clean)
|
|
collect
|
|
clean
|
|
printf 'cleaned run_id=%s\n' "$run_id"
|
|
;;
|
|
run)
|
|
trap 'status=$?; if [ "$status" -ne 0 ]; then collect; fi' EXIT
|
|
[ ! -e "$run_root" ] || fail "refusing to reuse existing run root $run_root; inspect with recover or remove with clean --purge --yes"
|
|
prepare_files
|
|
stage_stack
|
|
"$repo_root/scripts/windows/build-test-bundle" --native-fixture --run-id "$run_id" --config "$client_config"
|
|
"$repo_root/scripts/windows/test-host" prepare --run-id "$run_id"
|
|
vm_prepared=yes
|
|
"$repo_root/scripts/windows/test-host" stage --run-id "$run_id" --bundle "$run_root/windows-bundle"
|
|
"$repo_root/scripts/windows/test-host" install --run-id "$run_id"
|
|
"$repo_root/scripts/windows/test-host" run --run-id "$run_id" --endpoint "$endpoint_host:$port"
|
|
wait_client
|
|
assert_stdin_close
|
|
assert_signal_term
|
|
assert_server_restart_reconnect
|
|
assert_context active-user no active-user
|
|
assert_context active-user-elevated yes active-user-elevated
|
|
"$repo_root/scripts/windows/test-host" run --run-id "$run_id" --fail-contexts ACTIVE_USER_ELEVATED
|
|
wait_client
|
|
assert_context active-system yes active-system
|
|
"$repo_root/scripts/windows/test-host" run --run-id "$run_id" --fail-contexts ACTIVE_USER_ELEVATED,ACTIVE_SYSTEM
|
|
wait_client
|
|
assert_context local-system-active-fallback yes local-system
|
|
"$repo_root/scripts/windows/test-host" run --run-id "$run_id"
|
|
wait_client
|
|
"$repo_root/scripts/windows/test-host" logoff --run-id "$run_id"
|
|
assert_context local-service no local-service
|
|
assert_context local-system-no-user yes local-system
|
|
collect
|
|
if [ "$keep" = no ]; then clean; fi
|
|
printf 'native Windows hierarchy run passed: %s\n' "$run_id"
|
|
;;
|
|
esac
|