327 lines
13 KiB
Bash
Executable File
327 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
demo_repository_root() {
|
|
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P
|
|
}
|
|
|
|
demo_is_uint() {
|
|
[[ ${1:-} =~ ^[0-9]+$ ]]
|
|
}
|
|
|
|
demo_is_process_id() {
|
|
[[ ${1:-} =~ ^[1-9][0-9]*$ ]] && ((10#$1 > 1))
|
|
}
|
|
|
|
demo_read_one_line() {
|
|
local path=$1 destination=$2 value extra
|
|
IFS= read -r value <"$path" || return 1
|
|
if IFS= read -r extra < <(sed -n '2p' "$path") && [[ -n "$extra" ]]; then return 1; fi
|
|
printf -v "$destination" '%s' "$value"
|
|
}
|
|
|
|
demo_read_process_identity() {
|
|
local pid=$1 state_name=$2 start_name=$3 pgid_name=$4 sid_name=$5 stat rest
|
|
local -a fields
|
|
demo_is_process_id "$pid" || return 1
|
|
{ IFS= read -r stat <"/proc/$pid/stat"; } 2>/dev/null || return 1
|
|
rest=${stat##*) }
|
|
read -r -a fields <<<"$rest"
|
|
((${#fields[@]} >= 20)) || return 1
|
|
demo_is_uint "${fields[2]}" && demo_is_uint "${fields[3]}" && demo_is_uint "${fields[19]}" || return 1
|
|
printf -v "$state_name" '%s' "${fields[0]}"
|
|
printf -v "$pgid_name" '%s' "${fields[2]}"
|
|
printf -v "$sid_name" '%s' "${fields[3]}"
|
|
printf -v "$start_name" '%s' "${fields[19]}"
|
|
}
|
|
|
|
demo_start_tick() {
|
|
local state start pgid sid
|
|
demo_read_process_identity "$1" state start pgid sid || return 1
|
|
printf '%s\n' "$start"
|
|
}
|
|
|
|
demo_process_group() {
|
|
local state start pgid sid
|
|
demo_read_process_identity "$1" state start pgid sid || return 1
|
|
printf '%s\n' "$pgid"
|
|
}
|
|
|
|
demo_pid_is_running() {
|
|
local state start pgid sid
|
|
demo_read_process_identity "$1" state start pgid sid && [[ "$state" != Z ]]
|
|
}
|
|
|
|
demo_process_has_command_line() {
|
|
local pid=$1 first
|
|
{ IFS= read -r -d '' first <"/proc/$pid/cmdline"; } 2>/dev/null || return 1
|
|
[[ -n "$first" ]]
|
|
}
|
|
|
|
demo_resolve_command() {
|
|
local value=$1 resolved
|
|
if [[ "$value" == */* ]]; then
|
|
readlink -f -- "$value" 2>/dev/null
|
|
else
|
|
resolved=$(command -v -- "$value" 2>/dev/null) || return 1
|
|
readlink -f -- "$resolved" 2>/dev/null
|
|
fi
|
|
}
|
|
|
|
demo_entry_index() {
|
|
local expected=$1 array_name=$2 argument resolved index
|
|
local -n argv_ref=$array_name
|
|
for index in "${!argv_ref[@]}"; do
|
|
argument=${argv_ref[index]}
|
|
resolved=$(demo_resolve_command "$argument" 2>/dev/null || true)
|
|
if [[ "$resolved" == "$expected" ]]; then printf '%s\n' "$index"; return 0; fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
demo_interpreter_for_entry() {
|
|
local entry=$1 line interpreter
|
|
IFS= read -r line <"$entry" 2>/dev/null || return 1
|
|
[[ "$line" == '#!'* ]] || return 1
|
|
line=${line#\#!}
|
|
read -r -a interpreter <<<"$line"
|
|
((${#interpreter[@]} > 0)) || return 1
|
|
if [[ ${interpreter[0]} == */env && ${interpreter[1]:-} ]]; then demo_resolve_command "${interpreter[1]}"
|
|
else demo_resolve_command "${interpreter[0]}"; fi
|
|
}
|
|
|
|
demo_has_sequence_from() {
|
|
local array_name=$1 begin=$2
|
|
local -n argv_ref=$array_name
|
|
shift 2
|
|
local -a wanted=("$@")
|
|
local i j
|
|
for ((i = begin; i + ${#wanted[@]} <= ${#argv_ref[@]}; i++)); do
|
|
for ((j = 0; j < ${#wanted[@]}; j++)); do
|
|
[[ ${argv_ref[i+j]} == "${wanted[j]}" ]] || break
|
|
done
|
|
((j == ${#wanted[@]})) && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
demo_command_matches() {
|
|
local root=$1 pid=$2 kind=$3 expected executable interpreter entry_index
|
|
local -a arguments=()
|
|
[[ -r "/proc/$pid/cmdline" && -e "/proc/$pid/exe" ]] || return 1
|
|
mapfile -d '' -t arguments <"/proc/$pid/cmdline"
|
|
((${#arguments[@]} > 0)) || return 1
|
|
executable=$(readlink -f -- "/proc/$pid/exe") || return 1
|
|
|
|
if [[ "$kind" == anvil ]]; then
|
|
expected=$(demo_resolve_command anvil) || return 1
|
|
elif [[ "$kind" == vite ]]; then
|
|
expected=$(readlink -f -- "$root/web/node_modules/.bin/vite" 2>/dev/null) || return 1
|
|
else
|
|
return 1
|
|
fi
|
|
entry_index=$(demo_entry_index "$expected" arguments) || return 1
|
|
if [[ "$executable" != "$expected" ]]; then
|
|
interpreter=$(demo_interpreter_for_entry "$expected") || return 1
|
|
[[ "$executable" == "$interpreter" && "$entry_index" -eq 1 ]] || return 1
|
|
else
|
|
[[ "$entry_index" -eq 0 ]] || return 1
|
|
fi
|
|
|
|
if [[ "$kind" == anvil ]]; then
|
|
demo_has_sequence_from arguments "$((entry_index + 1))" --host 127.0.0.1 || return 1
|
|
demo_has_sequence_from arguments "$((entry_index + 1))" --port 8545 || return 1
|
|
demo_has_sequence_from arguments "$((entry_index + 1))" --chain-id 31337 || return 1
|
|
else
|
|
local -a expected_tail=(web --host 127.0.0.1 --port 5173)
|
|
local -a actual_tail=("${arguments[@]:entry_index+1}")
|
|
[[ "${actual_tail[*]}" == "${expected_tail[*]}" && ${#actual_tail[@]} -eq ${#expected_tail[@]} ]] || return 1
|
|
fi
|
|
}
|
|
|
|
demo_identity_matches() {
|
|
local root=$1 kind=$2 pid=$3 expected_start=$4 expected_pgid=$5 state start pgid sid
|
|
demo_read_process_identity "$pid" state start pgid sid || return 1
|
|
[[ "$state" != Z && "$start" == "$expected_start" && "$pgid" == "$expected_pgid" \
|
|
&& "$sid" == "$expected_pgid" && "$pid" == "$expected_pgid" ]] || return 1
|
|
demo_command_matches "$root" "$pid" "$kind"
|
|
}
|
|
|
|
demo_capture_process() {
|
|
local root=$1 kind=$2 pid=$3 start_name=$4 pgid_name=$5 state actual_start actual_pgid sid
|
|
demo_read_process_identity "$pid" state actual_start actual_pgid sid || return 1
|
|
[[ "$state" != Z && "$pid" == "$actual_pgid" && "$pid" == "$sid" ]] || return 1
|
|
demo_command_matches "$root" "$pid" "$kind" || return 1
|
|
printf -v "$start_name" '%s' "$actual_start"
|
|
printf -v "$pgid_name" '%s' "$actual_pgid"
|
|
}
|
|
|
|
demo_remove_record() {
|
|
local root=$1 kind=$2 path
|
|
for path in "$root/.demo/$kind.pid" "$root/.demo/$kind.start" "$root/.demo/$kind.pgid"; do
|
|
if [[ -e "$path" ]]; then
|
|
rm -f -- "$path"
|
|
printf 'Removed %s\n' "${path#"$root/"}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
demo_publish_process() {
|
|
local root=$1 kind=$2 pid=$3 start=$4 pgid=$5 path
|
|
demo_is_process_id "$pid" && demo_is_uint "$start" && demo_is_process_id "$pgid" && [[ "$pid" == "$pgid" ]] || return 1
|
|
demo_identity_matches "$root" "$kind" "$pid" "$start" "$pgid" || return 1
|
|
mkdir -p "$root/.demo"
|
|
chmod 0700 "$root/.demo"
|
|
for path in "$root/.demo/$kind.pid" "$root/.demo/$kind.start" "$root/.demo/$kind.pgid"; do [[ ! -e "$path" ]] || return 1; done
|
|
printf '%s\n' "$start" >"$root/.demo/$kind.start"
|
|
printf '%s\n' "$pgid" >"$root/.demo/$kind.pgid"
|
|
chmod 0600 "$root/.demo/$kind.start" "$root/.demo/$kind.pgid"
|
|
demo_identity_matches "$root" "$kind" "$pid" "$start" "$pgid" || return 1
|
|
# PID is the atomic commit marker. PID equals PGID, so a hard link publishes
|
|
# the validated value without a temporary path or separately visible write.
|
|
ln -- "$root/.demo/$kind.pgid" "$root/.demo/$kind.pid" || return 1
|
|
chmod 0600 "$root/.demo/$kind.pid"
|
|
}
|
|
|
|
demo_record_process() {
|
|
local root=$1 kind=$2 pid=$3 start pgid
|
|
demo_capture_process "$root" "$kind" "$pid" start pgid || return 1
|
|
demo_publish_process "$root" "$kind" "$pid" "$start" "$pgid"
|
|
}
|
|
|
|
demo_collect_group_members() {
|
|
local expected_pgid=$1 expected_sid=$2 output_name=$3 pid ps_pgid ps_sid ps_state state start pgid sid
|
|
local -n output_ref=$output_name
|
|
output_ref=()
|
|
while read -r pid ps_pgid ps_sid ps_state; do
|
|
[[ "$ps_pgid" == "$expected_pgid" && "$ps_sid" == "$expected_sid" && "$ps_state" != Z* ]] || continue
|
|
demo_read_process_identity "$pid" state start pgid sid || continue
|
|
[[ "$state" != Z && "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" ]] || continue
|
|
output_ref+=("$pid:$start")
|
|
done < <(ps -eo pid=,pgid=,sid=,stat=)
|
|
}
|
|
|
|
demo_anchor_allows_signal() {
|
|
local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 state start pgid sid
|
|
if ! demo_read_process_identity "$leader" state start pgid sid; then return 0; fi
|
|
[[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_pgid" ]] || return 1
|
|
[[ "$state" == Z ]] && return 0
|
|
demo_process_has_command_line "$leader" || return 0
|
|
demo_command_matches "$root" "$leader" "$kind"
|
|
}
|
|
|
|
demo_signal_member() {
|
|
local pid=$1 expected_start=$2 expected_pgid=$3 expected_sid=$4 signal=$5 state start pgid sid
|
|
if ! demo_read_process_identity "$pid" state start pgid sid; then return 0; fi
|
|
[[ "$state" != Z ]] || return 0
|
|
[[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" ]] || return 2
|
|
kill -"$signal" -- "$pid"
|
|
}
|
|
|
|
demo_group_has_live_members() {
|
|
local pgid=$1 sid=$2
|
|
local -a members
|
|
demo_collect_group_members "$pgid" "$sid" members
|
|
((${#members[@]} > 0))
|
|
}
|
|
|
|
demo_stop_launch() {
|
|
local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 member member_pid member_start result
|
|
local -a members
|
|
demo_is_process_id "$leader" && demo_is_uint "$expected_start" && demo_is_process_id "$expected_pgid" \
|
|
&& [[ "$leader" == "$expected_pgid" ]] || return 1
|
|
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || {
|
|
printf 'Refusing %s PID %s: process identity changed\n' "$kind" "$leader" >&2
|
|
return 1
|
|
}
|
|
demo_collect_group_members "$expected_pgid" "$expected_pgid" members
|
|
if ((${#members[@]} == 0)); then
|
|
demo_remove_record "$root" "$kind"
|
|
return 0
|
|
fi
|
|
|
|
printf 'Stopping validated %s session/process group %s\n' "$kind" "$expected_pgid"
|
|
for member in "${members[@]}"; do
|
|
IFS=: read -r member_pid member_start <<<"$member"
|
|
[[ "$member_pid" != "$leader" ]] || continue
|
|
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || return 1
|
|
demo_signal_member "$member_pid" "$member_start" "$expected_pgid" "$expected_pgid" TERM || return 1
|
|
done
|
|
for member in "${members[@]}"; do
|
|
IFS=: read -r member_pid member_start <<<"$member"
|
|
[[ "$member_pid" == "$leader" ]] || continue
|
|
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || return 1
|
|
demo_signal_member "$member_pid" "$member_start" "$expected_pgid" "$expected_pgid" TERM || return 1
|
|
done
|
|
for ((result = 0; result < ${DEMO_TERM_WAIT_ATTEMPTS:-50}; result++)); do
|
|
demo_group_has_live_members "$expected_pgid" "$expected_pgid" || break
|
|
sleep 0.1
|
|
done
|
|
|
|
if demo_group_has_live_members "$expected_pgid" "$expected_pgid"; then
|
|
printf 'Escalating surviving validated %s session members to KILL\n' "$kind"
|
|
for ((result = 0; result < ${DEMO_KILL_WAIT_ATTEMPTS:-20}; result++)); do
|
|
demo_collect_group_members "$expected_pgid" "$expected_pgid" members
|
|
((${#members[@]} > 0)) || break
|
|
for member in "${members[@]}"; do
|
|
IFS=: read -r member_pid member_start <<<"$member"
|
|
if ! demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid"; then
|
|
printf 'Refusing KILL escalation: %s leader identity changed\n' "$kind" >&2
|
|
return 1
|
|
fi
|
|
if demo_signal_member "$member_pid" "$member_start" "$expected_pgid" "$expected_pgid" KILL; then
|
|
:
|
|
else
|
|
result=$?
|
|
printf 'Refusing KILL escalation: %s member identity changed (%s)\n' "$kind" "$result" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
sleep 0.05
|
|
done
|
|
fi
|
|
wait "$leader" 2>/dev/null || true
|
|
if demo_group_has_live_members "$expected_pgid" "$expected_pgid"; then
|
|
printf 'Validated %s session/process group %s did not stop\n' "$kind" "$expected_pgid" >&2
|
|
return 1
|
|
fi
|
|
demo_remove_record "$root" "$kind"
|
|
}
|
|
|
|
demo_stop_recorded() {
|
|
local root=$1 kind=$2 pid start pgid
|
|
local pid_path="$root/.demo/$kind.pid" start_path="$root/.demo/$kind.start" pgid_path="$root/.demo/$kind.pgid"
|
|
[[ "$kind" == anvil || "$kind" == vite ]] || return 1
|
|
if [[ ! -e "$pid_path" ]]; then
|
|
if [[ ! -e "$start_path" && ! -e "$pgid_path" ]]; then return 0; fi
|
|
if [[ -e "$start_path" && -e "$pgid_path" ]] && demo_read_one_line "$start_path" start \
|
|
&& demo_read_one_line "$pgid_path" pgid && demo_is_uint "$start" && demo_is_process_id "$pgid"; then
|
|
pid=$pgid
|
|
printf 'Recovering partial %s launch record with validated in-memory shape\n' "$kind"
|
|
else
|
|
printf 'Removing incomplete %s launch metadata without signaling\n' "$kind"
|
|
demo_remove_record "$root" "$kind"
|
|
return 0
|
|
fi
|
|
else
|
|
if ! demo_read_one_line "$pid_path" pid || ! demo_is_process_id "$pid"; then
|
|
printf 'Refusing invalid %s PID record\n' "$kind" >&2
|
|
return 1
|
|
fi
|
|
if ! demo_read_one_line "$start_path" start || ! demo_is_uint "$start"; then
|
|
printf 'Refusing incomplete %s start-tick record\n' "$kind" >&2
|
|
return 1
|
|
fi
|
|
if ! demo_read_one_line "$pgid_path" pgid || ! demo_is_process_id "$pgid"; then
|
|
printf 'Refusing incomplete %s process-group record\n' "$kind" >&2
|
|
return 1
|
|
fi
|
|
fi
|
|
[[ "$pid" == "$pgid" ]] || { printf 'Refusing %s: PID/PGID ownership mismatch\n' "$kind" >&2; return 1; }
|
|
if ! demo_group_has_live_members "$pgid" "$pgid" && [[ ! -e "/proc/$pid/stat" ]]; then
|
|
printf 'Discarding stale %s process record for PID %s\n' "$kind" "$pid"
|
|
demo_remove_record "$root" "$kind"
|
|
return 0
|
|
fi
|
|
demo_stop_launch "$root" "$kind" "$pid" "$start" "$pgid"
|
|
}
|