mirror of
https://github.com/Cacti/spine.git
synced 2026-11-03 02:27:15 +09:00
## Summary - make subprocess-registry detach atomic so exactly one closer owns each entry - make cancellation cleanup release only the detached node it owns - replace `vfork`/`execve` child setup with `posix_spawn` and explicit file actions - mark every pipe and stdio-collision duplicate close-on-exec - restore the child's default SIGPIPE disposition while preserving the parent's handling - bound TERM/KILL reaping and park unreaped children in a fixed-capacity sweep list - harden PHP script-server slot ownership, startup, restart, signal, descriptor, and cleanup paths - retire and restart failed or oversized reads so stale response tails cannot poison later data sources - revalidate every command retry and cap recovery handshakes at two seconds This folds the child-process lifecycle portion of #597 into the narrower ownership PR. It does not absorb unrelated poll-host, SQL, SNMP, ICMP, or build-infrastructure work. ## Review-driven corrections - close-on-exec is explicitly restored after `dup()` in both read and write stdio-collision branches - the source invariant check now uses portable POSIX character classes - direct tests cover duplicate flags, both collision branches, failure after pipe creation, abandoned-child sweeping, and capacity overflow - collision cleanup preserves current-child redirects even when an older registry entry occupies fd 0 or 1 - script children receive SIGTERM before SIGKILL and unreaped PHP children transfer to the bounded sweep registry - the Docker integration suite now starts a real PHP script server, verifies its startup protocol and database result, and checks that shutdown leaves no zombie PHP process ## Validation - Ubuntu 24.04 container with GCC and GNU ld: `make check`, 6/6 targets passed with no skips, including `test_php_runtime` - real PHP script-server Docker integration: 6/6 checks passed; exact value `42` reached `poller_output` and no zombie remained - macOS Homebrew dependency build passed - macOS `make check`: 5/5 available binaries passed; GNU `--wrap` runtime suite is Linux-only - PHP fixture syntax checked through mise PHP 8.3 - actionlint, shellcheck, and `git diff --check` passed Closes #610. --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net>
133 lines
4.9 KiB
Bash
Executable File
133 lines
4.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Integration test for the PHP script-server reap fix in php_close().
|
|
#
|
|
# Bug: php_close() sent SIGTERM but never waitpid()'d the child, so each
|
|
# script-server shutdown left a <defunct> zombie. Fix: reap with waitpid()
|
|
# and reset php_pid. This test drives a real poll against a script-server
|
|
# data source (poller_item.action = POLLER_ACTION_PHP_SCRIPT_SERVER = 2) and
|
|
# asserts no zombie php child survives the run.
|
|
#
|
|
# Requires docker compose. The checked-in fixture supplies PHP and the minimal
|
|
# script-server protocol needed to exercise the real Spine process lifecycle.
|
|
#
|
|
# Usage: ./tests/integration/test_script_server_reap.sh
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
COMPOSE=(docker compose -f "$REPO_ROOT/tests/snmpv3/docker-compose.yml")
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
pass() { echo " PASS: $*"; PASS=$((PASS+1)); }
|
|
fail() { echo " FAIL: $*"; FAIL=$((FAIL+1)); }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Preconditions: docker, the compose fixture, and a PHP-capable spine image.
|
|
# ---------------------------------------------------------------------------
|
|
command -v docker >/dev/null 2>&1 || { echo "FAIL: docker not installed"; exit 1; }
|
|
docker compose version >/dev/null 2>&1 || { echo "FAIL: docker compose plugin not available"; exit 1; }
|
|
|
|
echo ""
|
|
echo "=== Setup: build spine image and probe for PHP ==="
|
|
"${COMPOSE[@]}" build spine >/dev/null 2>&1 \
|
|
|| { echo "FAIL: spine image failed to build"; exit 1; }
|
|
|
|
# The script server execs PHP; without it the action=2 path cannot run.
|
|
if ! "${COMPOSE[@]}" run --rm --no-deps --entrypoint sh spine \
|
|
-c 'command -v php >/dev/null 2>&1'; then
|
|
echo "FAIL: spine runtime image has no PHP" >&2
|
|
exit 1
|
|
fi
|
|
|
|
cleanup() {
|
|
echo ""
|
|
echo "=== Cleanup ==="
|
|
"${COMPOSE[@]}" down -v --remove-orphans 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
"${COMPOSE[@]}" up -d db snmpd >/dev/null 2>&1
|
|
|
|
# Wait for seed data.
|
|
elapsed=0
|
|
while [[ $elapsed -lt 120 ]]; do
|
|
count=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
|
-N -e "SELECT COUNT(*) FROM host;" 2>/dev/null || echo "0")
|
|
[[ "$count" -gt 0 ]] && break
|
|
sleep 3; elapsed=$((elapsed + 3))
|
|
done
|
|
[[ "$count" -gt 0 ]] || { fail "database did not start"; exit 1; }
|
|
pass "infrastructure ready"
|
|
|
|
"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e "
|
|
INSERT INTO settings (name, value) VALUES
|
|
('path_webroot', '/opt/cacti'),
|
|
('path_php_binary', '/usr/bin/php')
|
|
ON DUPLICATE KEY UPDATE value = VALUES(value);
|
|
" 2>/dev/null
|
|
pass "PHP script-server settings seeded"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Seed a script-server data source (action=2) for host 1.
|
|
# ---------------------------------------------------------------------------
|
|
echo ""
|
|
echo "=== Seed script-server poller_item (action=2) ==="
|
|
"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e "
|
|
INSERT IGNORE INTO poller_item (
|
|
local_data_id, host_id, action, hostname,
|
|
rrd_name, rrd_path, rrd_num, rrd_step, arg1, deleted, poller_id
|
|
) VALUES (
|
|
900, 1, 2, 'localhost',
|
|
'ss', '/dev/null', 1, 300, 'ss_test.php ss_value 1', '', 1
|
|
);" 2>/dev/null
|
|
pass "script-server poller_item seeded"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Poll: spine starts the script server, then php_close() must reap it.
|
|
# Run inside one container so we can inspect its process table afterward.
|
|
# ---------------------------------------------------------------------------
|
|
echo ""
|
|
echo "=== Poll and check for zombie php children ==="
|
|
|
|
set +e
|
|
poll_out=$("${COMPOSE[@]}" run --rm --entrypoint sh spine -c '
|
|
/usr/local/bin/spine --conf=/etc/spine/spine.conf -f 1 -l 1 -S
|
|
echo "---PROCTABLE---"
|
|
ps -eo pid,ppid,stat,comm 2>/dev/null || true
|
|
' 2>&1)
|
|
poll_status=$?
|
|
set -e
|
|
echo "$poll_out"
|
|
|
|
if [[ $poll_status -ne 0 ]]; then
|
|
fail "spine exited with status $poll_status during script-server poll"
|
|
elif echo "$poll_out" | grep -qi "segfault\|SIGSEGV\|Aborted"; then
|
|
fail "spine crashed during script-server poll"
|
|
else
|
|
pass "spine completed script-server poll without crash"
|
|
fi
|
|
|
|
# A reaped child leaves no <defunct>/Z-state php entry.
|
|
proctable=$(echo "$poll_out" | sed -n '/---PROCTABLE---/,$p')
|
|
if echo "$proctable" | grep -Ei '<defunct>|[[:space:]]Z[[:space:]+]*[[:space:]]php'; then
|
|
fail "zombie php child remained after poll (php_close reap regression)"
|
|
else
|
|
pass "no zombie php child after poll"
|
|
fi
|
|
|
|
value=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -N -B \
|
|
-e "SELECT output FROM poller_output WHERE local_data_id = 900 LIMIT 1;" 2>/dev/null)
|
|
if [[ $value == 42 ]]; then
|
|
pass "script-server command returned and stored 42"
|
|
else
|
|
fail "script-server command did not store 42 (got '$value')"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Summary
|
|
# ---------------------------------------------------------------------------
|
|
echo ""
|
|
echo "=== Results: ${PASS} passed, ${FAIL} failed ==="
|
|
[[ $FAIL -eq 0 ]]
|