#!/usr/bin/env bash # AzerothCore Starter Script # This script handles the execution of AzerothCore binaries with optional GDB support # # Usage: starter [gdb_file] [config] [syslog] [syserr] [gdb_enabled] [crashes_path] # # Parameters: # $1 - Binary path (required) # $2 - Binary file name (required) # $3 - GDB configuration file (optional) # $4 - Configuration file path (optional) # $5 - System log file (optional) # $6 - System error file (optional) # $7 - GDB enabled flag (0/1, optional) # $8 - Crashes directory path (optional) BINPATH="$1" BINFILE="$2" GDB_FILE="$3" CONFIG="$4" SYSLOG="$5" SYSERR="$6" GDB_ENABLED="${7:-0}" CRASHES_PATH="$8" # Default values CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEFAULT_CRASHES_PATH=$(realpath "$BINPATH/crashes") [ -n "$CONFIG" ] && CONFIG_ABS=$(realpath "$CONFIG") # Set defaults if not provided CRASHES_PATH="${CRASHES_PATH:-$DEFAULT_CRASHES_PATH}" # Validate binary if [ -z "$BINPATH" ] || [ -z "$BINFILE" ]; then echo "Error: Binary path and file are required" echo "Usage: $0 [gdb_file] [config] [syslog] [syserr] [gdb_enabled] [crashes_path]" exit 1 fi BINARY="$BINPATH/$BINFILE" if [ ! -f "$BINARY" ]; then echo "Error: Binary '$BINARY' not found" exit 1 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 mkdir -p "$CRASHES_PATH" cd "$BINPATH" || { echo "Error: Could not change to binary path '$BINPATH'" exit 1 } EXECPATH=$(realpath "$BINFILE") wait_for_startup_guard if [ "$GDB_ENABLED" -eq 1 ]; then echo "Starting $EXECPATH with GDB enabled" # Generate GDB configuration on the fly TIMESTAMP=$(date +%Y-%m-%d-%H-%M-%S) GDB_TEMP_FILE="$CRASHES_PATH/gdb-$TIMESTAMP.conf" GDB_OUTPUT_FILE="$CRASHES_PATH/gdb-$TIMESTAMP.txt" # Create GDB configuration file if it is not defined if [ -z "$GDB_FILE" ]; then # Create GDB configuration cat > "$GDB_TEMP_FILE" << EOF set logging file $GDB_OUTPUT_FILE set logging enabled on set debug timestamp EOF # Add run command with config if specified if [ -n "$CONFIG_ABS" ]; then echo "run -c $CONFIG_ABS" >> "$GDB_TEMP_FILE" else echo "run" >> "$GDB_TEMP_FILE" fi cat >> "$GDB_TEMP_FILE" << EOF bt bt full info thread thread apply all backtrace full EOF GDB_FILE="$GDB_TEMP_FILE" fi # Create log files if specified if [ -n "$SYSLOG" ]; then [ ! -f "$SYSLOG" ] && touch "$SYSLOG" fi if [ -n "$SYSERR" ]; then [ ! -f "$SYSERR" ] && touch "$SYSERR" fi # Execute with GDB if [ "${WITH_CONSOLE:-0}" -eq 0 ] && [ -n "$SYSLOG" ] && [ -n "$SYSERR" ]; then gdb -x "$GDB_FILE" --batch "$EXECPATH" >> "$SYSLOG" 2>> "$SYSERR" else echo "> Console enabled" if [ -n "$SYSLOG" ] && [ -n "$SYSERR" ]; then gdb -x "$GDB_FILE" --batch "$EXECPATH" > >(tee "$SYSLOG") 2> >(tee "$SYSERR" >&2) else gdb -x "$GDB_FILE" --batch "$EXECPATH" fi fi # clean up temporary GDB file if it exists if [ -n "$GDB_TEMP_FILE" ]; then # Clean up temporary GDB file rm -f "$GDB_TEMP_FILE" fi else echo "Starting $BINFILE without GDB" # Determine if PM2 is active is_pm2_active="0" [ "$AC_LAUNCHED_BY_PM2" == "1" ] && is_pm2_active="1" # Determine if interactive mode is enabled is_interactive_enabled="1" [ "$AC_DISABLE_INTERACTIVE" == "1" ] && is_interactive_enabled="0" # use normal execution if we are running the binary under PM2 # or when interactive mode is enabled if [[ "$is_pm2_active" == "1" || "$is_interactive_enabled" == "1" ]]; then echo "Running AC" "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"} else # When AC_DISABLE_INTERACTIVE is set to 1 and we are not in PM2 # This means we are using systemd without interactive mode and no session managers # in this case we need to run AC with unbuffer for line-buffered output # NOTE unbuffer doesn't fully support interactive mode if command -v unbuffer >/dev/null 2>&1; then echo "Running AC with unbuffer for line-buffered output" unbuffer "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"} else echo "⚠️ unbuffer not found, the output may not be line-buffered. Try installing expect." exec "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"} fi fi fi