fix(apps/startup-scripts): guard against duplicate/too-early server starts (#26149)
Co-authored-by: YehonalBot <yehonalbot@users.noreply.github.com>
This commit is contained in:
parent
a43f01c351
commit
49cf6c14f3
2 changed files with 251 additions and 1 deletions
|
|
@ -45,6 +45,174 @@ if [ ! -f "$BINARY" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
function config_value() {
|
||||||
|
local key="$1"
|
||||||
|
|
||||||
|
if [ -z "${CONFIG_ABS:-}" ] || [ ! -f "$CONFIG_ABS" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
awk -F= -v key="$key" '
|
||||||
|
{
|
||||||
|
configKey = $1
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", configKey)
|
||||||
|
}
|
||||||
|
configKey == key {
|
||||||
|
value = $0
|
||||||
|
sub(/^[^=]*=/, "", value)
|
||||||
|
sub(/[[:space:]]*[#;].*$/, "", value)
|
||||||
|
gsub(/"/, "", value)
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
||||||
|
print value
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$CONFIG_ABS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function command_contains_config() {
|
||||||
|
local command_line="$1"
|
||||||
|
|
||||||
|
if [ -z "${CONFIG_ABS:-}" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$command_line" in
|
||||||
|
*" -c $CONFIG_ABS"*|*" -c \"$CONFIG_ABS\""*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function find_existing_instances() {
|
||||||
|
local line pid command_line
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
pid="${line%% *}"
|
||||||
|
command_line="${line#* }"
|
||||||
|
|
||||||
|
[ -z "$pid" ] && continue
|
||||||
|
[ "$pid" = "$$" ] && continue
|
||||||
|
[ "$pid" = "$PPID" ] && continue
|
||||||
|
|
||||||
|
case "$command_line" in
|
||||||
|
*"/starter "*|*"pgrep "*)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if command_contains_config "$command_line"; then
|
||||||
|
printf '%s\n' "$line"
|
||||||
|
fi
|
||||||
|
done < <(pgrep -fa "$EXECPATH" 2>/dev/null || true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function listening_on_port() {
|
||||||
|
local port="$1"
|
||||||
|
|
||||||
|
command -v ss >/dev/null 2>&1 || return 1
|
||||||
|
ss -H -ltn "sport = :$port" 2>/dev/null | grep -q .
|
||||||
|
}
|
||||||
|
|
||||||
|
function guarded_ports() {
|
||||||
|
local port soap_enabled soap_port
|
||||||
|
|
||||||
|
if [ "$BINFILE" != "worldserver" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
port="$(config_value "WorldServerPort" || true)"
|
||||||
|
if [ -n "$port" ]; then
|
||||||
|
printf '%s\n' "$port"
|
||||||
|
fi
|
||||||
|
|
||||||
|
soap_enabled="$(config_value "SOAP.Enabled" || true)"
|
||||||
|
soap_port="$(config_value "SOAP.Port" || true)"
|
||||||
|
if [ "$soap_enabled" = "1" ] && [ -n "$soap_port" ]; then
|
||||||
|
printf '%s\n' "$soap_port"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function find_listening_guard_ports() {
|
||||||
|
local port
|
||||||
|
|
||||||
|
while IFS= read -r port; do
|
||||||
|
[ -z "$port" ] && continue
|
||||||
|
if listening_on_port "$port"; then
|
||||||
|
printf '%s\n' "$port"
|
||||||
|
fi
|
||||||
|
done < <(guarded_ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait_for_startup_guard() {
|
||||||
|
local timeout="${AC_STARTUP_GUARD_TIMEOUT:-90}"
|
||||||
|
local interval="${AC_STARTUP_GUARD_INTERVAL:-2}"
|
||||||
|
local elapsed=0
|
||||||
|
local instances ports
|
||||||
|
local sleep_for
|
||||||
|
local warned=0
|
||||||
|
|
||||||
|
if [ "${AC_STARTUP_SKIP_GUARD:-0}" = "1" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$timeout" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
echo "Startup guard: invalid AC_STARTUP_GUARD_TIMEOUT '$timeout', using 90."
|
||||||
|
timeout=90
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$interval" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
echo "Startup guard: invalid AC_STARTUP_GUARD_INTERVAL '$interval', using 2."
|
||||||
|
interval=2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$interval" -lt 1 ]; then
|
||||||
|
echo "Startup guard: AC_STARTUP_GUARD_INTERVAL must be at least 1, using 1."
|
||||||
|
interval=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
instances="$(find_existing_instances)"
|
||||||
|
ports="$(find_listening_guard_ports)"
|
||||||
|
|
||||||
|
if [ -z "$instances" ] && [ -z "$ports" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$elapsed" -ge "$timeout" ]; then
|
||||||
|
echo "Startup guard: refusing to start duplicate $BINFILE after waiting ${timeout}s."
|
||||||
|
if [ -n "$instances" ]; then
|
||||||
|
echo "Startup guard: existing matching process(es):"
|
||||||
|
echo "$instances"
|
||||||
|
fi
|
||||||
|
if [ -n "$ports" ]; then
|
||||||
|
echo "Startup guard: configured port(s) still listening: $(echo "$ports" | paste -sd ',' -)"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$warned" -eq 0 ]; then
|
||||||
|
echo "Startup guard: waiting for existing $BINFILE instance or configured port(s) to clear..."
|
||||||
|
warned=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep_for="$interval"
|
||||||
|
if [ $((elapsed + sleep_for)) -gt "$timeout" ]; then
|
||||||
|
sleep_for=$((timeout - elapsed))
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$sleep_for" -le 0 ] && continue
|
||||||
|
|
||||||
|
sleep "$sleep_for"
|
||||||
|
elapsed=$((elapsed + sleep_for))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# Create crashes directory if it doesn't exist
|
# Create crashes directory if it doesn't exist
|
||||||
mkdir -p "$CRASHES_PATH"
|
mkdir -p "$CRASHES_PATH"
|
||||||
|
|
||||||
|
|
@ -55,6 +223,8 @@ cd "$BINPATH" || {
|
||||||
|
|
||||||
EXECPATH=$(realpath "$BINFILE")
|
EXECPATH=$(realpath "$BINFILE")
|
||||||
|
|
||||||
|
wait_for_startup_guard
|
||||||
|
|
||||||
if [ "$GDB_ENABLED" -eq 1 ]; then
|
if [ "$GDB_ENABLED" -eq 1 ]; then
|
||||||
echo "Starting $EXECPATH with GDB enabled"
|
echo "Starting $EXECPATH with GDB enabled"
|
||||||
|
|
||||||
|
|
@ -148,4 +318,4 @@ else
|
||||||
exec "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"}
|
exec "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,86 @@ teardown() {
|
||||||
[[ "$output" =~ "Test server starting" ]]
|
[[ "$output" =~ "Test server starting" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "starter: should refuse duplicate binary/config start" {
|
||||||
|
cat > "$TEST_DIR/bin/duplicate-server" << 'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
sleep 30
|
||||||
|
EOF
|
||||||
|
chmod +x "$TEST_DIR/bin/duplicate-server"
|
||||||
|
|
||||||
|
"$TEST_DIR/bin/duplicate-server" -c "$TEST_DIR/test-server.conf" &
|
||||||
|
local duplicate_pid=$!
|
||||||
|
|
||||||
|
AC_STARTUP_GUARD_TIMEOUT=1 AC_STARTUP_GUARD_INTERVAL=1 run timeout 5s "$SCRIPT_DIR/starter" "$TEST_DIR/bin" "duplicate-server" "" "$TEST_DIR/test-server.conf" "" "" 0
|
||||||
|
kill "$duplicate_pid" 2>/dev/null || true
|
||||||
|
wait "$duplicate_pid" 2>/dev/null || true
|
||||||
|
|
||||||
|
debug_on_failure
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" =~ "Startup guard: refusing to start duplicate duplicate-server" ]]
|
||||||
|
[[ "$output" =~ "existing matching process" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "starter: should refuse occupied configured worldserver ports" {
|
||||||
|
if ! command -v nc >/dev/null 2>&1; then
|
||||||
|
skip "nc is required for this test"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$TEST_DIR/bin/worldserver" << 'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "worldserver should not start"
|
||||||
|
EOF
|
||||||
|
chmod +x "$TEST_DIR/bin/worldserver"
|
||||||
|
|
||||||
|
local listen_port=40123
|
||||||
|
cat > "$TEST_DIR/worldserver.conf" << EOF
|
||||||
|
WorldServerPort = $listen_port
|
||||||
|
SOAP.Enabled = 0
|
||||||
|
EOF
|
||||||
|
nc -l 127.0.0.1 "$listen_port" >/dev/null &
|
||||||
|
local listener_pid=$!
|
||||||
|
|
||||||
|
AC_STARTUP_GUARD_TIMEOUT=1 AC_STARTUP_GUARD_INTERVAL=1 run timeout 5s "$SCRIPT_DIR/starter" "$TEST_DIR/bin" "worldserver" "" "$TEST_DIR/worldserver.conf" "" "" 0
|
||||||
|
kill "$listener_pid" 2>/dev/null || true
|
||||||
|
wait "$listener_pid" 2>/dev/null || true
|
||||||
|
|
||||||
|
debug_on_failure
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" =~ "Startup guard: refusing to start duplicate worldserver" ]]
|
||||||
|
[[ "$output" =~ "configured port(s) still listening: $listen_port" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "starter: should parse config keys literally for startup guard" {
|
||||||
|
if ! command -v nc >/dev/null 2>&1; then
|
||||||
|
skip "nc is required for this test"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$TEST_DIR/bin/worldserver" << 'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "worldserver started"
|
||||||
|
EOF
|
||||||
|
chmod +x "$TEST_DIR/bin/worldserver"
|
||||||
|
|
||||||
|
local listen_port=40124
|
||||||
|
cat > "$TEST_DIR/worldserver.conf" << EOF
|
||||||
|
WorldServerPort = 40125
|
||||||
|
SOAPXEnabled = 1
|
||||||
|
SOAPXPort = $listen_port
|
||||||
|
SOAP.Enabled = 0
|
||||||
|
EOF
|
||||||
|
nc -l 127.0.0.1 "$listen_port" >/dev/null &
|
||||||
|
local listener_pid=$!
|
||||||
|
|
||||||
|
AC_STARTUP_GUARD_TIMEOUT=1 AC_STARTUP_GUARD_INTERVAL=1 run timeout 5s "$SCRIPT_DIR/starter" "$TEST_DIR/bin" "worldserver" "" "$TEST_DIR/worldserver.conf" "" "" 0
|
||||||
|
kill "$listener_pid" 2>/dev/null || true
|
||||||
|
wait "$listener_pid" 2>/dev/null || true
|
||||||
|
|
||||||
|
debug_on_failure
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" =~ "worldserver started" ]]
|
||||||
|
[[ ! "$output" =~ "Startup guard: refusing" ]]
|
||||||
|
}
|
||||||
|
|
||||||
# ===== SIMPLE RESTARTER TESTS =====
|
# ===== SIMPLE RESTARTER TESTS =====
|
||||||
|
|
||||||
@test "simple-restarter: should fail with missing parameters" {
|
@test "simple-restarter: should fail with missing parameters" {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue