summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/dashboard-ci.yml28
-rw-r--r--apps/startup-scripts/README.md26
-rwxr-xr-xapps/startup-scripts/src/service-manager.sh302
-rw-r--r--apps/startup-scripts/test/test_startup_scripts.bats124
4 files changed, 477 insertions, 3 deletions
diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml
index 8b22541fbd..5901d19447 100644
--- a/.github/workflows/dashboard-ci.yml
+++ b/.github/workflows/dashboard-ci.yml
@@ -95,6 +95,7 @@ jobs:
run: |
# This runs: install-deps, compile, database setup, client-data download
./acore.sh init
+ sudo npm install -g pm2
timeout-minutes: 120
- name: Test module removal
@@ -115,3 +116,30 @@ jobs:
cd env/dist/bin
timeout 5m ./worldserver -dry-run
continue-on-error: false
+
+
+ - name: Test worldserver with startup scripts
+ run: |
+ ./acore.sh sm create world worldserver --bin-path ./env/dist/bin --provider pm2
+ ./acore.sh sm show-config worldserver
+ ./acore.sh sm start worldserver
+ ./acore.sh sm wait-uptime worldserver 10 300
+ ./acore.sh sm send worldserver "account create tester password 3"
+ ./acore.sh sm send worldserver "account set gm tester 3"
+ ./acore.sh sm send worldserver "account set addon tester 1"
+ ./acore.sh sm wait-uptime worldserver 10 300
+ ./acore.sh sm stop worldserver
+ ./acore.sh sm delete worldserver
+ timeout-minutes: 30
+ continue-on-error: false
+
+ - name: Test authserver with startup scripts
+ run: |
+ ./acore.sh sm create auth authserver --bin-path ./env/dist/bin --provider pm2
+ ./acore.sh sm show-config authserver
+ ./acore.sh sm start authserver
+ ./acore.sh sm wait-uptime authserver 10 300
+ ./acore.sh sm stop authserver
+ ./acore.sh sm delete authserver
+ timeout-minutes: 30
+ continue-on-error: false
diff --git a/apps/startup-scripts/README.md b/apps/startup-scripts/README.md
index 176ec3ed6c..0c7cb0940d 100644
--- a/apps/startup-scripts/README.md
+++ b/apps/startup-scripts/README.md
@@ -305,6 +305,31 @@ Services support two restart policies:
./service-manager.sh delete auth
```
+#### Health and Console Commands
+
+Use these commands to programmatically check service health and interact with the console (used by CI workflows):
+
+```bash
+# Check if service is currently running (exit 0 if running)
+./service-manager.sh is-running world
+
+# Print current uptime in seconds (fails if not running)
+./service-manager.sh uptime-seconds world
+
+# Wait until uptime >= 10s (optional timeout 240s)
+./service-manager.sh wait-uptime world 10 240
+
+# Send a console command (uses pm2 send or tmux/screen)
+./service-manager.sh send world "server info"
+
+# Show provider, configs and run-engine settings
+./service-manager.sh show-config world
+```
+
+Notes:
+- For `send`, PM2 provider uses `pm2 send` with the process ID; systemd provider requires a session manager (tmux/screen). If no attachable session is configured, the command fails.
+- `wait-uptime` fails with a non-zero exit code if the service does not reach the requested uptime within the timeout window.
+
#### Service Configuration
```bash
# Update service settings
@@ -624,4 +649,3 @@ sudo npm install -g pm2
```
-
diff --git a/apps/startup-scripts/src/service-manager.sh b/apps/startup-scripts/src/service-manager.sh
index 34dc4c4d5a..ccc4e8e359 100755
--- a/apps/startup-scripts/src/service-manager.sh
+++ b/apps/startup-scripts/src/service-manager.sh
@@ -279,6 +279,11 @@ function print_help() {
echo " $base_name start|stop|restart|status <service-name>"
echo " $base_name logs <service-name> [--follow]"
echo " $base_name attach <service-name>"
+ echo " $base_name is-running <service-name> # exit 0 if running, 1 otherwise"
+ echo " $base_name uptime-seconds <service-name> # print uptime in seconds (fails if not running)"
+ echo " $base_name wait-uptime <service> <sec> [t] # wait until uptime >= seconds (timeout t, default 120)"
+ echo " $base_name send <service-name> <command...> # send console command to service"
+ echo " $base_name show-config <service-name> # print current service + run-engine config"
echo " $base_name edit-config <service-name>"
echo ""
echo "Providers:"
@@ -735,7 +740,7 @@ EOF
systemctl --user enable "$service_name.service"
fi
- echo -e "${GREEN}Systemd service '$service_name' created successfully${NC}"
+ echo -e "${GREEN}Systemd service '$service_name' created successfully with session manager '$session_manager'${NC}"
# Add to registry
add_service_to_registry "$service_name" "systemd" "service" "$command" "" "$systemd_type" "$restart_policy" "$session_manager" "$gdb_enabled" "" "$server_config"
@@ -1473,6 +1478,253 @@ function attach_to_service() {
fi
}
+#########################################
+# Runtime helpers: status / send / show #
+#########################################
+
+function service_is_running() {
+ local service_name="$1"
+
+ local service_info=$(get_service_info "$service_name")
+ if [ -z "$service_info" ]; then
+ echo -e "${RED}Error: Service '$service_name' not found${NC}" >&2
+ return 1
+ fi
+
+ local provider=$(echo "$service_info" | jq -r '.provider')
+
+ if [ "$provider" = "pm2" ]; then
+ # pm2 jlist -> JSON array with .name and .pm2_env.status
+ if pm2 jlist | jq -e ".[] | select(.name==\"$service_name\" and .pm2_env.status==\"online\")" >/dev/null; then
+ return 0
+ else
+ return 1
+ fi
+ elif [ "$provider" = "systemd" ]; then
+ # Check user service first, then system
+ if systemctl --user is-active --quiet "$service_name.service" 2>/dev/null; then
+ return 0
+ elif systemctl is-active --quiet "$service_name.service" 2>/dev/null; then
+ return 0
+ else
+ return 1
+ fi
+ else
+ return 1
+ fi
+}
+
+function service_send_command() {
+ local service_name="$1"; shift || true
+ local cmd_str="$*"
+ if [ -z "$service_name" ] || [ -z "$cmd_str" ]; then
+ echo -e "${RED}Error: send requires <service-name> and <command>${NC}" >&2
+ return 1
+ fi
+
+ local service_info=$(get_service_info "$service_name")
+ if [ -z "$service_info" ]; then
+ echo -e "${RED}Error: Service '$service_name' not found${NC}" >&2
+ return 1
+ fi
+
+ local provider=$(echo "$service_info" | jq -r '.provider')
+ local config_file="$CONFIG_DIR/$service_name.conf"
+
+ if [ ! -f "$config_file" ]; then
+ echo -e "${RED}Error: Service configuration file not found: $config_file${NC}" >&2
+ return 1
+ fi
+
+ # Load run-engine config path
+ # shellcheck source=/dev/null
+ source "$config_file"
+ if [ -z "${RUN_ENGINE_CONFIG_FILE:-}" ] || [ ! -f "$RUN_ENGINE_CONFIG_FILE" ]; then
+ echo -e "${RED}Error: Run-engine configuration file not found for $service_name${NC}" >&2
+ return 1
+ fi
+
+ # shellcheck source=/dev/null
+ if ! source "$RUN_ENGINE_CONFIG_FILE"; then
+ echo -e "${RED}Error: Failed to source run-engine configuration file: $RUN_ENGINE_CONFIG_FILE${NC}" >&2
+ return 1
+ fi
+
+ local session_manager="${SESSION_MANAGER:-auto}"
+ local session_name="${SESSION_NAME:-$service_name}"
+
+ if [ "$provider" = "pm2" ]; then
+ # Use pm2 send (requires pm2 >= 5)
+ local pm2_id_json
+ pm2_id_json=$(pm2 id "$service_name" 2>/dev/null || true)
+ local numeric_id
+ numeric_id=$(echo "$pm2_id_json" | jq -r '.[0] // empty')
+ if [ -z "$numeric_id" ]; then
+ echo -e "${RED}Error: PM2 process '$service_name' not found${NC}" >&2
+ return 1
+ fi
+ echo -e "${YELLOW}Sending to PM2 process $service_name (ID: $numeric_id): $cmd_str${NC}"
+ pm2 send "$numeric_id" "$cmd_str" ENTER
+ return $?
+ fi
+
+ # systemd provider: need a session manager to interact with the console
+ case "$session_manager" in
+ tmux|auto)
+ if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$session_name" 2>/dev/null; then
+ echo -e "${YELLOW}Sending to tmux session $session_name: $cmd_str${NC}"
+ tmux send-keys -t "$session_name" "$cmd_str" C-m
+ return $?
+ elif [ "$session_manager" = "tmux" ]; then
+ echo -e "${RED}Error: tmux session '$session_name' not available${NC}" >&2
+ return 1
+ fi
+ ;;&
+ screen|auto)
+ if command -v screen >/dev/null 2>&1; then
+ echo -e "${YELLOW}Sending to screen session $session_name: $cmd_str${NC}"
+ screen -S "$session_name" -X stuff "$cmd_str\n"
+ return $?
+ elif [ "$session_manager" = "screen" ]; then
+ echo -e "${RED}Error: screen not installed${NC}" >&2
+ return 1
+ fi
+ ;;
+ none|*)
+ echo -e "${RED}Error: No session manager configured (SESSION_MANAGER=$session_manager). Cannot send command.${NC}" >&2
+ return 1
+ ;;
+ esac
+
+ echo -e "${RED}Error: Unable to find usable session (tmux/screen) to send command.${NC}" >&2
+ return 1
+}
+
+function show_config() {
+ local service_name="$1"
+ if [ -z "$service_name" ]; then
+ echo -e "${RED}Error: Service name required for show-config${NC}"
+ return 1
+ fi
+
+ local service_info=$(get_service_info "$service_name")
+ if [ -z "$service_info" ]; then
+ echo -e "${RED}Error: Service '$service_name' not found${NC}"
+ return 1
+ fi
+
+ local provider=$(echo "$service_info" | jq -r '.provider')
+ local cfg_file="$CONFIG_DIR/$service_name.conf"
+ echo -e "${BLUE}Service: $service_name${NC}"
+ echo "Provider: $provider"
+ echo "Config file: $cfg_file"
+ if [ -f "$cfg_file" ]; then
+ # shellcheck source=/dev/null
+ source "$cfg_file"
+ echo "RUN_ENGINE_CONFIG_FILE: ${RUN_ENGINE_CONFIG_FILE:-<none>}"
+ if [ -n "${RUN_ENGINE_CONFIG_FILE:-}" ] && [ -f "$RUN_ENGINE_CONFIG_FILE" ]; then
+ # shellcheck source=/dev/null
+ source "$RUN_ENGINE_CONFIG_FILE"
+ echo "Session manager: ${SESSION_MANAGER:-}"
+ echo "Session name: ${SESSION_NAME:-}"
+ echo "BINPATH: ${BINPATH:-}"
+ echo "SERVERBIN: ${SERVERBIN:-}"
+ echo "CONFIG: ${CONFIG:-}"
+ echo "RESTART_POLICY: ${RESTART_POLICY:-}"
+ fi
+ else
+ echo "Config file not found"
+ fi
+}
+
+# Return uptime in seconds for a service (echo integer), non-zero exit if not running
+function service_uptime_seconds() {
+ local service_name="$1"
+ local service_info=$(get_service_info "$service_name")
+ if [ -z "$service_info" ]; then
+ echo -e "${RED}Error: Service '$service_name' not found${NC}" >&2
+ return 1
+ fi
+
+ local provider=$(echo "$service_info" | jq -r '.provider')
+
+ if [ "$provider" = "pm2" ]; then
+ check_pm2 || return 1
+ local info_json
+ info_json=$(pm2 jlist 2>/dev/null)
+ local pm_uptime_ms
+ pm_uptime_ms=$(echo "$info_json" | jq -r ".[] | select(.name==\"$service_name\").pm2_env.pm_uptime // empty")
+ local status
+ status=$(echo "$info_json" | jq -r ".[] | select(.name==\"$service_name\").pm2_env.status // empty")
+ if [ -z "$pm_uptime_ms" ] || [ "$status" != "online" ]; then
+ return 1
+ fi
+ # Current time in ms (fallback to seconds*1000 if %N unsupported)
+ local now_ms
+ if date +%s%N >/dev/null 2>&1; then
+ now_ms=$(( $(date +%s%N) / 1000000 ))
+ else
+ now_ms=$(( $(date +%s) * 1000 ))
+ fi
+ local diff_ms=$(( now_ms - pm_uptime_ms ))
+ [ "$diff_ms" -lt 0 ] && diff_ms=0
+ echo $(( diff_ms / 1000 ))
+ return 0
+ elif [ "$provider" = "systemd" ]; then
+ check_systemd || return 1
+ local systemd_type="--user"
+ [ -f "/etc/systemd/system/$service_name.service" ] && systemd_type="--system"
+
+ # Get ActiveEnterTimestampMonotonic in usec and ActiveState
+ local prop
+ if [ "$systemd_type" = "--system" ]; then
+ prop=$(systemctl show "$service_name.service" --property=ActiveEnterTimestampMonotonic,ActiveState 2>/dev/null)
+ else
+ prop=$(systemctl --user show "$service_name.service" --property=ActiveEnterTimestampMonotonic,ActiveState 2>/dev/null)
+ fi
+ local state
+ state=$(echo "$prop" | awk -F= '/^ActiveState=/{print $2}')
+ [ "$state" != "active" ] && return 1
+ local enter_us
+ enter_us=$(echo "$prop" | awk -F= '/^ActiveEnterTimestampMonotonic=/{print $2}')
+ # Current monotonic time in seconds since boot
+ local now_s
+ now_s=$(cut -d' ' -f1 /proc/uptime)
+ # Compute uptime = now_monotonic - enter_monotonic
+ # enter_us may be empty on some systems; fallback to 0
+ enter_us=${enter_us:-0}
+ # Convert now_s to microseconds using awk for precision, then compute diff
+ local diff_s
+ diff_s=$(awk -v now="$now_s" -v enter="$enter_us" 'BEGIN{printf "%d", (now*1000000 - enter)/1000000}')
+ [ "$diff_s" -lt 0 ] && diff_s=0
+ echo "$diff_s"
+ return 0
+ fi
+
+ return 1
+}
+
+# Wait until service has at least <min_seconds> uptime. Optional timeout seconds (default 120)
+function wait_service_uptime() {
+ local service_name="$1"
+ local min_seconds="$2"
+ local timeout="${3:-120}"
+ local waited=0
+
+ while [ "$waited" -le "$timeout" ]; do
+ if secs=$(service_uptime_seconds "$service_name" 2>/dev/null); then
+ if [ "$secs" -ge "$min_seconds" ]; then
+ echo -e "${GREEN}Service '$service_name' has reached ${secs}s uptime (required: ${min_seconds}s)${NC}"
+ return 0
+ fi
+ fi
+ sleep 1
+ waited=$((waited + 1))
+ done
+ echo -e "${RED}Timeout: $service_name did not reach ${min_seconds}s uptime within ${timeout}s${NC}" >&2
+ return 1
+}
+
function attach_pm2_process() {
local service_name="$1"
@@ -1629,7 +1881,7 @@ case "${1:-help}" in
delete_service "$2"
;;
list)
- list_services "$2"
+ list_services "${2:-}"
;;
restore)
restore_missing_services
@@ -1670,6 +1922,52 @@ case "${1:-help}" in
fi
attach_to_service "$2"
;;
+ uptime-seconds)
+ if [ $# -lt 2 ]; then
+ echo -e "${RED}Error: Service name required for uptime-seconds command${NC}"
+ print_help
+ exit 1
+ fi
+ service_uptime_seconds "$2"
+ ;;
+ wait-uptime)
+ if [ $# -lt 3 ]; then
+ echo -e "${RED}Error: Usage: $0 wait-uptime <service-name> <min-seconds> [timeout]${NC}"
+ print_help
+ exit 1
+ fi
+ wait_service_uptime "$2" "$3" "${4:-120}"
+ ;;
+ is-running)
+ if [ $# -lt 2 ]; then
+ echo -e "${RED}Error: Service name required for is-running command${NC}"
+ print_help
+ exit 1
+ fi
+ if service_is_running "$2"; then
+ echo -e "${GREEN}Service '$2' is running${NC}"
+ exit 0
+ else
+ echo -e "${YELLOW}Service '$2' is not running${NC}"
+ exit 1
+ fi
+ ;;
+ send)
+ if [ $# -lt 3 ]; then
+ echo -e "${RED}Error: Not enough arguments for send command${NC}"
+ print_help
+ exit 1
+ fi
+ service_send_command "$2" "${@:3}"
+ ;;
+ show-config)
+ if [ $# -lt 2 ]; then
+ echo -e "${RED}Error: Service name required for show-config command${NC}"
+ print_help
+ exit 1
+ fi
+ show_config "$2"
+ ;;
help|--help|-h)
print_help
;;
diff --git a/apps/startup-scripts/test/test_startup_scripts.bats b/apps/startup-scripts/test/test_startup_scripts.bats
index 2bbca2ffd3..119a8c80cc 100644
--- a/apps/startup-scripts/test/test_startup_scripts.bats
+++ b/apps/startup-scripts/test/test_startup_scripts.bats
@@ -143,6 +143,130 @@ teardown() {
[[ "$output" =~ "on-failure|always" ]]
}
+@test "service-manager: help lists health and console commands" {
+ run "$SCRIPT_DIR/service-manager.sh" help
+ [ "$status" -eq 0 ]
+ [[ "$output" =~ "is-running <service-name>" ]]
+ [[ "$output" =~ "uptime-seconds <service-name>" ]]
+ [[ "$output" =~ "wait-uptime <service> <sec>" ]]
+ [[ "$output" =~ "send <service-name>" ]]
+ [[ "$output" =~ "show-config <service-name>" ]]
+}
+
+@test "service-manager: pm2 uptime and wait-uptime work with mocked pm2" {
+ command -v jq >/dev/null 2>&1 || skip "jq not installed"
+ export AC_SERVICE_CONFIG_DIR="$TEST_DIR/services"
+ mkdir -p "$AC_SERVICE_CONFIG_DIR"
+ # Create registry with pm2 provider service
+ cat > "$AC_SERVICE_CONFIG_DIR/service_registry.json" << 'EOF'
+[
+ {"name":"test-world","provider":"pm2","type":"service","bin_path":"/bin/worldserver","args":"","systemd_type":"--user","restart_policy":"always"}
+]
+EOF
+ # Create minimal service config and run-engine config files required by 'send'
+ echo "RUN_ENGINE_CONFIG_FILE=\"$AC_SERVICE_CONFIG_DIR/test-world-run-engine.conf\"" > "$AC_SERVICE_CONFIG_DIR/test-world.conf"
+ cat > "$AC_SERVICE_CONFIG_DIR/test-world-run-engine.conf" << 'EOF'
+export SESSION_MANAGER="none"
+export SESSION_NAME="test-world"
+EOF
+ # Mock pm2
+ cat > "$TEST_DIR/bin/pm2" << 'EOF'
+#!/usr/bin/env bash
+case "$1" in
+ jlist)
+ # Produce a JSON with uptime ~20 seconds
+ if date +%s%N >/dev/null 2>&1; then
+ nowms=$(( $(date +%s%N) / 1000000 ))
+ else
+ nowms=$(( $(date +%s) * 1000 ))
+ fi
+ up=$(( nowms - 20000 ))
+ echo "[{\"name\":\"test-world\",\"pm2_env\":{\"status\":\"online\",\"pm_uptime\":$up}}]"
+ ;;
+ id)
+ echo "[1]"
+ ;;
+ attach|send|list|describe|logs)
+ exit 0
+ ;;
+ *)
+ exit 0
+ ;;
+esac
+EOF
+ chmod +x "$TEST_DIR/bin/pm2"
+
+ run "$SCRIPT_DIR/service-manager.sh" uptime-seconds test-world
+ debug_on_failure
+ [ "$status" -eq 0 ]
+ # Output should be a number >= 10
+ [[ "$output" =~ ^[0-9]+$ ]]
+ [ "$output" -ge 10 ]
+
+ run "$SCRIPT_DIR/service-manager.sh" wait-uptime test-world 10 5
+ debug_on_failure
+ [ "$status" -eq 0 ]
+}
+
+@test "service-manager: send works under pm2 with mocked pm2" {
+ command -v jq >/dev/null 2>&1 || skip "jq not installed"
+ export AC_SERVICE_CONFIG_DIR="$TEST_DIR/services"
+ mkdir -p "$AC_SERVICE_CONFIG_DIR"
+ # Create registry and config as in previous test
+ cat > "$AC_SERVICE_CONFIG_DIR/service_registry.json" << 'EOF'
+[
+ {"name":"test-world","provider":"pm2","type":"service","bin_path":"/bin/worldserver","args":"","systemd_type":"--user","restart_policy":"always"}
+]
+EOF
+ echo "RUN_ENGINE_CONFIG_FILE=\"$AC_SERVICE_CONFIG_DIR/test-world-run-engine.conf\"" > "$AC_SERVICE_CONFIG_DIR/test-world.conf"
+ cat > "$AC_SERVICE_CONFIG_DIR/test-world-run-engine.conf" << 'EOF'
+export SESSION_MANAGER="none"
+export SESSION_NAME="test-world"
+EOF
+ # pm2 mock
+ cat > "$TEST_DIR/bin/pm2" << 'EOF'
+#!/usr/bin/env bash
+case "$1" in
+ jlist)
+ if date +%s%N >/dev/null 2>&1; then
+ nowms=$(( $(date +%s%N) / 1000000 ))
+ else
+ nowms=$(( $(date +%s) * 1000 ))
+ fi
+ up=$(( nowms - 15000 ))
+ echo "[{\"name\":\"test-world\",\"pm2_env\":{\"status\":\"online\",\"pm_uptime\":$up}}]"
+ ;;
+ id)
+ echo "[1]"
+ ;;
+ send)
+ # simulate success
+ exit 0
+ ;;
+ attach|list|describe|logs)
+ exit 0
+ ;;
+ *)
+ exit 0
+ ;;
+esac
+EOF
+ chmod +x "$TEST_DIR/bin/pm2"
+
+ run "$SCRIPT_DIR/service-manager.sh" send test-world "server info"
+ debug_on_failure
+ [ "$status" -eq 0 ]
+}
+
+@test "service-manager: wait-uptime times out for unknown service" {
+ command -v jq >/dev/null 2>&1 || skip "jq not installed"
+ export AC_SERVICE_CONFIG_DIR="$TEST_DIR/services"
+ mkdir -p "$AC_SERVICE_CONFIG_DIR"
+ echo "[]" > "$AC_SERVICE_CONFIG_DIR/service_registry.json"
+ run "$SCRIPT_DIR/service-manager.sh" wait-uptime unknown 2 1
+ [ "$status" -ne 0 ]
+}
+
# ===== EXAMPLE SCRIPTS TESTS =====
@test "examples: restarter-world should show configuration error" {