feat: improve session management and PM2 support in startup scripts (#22420)

New feature to manage service restart policies and refactors crash logging paths for better flexibility and clarity. The most significant changes include adding support for configurable restart policies (`on-failure` and `always`), updating documentation to reflect these changes, and improving crash path handling in multiple scripts.
This commit is contained in:
Yehonal
2025-07-06 12:00:38 +02:00
committed by GitHub
parent 9a837ee1f7
commit 9fcacf7ea7
10 changed files with 150 additions and 57 deletions

View File

@@ -27,7 +27,7 @@ $SUDO apt-get install -y gdbserver gdb unzip curl \
libncurses-dev libreadline-dev clang g++ \
gcc git cmake make ccache \
libssl-dev libbz2-dev \
libboost-all-dev gnupg wget jq screen tmux
libboost-all-dev gnupg wget jq screen tmux expect
VAR_PATH="$CURRENT_PATH/../../../../var"

View File

@@ -31,4 +31,4 @@ if ! command -v cmake &>/dev/null ; then
fi
##########################################
brew install openssl@3 readline boost bash-completion curl unzip mysql ccache
brew install openssl@3 readline boost bash-completion curl unzip mysql ccache expect tmux screen jq

View File

@@ -32,7 +32,7 @@ $SUDO apt update
DEBIAN_FRONTEND="noninteractive" $SUDO \
apt-get -y install ccache clang cmake curl google-perftools libmysqlclient-dev make unzip jq screen tmux \
libreadline-dev libncurses5-dev libncursesw5-dev libbz2-dev git gcc g++ libssl-dev \
libncurses-dev libboost-all-dev gdb gdbserver
libncurses-dev libboost-all-dev gdb gdbserver expect
VAR_PATH="$CURRENT_PATH/../../../../var"

View File

@@ -257,6 +257,29 @@ Production-ready service management:
# Force systemd
./service-manager.sh create world worldserver --provider systemd --bin-path /path/to/bin
# Create service with restart policy
./service-manager.sh create world worldserver --bin-path /path/to/bin --restart-policy always
```
#### Restart Policies
Services support two restart policies:
- **`on-failure`** (default): Restart only on crashes or errors (exit code != 0, only works with PM2 or systemd without tmux/screen)
- **`always`**: Restart on any exit, including clean shutdown (exit code 0)
**Important**: When using `--restart-policy always`, the in-game command `server shutdown X` will behave like `server restart X` - the service will automatically restart after shutdown. Only the shutdown message differs from a restart message.
```bash
# Service that restarts only on crashes (default behavior)
./service-manager.sh create auth authserver --bin-path /path/to/bin --restart-policy on-failure
# Service that always restarts (even on manual shutdown)
./service-manager.sh create world worldserver --bin-path /path/to/bin --restart-policy always
# Update existing service restart policy
./service-manager.sh update worldserver --restart-policy always
```
#### Service Operations
@@ -296,19 +319,22 @@ Production-ready service management:
### Method 1: Using Service Manager (Recommended)
```bash
# Create multiple world server instances
# Create multiple world server instances with different restart policies
./service-manager.sh create world1 worldserver \
--bin-path /path/to/bin \
--server-config /path/to/worldserver-realm1.conf
--server-config /path/to/worldserver-realm1.conf \
--restart-policy on-failure
./service-manager.sh create world2 worldserver \
--bin-path /path/to/bin \
--server-config /path/to/worldserver-realm2.conf
--server-config /path/to/worldserver-realm2.conf \
--restart-policy always
# Single auth server for all realms
# Single auth server for all realms (always restart for stability)
./service-manager.sh create auth authserver \
--bin-path /path/to/bin \
--server-config /path/to/authserver.conf
--server-config /path/to/authserver.conf \
--restart-policy always
```
### Method 2: Using Run Engine with Different Configurations
@@ -384,7 +410,7 @@ The startup scripts recognize several environment variables for configuration an
#### Service Detection Variables
- **`AC_LAUNCHED_BY_PM2`**: Set to `1` when launched by PM2 (automatically set by service-manager)
- Disables the use of the `script` command for output capture
- Disables the use of the `unbuffer` command for output capture
- Enables non-interactive mode to prevent prompts
- More robust than relying on PM2's internal variables

View File

@@ -51,7 +51,7 @@ export SCREEN_OPTIONS="${RUN_ENGINE_SCREEN_OPTIONS:-}"
# If disabled, output will be redirected to logging files
export WITH_CONSOLE="${RUN_ENGINE_WITH_CONSOLE:-0}"
# Server PID (needed when GDB_ENABLED=1)
export SERVERPID="${RUN_ENGINE_SERVERPID:-}"
# Restart policy (on-failure|always)
export RESTART_POLICY="always"

View File

@@ -279,8 +279,9 @@ function start_service() {
# Set up directories and logging relative to BINPATH
LOGS_PATH="${LOGS_PATH:-"$BINPATH/logs"}"
CRASHES_PATH="${CRASHES_PATH:-"$BINPATH/crashes"}"
mkdir -p "$LOGS_PATH"
mkdir -p "$LOGS_PATH/crashes"
mkdir -p "$CRASHES_PATH"
else
# For system binaries, try to detect binary location and create logs accordingly
local detected_binpath=""
@@ -297,12 +298,13 @@ function start_service() {
# Set up log paths based on detected or fallback location
if [ -n "$detected_binpath" ]; then
LOGS_PATH="${LOGS_PATH:-"$detected_binpath/logs"}"
CRASHES_PATH="${CRASHES_PATH:-"$detected_binpath/crashes"}"
else
# Fallback to current directory for logs
LOGS_PATH="${LOGS_PATH:-./logs}"
CRASHES_PATH="${CRASHES_PATH:-"$./crashes"}"
fi
CRASHES_PATH="${CRASHES_PATH:-"$LOGS_PATH/crashes"}"
mkdir -p "$LOGS_PATH"
mkdir -p "$CRASHES_PATH"

View File

@@ -97,7 +97,7 @@ function print_help() {
echo ""
echo "Options:"
echo " --provider <type> - Service provider (pm2|systemd|auto, default: auto)"
echo " --bin-path <path> - Path to the server binary directory (required)"
echo " --bin-path <path> - Path to the server binary directory"
echo " --server-config <path> - Path to the server configuration file"
echo " --session-manager <type> - Session manager (none|tmux|screen, default: none)"
echo " Note: PM2 doesn't support tmux/screen, always uses 'none'"
@@ -106,6 +106,9 @@ function print_help() {
echo " --user - Create as user service (systemd only, default)"
echo " --max-memory <value> - Maximum memory limit (PM2 only)"
echo " --max-restarts <value> - Maximum restart attempts (PM2 only)"
echo " --restart-policy <policy> - Restart policy (on-failure|always, default: always)"
echo " on-failure: restart only on crash/error (only works with PM2 or systemd without tmux/screen)"
echo " always: restart on any exit (including 'server shutdown')"
echo " --no-start - Do not start the service after creation"
echo ""
echo "Examples:"
@@ -124,6 +127,9 @@ function print_help() {
echo " # Create service without starting it"
echo " $base_name create auth authserver --bin-path /home/user/azerothcore/bin --no-start"
echo ""
echo " # Create service with always restart policy"
echo " $base_name create world worldserver --bin-path /home/user/azerothcore/bin --restart-policy always"
echo ""
echo " # Update run-engine configuration"
echo " $base_name update worldserver-realm1 --session-manager screen --gdb-enabled 0"
echo ""
@@ -138,6 +144,8 @@ function print_help() {
echo " - Use --server-config for the actual server configuration file"
echo " - Services use run-engine in 'start' mode for single-shot execution"
echo " - Restart on crash is handled by PM2 or systemd, not by run-engine"
echo " - When restart-policy is 'always': 'server shutdown X' behaves like 'server restart X'"
echo " (only the in-game message differs, but the service will restart automatically)"
echo " - PM2 services always use session-manager 'none' and have built-in attach functionality"
echo " - attach command automatically detects the configured session manager and connects appropriately"
echo " - attach always provides interactive access to the server console"
@@ -253,7 +261,8 @@ function get_service_info() {
function pm2_create_service() {
local service_name="$1"
local command="$2"
shift 2
local restart_policy="$3"
shift 3
check_pm2 || return 1
@@ -279,8 +288,18 @@ function pm2_create_service() {
esac
done
# Set stop exit codes based on restart policy
local stop_exit_codes=""
if [ "$restart_policy" = "always" ]; then
# PM2 will restart on any exit code (including 0)
stop_exit_codes=""
else
# PM2 will not restart on clean shutdown (exit code 0)
stop_exit_codes=" --stop-exit-codes 0"
fi
# Build PM2 start command with AzerothCore environment variable
local pm2_cmd="AC_LAUNCHED_BY_PM2=1 pm2 start '$command$additional_args' --name '$service_name'"
local pm2_cmd="AC_LAUNCHED_BY_PM2=1 pm2 start '$command$additional_args' --name '$service_name'$stop_exit_codes"
# Add memory limit if specified
if [ -n "$max_memory" ]; then
@@ -382,8 +401,9 @@ function get_systemd_dir() {
function systemd_create_service() {
local service_name="$1"
local command="$2"
local restart_policy="$3"
local systemd_type="--user"
shift 2
shift 3
check_systemd || return 1
@@ -430,14 +450,8 @@ function systemd_create_service() {
session_name=$(grep -oP 'SESSION_NAME="\K[^"]+' "$run_engine_config_path" || echo "$service_name")
fi
if [ "$session_manager" = "tmux" ]; then
if [ "$session_manager" = "tmux" ] || [ "$session_manager" = "screen" ]; then
service_type="forking"
# Provide a direct and absolute path for the ExecStop command
exec_stop="ExecStop=/usr/bin/tmux kill-session -t $session_name"
elif [ "$session_manager" = "screen" ]; then
service_type="forking"
# Provide a direct and absolute path for the ExecStop command
exec_stop="ExecStop=/usr/bin/screen -S $session_name -X quit"
fi
# Create service file
@@ -453,8 +467,7 @@ After=network.target
[Service]
Type=${service_type}
ExecStart=$command
${exec_stop}
Restart=always
Restart=$restart_policy
RestartSec=3
User=$(whoami)
Group=$(id -gn)
@@ -475,8 +488,7 @@ After=network.target
[Service]
Type=${service_type}
ExecStart=$command
${exec_stop}
Restart=always
Restart=$restart_policy
RestartSec=3
WorkingDirectory=$(realpath "$bin_path")
StandardOutput=journal+console
@@ -651,6 +663,7 @@ function create_service() {
local server_config=""
local session_manager="none"
local gdb_enabled="0"
local restart_policy="always"
local systemd_type="--user"
local pm2_opts=""
local auto_start="true"
@@ -678,6 +691,10 @@ function create_service() {
gdb_enabled="$2"
shift 2
;;
--restart-policy)
restart_policy="$2"
shift 2
;;
--system)
systemd_type="--system"
shift
@@ -714,7 +731,13 @@ function create_service() {
echo -e "${RED}Error: Invalid provider. Use 'pm2', 'systemd', or 'auto'${NC}"
return 1
fi
# Validate restart policy
if [[ "$restart_policy" != "on-failure" && "$restart_policy" != "always" ]]; then
echo -e "${RED}Error: Invalid restart policy. Use 'on-failure' or 'always'${NC}"
return 1
fi
# PM2 specific validation and adjustments
if [ "$provider" = "pm2" ]; then
# PM2 doesn't support session managers (tmux/screen), force to none
@@ -728,11 +751,14 @@ function create_service() {
# Determine server binary based on service type
local server_bin="${service_type}server"
local server_binary_path=$(realpath "$bin_path/$server_bin")
local real_config_path=$(realpath "$server_config")
local real_config_path=""
if [ -n "$server_config" ]; then
real_config_path=$(realpath "$server_config")
fi
# Check if binary exists
if [ ! -f "$server_binary_path" ]; then
echo -e "${RED}Error: Server binary not found: $server_binary_path${NC}"
echo -e "${RED}Error: Server binary not found: $server_binary_path, please check your --bin-path option ${NC}"
return 1
fi
@@ -748,6 +774,9 @@ export GDB_ENABLED=$gdb_enabled
# Session manager (none|auto|tmux|screen)
export SESSION_MANAGER="$session_manager"
# Restart policy (on-failure|always)
export RESTART_POLICY="$restart_policy"
# Service mode - indicates this is running under a service manager (systemd/pm2)
# When true, AC_DISABLE_INTERACTIVE will be set if no interactive session manager is used
export SERVICE_MODE="true"
@@ -778,6 +807,9 @@ EOF
# run-engine configuration file
RUN_ENGINE_CONFIG_FILE="$run_engine_config"
# Restart policy
RESTART_POLICY="$restart_policy"
# Provider-specific options
SYSTEMD_TYPE="$systemd_type"
PM2_OPTS="$pm2_opts"
@@ -790,17 +822,17 @@ EOF
local service_creation_success=false
if [ "$provider" = "pm2" ]; then
if [ -n "$pm2_opts" ]; then
if pm2_create_service "$service_name" "$run_engine_cmd" $pm2_opts; then
if pm2_create_service "$service_name" "$run_engine_cmd" "$restart_policy" $pm2_opts; then
service_creation_success=true
fi
else
if pm2_create_service "$service_name" "$run_engine_cmd"; then
if pm2_create_service "$service_name" "$run_engine_cmd" "$restart_policy"; then
service_creation_success=true
fi
fi
elif [ "$provider" = "systemd" ]; then
if systemd_create_service "$service_name" "$run_engine_cmd" "$systemd_type"; then
if systemd_create_service "$service_name" "$run_engine_cmd" "$restart_policy" "$systemd_type"; then
service_creation_success=true
fi
fi
@@ -882,6 +914,11 @@ function update_service() {
config_updated=true
shift 2
;;
--restart-policy)
export RESTART_POLICY="$2"
config_updated=true
shift 2
;;
--system)
SYSTEMD_TYPE="--system"
shift
@@ -908,7 +945,13 @@ function update_service() {
export SESSION_MANAGER="none"
config_updated=true
fi
# Validate restart policy if provided
if [ -n "$RESTART_POLICY" ] && [[ "$RESTART_POLICY" != "on-failure" && "$RESTART_POLICY" != "always" ]]; then
echo -e "${RED}Error: Invalid restart policy. Use 'on-failure' or 'always'${NC}"
return 1
fi
if [ "$config_updated" = "true" ]; then
# Update run-engine configuration file
cat > "$RUN_ENGINE_CONFIG_FILE" << EOF
@@ -921,6 +964,9 @@ export GDB_ENABLED=${GDB_ENABLED:-0}
# Session manager (none|auto|tmux|screen)
export SESSION_MANAGER="${SESSION_MANAGER:-none}"
# Restart policy (on-failure|always)
export RESTART_POLICY="${RESTART_POLICY:-on-failure}"
# Service mode - indicates this is running under a service manager (systemd/pm2)
export SERVICE_MODE="true"
@@ -953,6 +999,9 @@ EOF
# run-engine configuration file
RUN_ENGINE_CONFIG_FILE="$RUN_ENGINE_CONFIG_FILE"
# Restart policy
RESTART_POLICY="${RESTART_POLICY:-on-failure}"
# Provider-specific options
SYSTEMD_TYPE="$SYSTEMD_TYPE"
PM2_OPTS="$PM2_OPTS"
@@ -1261,9 +1310,7 @@ function attach_tmux_session() {
else
echo -e "${RED}Error: tmux session '$tmux_session' not found${NC}"
echo -e "${YELLOW}Available tmux sessions:${NC}"
tmux list-sessions 2>/dev/null || echo "No active tmux sessions"
echo -e "${BLUE}Starting new interactive session instead...${NC}"
attach_interactive_shell "$service_name" "$provider"
tmux list-sessions 2>/dev/null || echo "No active tmux sessions (is it stopped or restarting?)"
fi
}
@@ -1289,9 +1336,7 @@ function attach_screen_session() {
else
echo -e "${RED}Error: screen session '$screen_session' not found${NC}"
echo -e "${YELLOW}Available screen sessions:${NC}"
screen -list 2>/dev/null || echo "No active screen sessions"
echo -e "${BLUE}Starting new interactive session instead...${NC}"
attach_interactive_shell "$service_name" "$provider"
screen -list 2>/dev/null || echo "No active screen sessions (is it stopped or restarting?)"
fi
}

View File

@@ -31,11 +31,9 @@ CRASHES_PATH="$8"
BINARY="$BINPATH/$BINFILE"
# Default values (same as starter)
DEFAULT_CRASHES_PATH="./crashes"
DEFAULT_GDB_FILE="$CURRENT_PATH/gdb.conf"
# Set defaults if not provided
CRASHES_PATH="${CRASHES_PATH:-$DEFAULT_CRASHES_PATH}"
GDB_FILE="${GDB_FILE:-$DEFAULT_GDB_FILE}"
# Counters for crash detection

View File

@@ -26,7 +26,7 @@ CRASHES_PATH="$8"
# Default values
CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_CRASHES_PATH="$CURRENT_PATH/logs/crashes"
DEFAULT_CRASHES_PATH=$(realpath "$BINPATH/crashes")
[ -n "$CONFIG" ] && CONFIG_ABS=$(realpath "$CONFIG")
# Set defaults if not provided
@@ -121,20 +121,17 @@ EOF
rm -f "$GDB_TEMP_FILE"
fi
else
# check if it's running under PM2 or `script` command does not exist
# Note: script is used to capture output in non-interactive sessions such as systemd (without tmux/screen)
# We use AC_LAUNCHED_BY_PM2 environment variable set by service-manager for robust PM2 detection
if [[ "$AC_LAUNCHED_BY_PM2" == "1" || ! -x "$(command -v script)" ]]; then
if [ -n "$CONFIG_ABS" ]; then
"$EXECPATH" -c "$CONFIG_ABS"
else
"$EXECPATH"
fi
echo "Starting $BINFILE without GDB"
if [[ "$AC_LAUNCHED_BY_PM2" == "1" ]]; then
echo "Running under PM2"
"$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"}
else
if [ -n "$CONFIG_ABS" ]; then
script -q -e -c "$EXECPATH -c \"$CONFIG_ABS\""
if command -v unbuffer >/dev/null 2>&1; then
export AC_DISABLE_INTERACTIVE=0
unbuffer "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"}
else
script -q -e -c "$EXECPATH"
echo "⚠️ unbuffer not found, the output may not be line-buffered. Try installing expect."
exec "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"}
fi
fi
fi
fi

View File

@@ -118,6 +118,31 @@ teardown() {
[[ "$output" =~ "Missing required arguments" ]] || [[ "$output" =~ "Error:" ]]
}
@test "service-manager: should validate restart policy values" {
run "$SCRIPT_DIR/service-manager.sh" create auth test-auth --bin-path /nonexistent --restart-policy invalid
[ "$status" -ne 0 ]
[[ "$output" =~ "Invalid restart policy" ]]
}
@test "service-manager: should accept valid restart policy values" {
# Test on-failure (should be accepted)
run "$SCRIPT_DIR/service-manager.sh" create auth test-auth --bin-path /nonexistent --restart-policy on-failure
# Should fail due to missing binary, not restart policy validation
[[ ! "$output" =~ "Invalid restart policy" ]]
# Test always (should be accepted)
run "$SCRIPT_DIR/service-manager.sh" create auth test-auth2 --bin-path /nonexistent --restart-policy always
# Should fail due to missing binary, not restart policy validation
[[ ! "$output" =~ "Invalid restart policy" ]]
}
@test "service-manager: should include restart policy in help output" {
run "$SCRIPT_DIR/service-manager.sh" help
[ "$status" -eq 0 ]
[[ "$output" =~ "--restart-policy" ]]
[[ "$output" =~ "on-failure|always" ]]
}
# ===== EXAMPLE SCRIPTS TESTS =====
@test "examples: restarter-world should show configuration error" {