#!/bin/sh # 2026年5月7日04点25分 # 项目地址:https://git.a-hxin.cn/ahxin/ets2-server/rss/branch/master/server/server.sh # ============================== # ETS2 Dedicated Server 管理脚本 # ============================== # 服务器目录 SERVER_HOME="/home/steam/ets2_sv/bin/linux_x64" # 官方启动脚本 SERVER_LAUNCH="$SERVER_HOME/server_launch.sh" # Steam 运行库目录 STEAM_PATH="/home/steam/ets2_sv/linux64" # SteamCMD 目录 STEAMCMD_HOME="/home/steam/steamcmd" # ETS2 服务端安装目录 ETS2_INSTALL_DIR="/home/steam/ets2_sv" # ETS2 Dedicated Server AppID ETS2_APP_ID="1948160" # 欧卡文档目录 # 实际读取目录: # /home/steam/ets2_doc/Euro Truck Simulator 2 export XDG_DATA_HOME="/home/steam/ets2_doc" # 设置 LD_LIBRARY_PATH export LD_LIBRARY_PATH="$STEAM_PATH:$SERVER_HOME:${LD_LIBRARY_PATH:-}" # 启动参数 SERVER_OPTIONS="-nosingle -server server_packages.sii -server_cfg server_config.sii" # 实际配置目录 ETS2_DOC_HOME="$XDG_DATA_HOME/Euro Truck Simulator 2" # 日志根目录 LOG_ROOT="$SERVER_HOME/logs" # PID 文件 PID_FILE="$LOG_ROOT/server.pid" # 后台启动器 PID 文件 RUNNER_PID_FILE="$LOG_ROOT/runner.pid" # 进程组 ID 文件 PGID_FILE="$LOG_ROOT/server.pgid" # 最新日志软链接 LATEST_LOG_LINK="$LOG_ROOT/latest.log" # 当前日志路径记录 CURRENT_LOG_PATH_FILE="$LOG_ROOT/current_log.path" # 日志保留天数 LOG_KEEP_DAYS=15 # 当前运行日志文件 SERVER_LOG="" show_info() { echo "[INFO] $1" } show_ok() { echo "[OK] $1" } show_warn() { echo "[WARN] $1" } show_error() { echo "[ERROR] $1" } prepare_dirs() { mkdir -p "$LOG_ROOT" mkdir -p "$ETS2_DOC_HOME" } create_log_file() { LOG_DAY=$(date +"%Y-%m-%d") LOG_TIME=$(date +"%Y%m%d_%H%M%S") LOG_DIR="$LOG_ROOT/$LOG_DAY" mkdir -p "$LOG_DIR" SERVER_LOG="$LOG_DIR/server_$LOG_TIME.log" touch "$SERVER_LOG" ln -sfn "$SERVER_LOG" "$LATEST_LOG_LINK" echo "$SERVER_LOG" > "$CURRENT_LOG_PATH_FILE" show_info "本次日志文件: $SERVER_LOG" } cleanup_old_logs() { if [ -d "$LOG_ROOT" ]; then find "$LOG_ROOT" -mindepth 1 -maxdepth 1 -type d -mtime +"$LOG_KEEP_DAYS" -exec rm -rf {} \; 2>/dev/null fi } check_required_files() { echo "========== ETS2 配置文件检查 ==========" echo "当前用户: $(whoami)" echo "SERVER_HOME: $SERVER_HOME" echo "SERVER_LAUNCH: $SERVER_LAUNCH" echo "STEAM_PATH: $STEAM_PATH" echo "XDG_DATA_HOME: $XDG_DATA_HOME" echo "ETS2_DOC_HOME: $ETS2_DOC_HOME" echo "LD_LIBRARY_PATH: $LD_LIBRARY_PATH" echo "SERVER_OPTIONS: $SERVER_OPTIONS" echo "" if [ -x "$SERVER_LAUNCH" ]; then echo "[OK] server_launch.sh 存在并可执行" else echo "[ERROR] server_launch.sh 不存在或不可执行" fi if [ -f "$ETS2_DOC_HOME/server_config.sii" ]; then echo "[OK] server_config.sii 存在" else echo "[WARN] 缺少 server_config.sii" fi if [ -f "$ETS2_DOC_HOME/server_packages.sii" ]; then echo "[OK] server_packages.sii 存在" else echo "[ERROR] 缺少 server_packages.sii" fi if [ -f "$ETS2_DOC_HOME/server_packages.dat" ]; then echo "[OK] server_packages.dat 存在" else echo "[ERROR] 缺少 server_packages.dat" fi echo "" echo "当前配置目录文件:" ls -lah "$ETS2_DOC_HOME" 2>/dev/null echo "=======================================" } get_server_pid() { pgrep -f "eurotrucks2_server" | head -n 1 } get_launch_pid() { pgrep -f "server_launch.sh" | head -n 1 } get_config_port() { KEY="$1" CONFIG_FILE="$ETS2_DOC_HOME/server_config.sii" if [ ! -f "$CONFIG_FILE" ]; then return fi grep -E "^[[:space:]]*$KEY[[:space:]]*:" "$CONFIG_FILE" | \ head -n 1 | \ sed -E 's/.*:[[:space:]]*([0-9]+).*/\1/' } get_connection_port() { PORT=$(get_config_port "connection_dedicated_port") if [ -z "$PORT" ]; then PORT="27015" fi echo "$PORT" } get_query_port() { PORT=$(get_config_port "query_dedicated_port") if [ -z "$PORT" ]; then PORT="27016" fi echo "$PORT" } port_in_use() { PORT="$1" if [ -z "$PORT" ]; then return 1 fi ss -H -lntup 2>/dev/null | awk -v port=":$PORT" ' $5 ~ port"$" { found=1 } END { exit !found } ' } show_port_owner() { PORT="$1" ss -lntup 2>/dev/null | grep -E ":$PORT[[:space:]]|:$PORT$" || true } get_port_owner_pids() { PORT="$1" ss -H -lntup 2>/dev/null | awk -v port=":$PORT" ' $0 ~ port { line=$0 while (match(line, /pid=[0-9]+/)) { pid=substr(line, RSTART + 4, RLENGTH - 4) print pid line=substr(line, RSTART + RLENGTH) } } ' | sort -u } get_ets2_pid_from_ports() { CONN_PORT=$(get_connection_port) QUERY_PORT=$(get_query_port) for PORT in "$CONN_PORT" "$QUERY_PORT"; do for P in $(get_port_owner_pids "$PORT"); do CMD=$(ps -p "$P" -o command= 2>/dev/null) case "$CMD" in *eurotrucks2_server*) echo "$P" return 0 ;; esac done done return 1 } check_ports_available() { CONN_PORT=$(get_connection_port) QUERY_PORT=$(get_query_port) echo "========== ETS2 端口检查 ==========" echo "connection_dedicated_port: $CONN_PORT" echo "query_dedicated_port: $QUERY_PORT" echo "" HAS_ERROR=0 if port_in_use "$CONN_PORT"; then show_error "端口 $CONN_PORT 已被占用" show_port_owner "$CONN_PORT" HAS_ERROR=1 else show_ok "端口 $CONN_PORT 可用" fi if port_in_use "$QUERY_PORT"; then show_error "端口 $QUERY_PORT 已被占用" show_port_owner "$QUERY_PORT" HAS_ERROR=1 else show_ok "端口 $QUERY_PORT 可用" fi echo "==================================" if [ "$HAS_ERROR" -ne 0 ]; then echo "" show_error "端口被占用,请先执行:" echo "$0 stop" echo "" echo "如果 stop 后仍占用,执行:" echo "$0 kill" return 1 fi return 0 } show_ports_status() { CONN_PORT=$(get_connection_port) QUERY_PORT=$(get_query_port) echo "========== ETS2 端口状态 ==========" echo "connection_dedicated_port: $CONN_PORT" show_port_owner "$CONN_PORT" echo "" echo "query_dedicated_port: $QUERY_PORT" show_port_owner "$QUERY_PORT" echo "==================================" } pgid_has_ets2() { TARGET_PGID="$1" if [ -z "$TARGET_PGID" ]; then return 1 fi ps -eo pgid=,cmd= | awk -v pgid="$TARGET_PGID" ' $1 == pgid && ($0 ~ /eurotrucks2_server/ || $0 ~ /server_launch.sh/ || $0 ~ /script .*server_launch.sh/) { found=1 } END { exit !found } ' } kill_ets2_port_owners() { CONN_PORT=$(get_connection_port) QUERY_PORT=$(get_query_port) for PORT in "$CONN_PORT" "$QUERY_PORT"; do for P in $(get_port_owner_pids "$PORT"); do CMD=$(ps -p "$P" -o command= 2>/dev/null) case "$CMD" in *eurotrucks2_server*|*server_launch.sh*) show_warn "端口 $PORT 仍被 ETS2 进程占用,强制清理 PID: $P" kill -TERM "$P" 2>/dev/null || true sleep 1 kill -KILL "$P" 2>/dev/null || true ;; *) show_warn "端口 $PORT 被非 ETS2 进程占用,PID: $P" echo "$CMD" ;; esac done done } wait_ports_release() { CONN_PORT=$(get_connection_port) QUERY_PORT=$(get_query_port) COUNT=0 while [ "$COUNT" -lt 10 ]; do if ! port_in_use "$CONN_PORT" && ! port_in_use "$QUERY_PORT"; then show_ok "端口 $CONN_PORT / $QUERY_PORT 已释放。" return 0 fi sleep 1 COUNT=$((COUNT + 1)) done show_warn "端口仍未完全释放,尝试按端口兜底清理..." kill_ets2_port_owners sleep 2 if ! port_in_use "$CONN_PORT" && ! port_in_use "$QUERY_PORT"; then show_ok "端口 $CONN_PORT / $QUERY_PORT 已释放。" return 0 fi show_error "端口仍被占用:" show_port_owner "$CONN_PORT" show_port_owner "$QUERY_PORT" return 1 } force_cleanup_server() { prepare_dirs show_warn "正在清理残留 ETS2 进程..." RUNNER_PID="" PGID="" if [ -f "$RUNNER_PID_FILE" ]; then RUNNER_PID=$(cat "$RUNNER_PID_FILE") fi if [ -f "$PGID_FILE" ]; then PGID=$(cat "$PGID_FILE") fi if [ -n "$PGID" ]; then if pgid_has_ets2 "$PGID"; then show_warn "正在停止 ETS2 进程组 PGID: $PGID" kill -TERM "-$PGID" 2>/dev/null || true else show_warn "PGID $PGID 不存在或不属于 ETS2,跳过进程组终止" fi fi if [ -n "$RUNNER_PID" ]; then show_warn "正在停止启动器 PID: $RUNNER_PID" kill -TERM "$RUNNER_PID" 2>/dev/null || true fi pkill -TERM -f "eurotrucks2_server" 2>/dev/null || true pkill -TERM -f "server_launch.sh" 2>/dev/null || true pkill -TERM -f "script .*server_launch.sh" 2>/dev/null || true sleep 3 if [ -n "$PGID" ]; then if pgid_has_ets2 "$PGID"; then kill -KILL "-$PGID" 2>/dev/null || true fi fi if [ -n "$RUNNER_PID" ]; then kill -KILL "$RUNNER_PID" 2>/dev/null || true fi pkill -KILL -f "eurotrucks2_server" 2>/dev/null || true pkill -KILL -f "server_launch.sh" 2>/dev/null || true pkill -KILL -f "script .*server_launch.sh" 2>/dev/null || true kill_ets2_port_owners rm -f "$PID_FILE" rm -f "$RUNNER_PID_FILE" rm -f "$PGID_FILE" wait_ports_release || true show_ok "残留进程清理完成。" } run_server_realtime() { cd "$SERVER_HOME" || exit 1 if command -v script >/dev/null 2>&1; then script -qefc "$SERVER_LAUNCH $SERVER_OPTIONS" /dev/null elif command -v stdbuf >/dev/null 2>&1; then stdbuf -oL -eL "$SERVER_LAUNCH" $SERVER_OPTIONS else "$SERVER_LAUNCH" $SERVER_OPTIONS fi } case "$1" in start) show_info "正在启动 ETS2 服务器..." prepare_dirs cleanup_old_logs SERVER_PID=$(get_server_pid) if [ -z "$SERVER_PID" ]; then SERVER_PID=$(get_ets2_pid_from_ports) fi if [ -n "$SERVER_PID" ]; then show_warn "ETS2 服务器似乎已经在运行,PID: $SERVER_PID" echo "$SERVER_PID" > "$PID_FILE" if [ -f "$CURRENT_LOG_PATH_FILE" ]; then show_info "本次日志: $(cat "$CURRENT_LOG_PATH_FILE")" fi exit 0 fi check_ports_available || exit 1 create_log_file cd "$SERVER_HOME" || exit 1 setsid sh -c ' SERVER_HOME="$1" SERVER_LAUNCH="$2" SERVER_OPTIONS="$3" SERVER_LOG="$4" cd "$SERVER_HOME" || exit 1 if command -v script >/dev/null 2>&1; then script -qefc "$SERVER_LAUNCH $SERVER_OPTIONS" /dev/null elif command -v stdbuf >/dev/null 2>&1; then stdbuf -oL -eL "$SERVER_LAUNCH" $SERVER_OPTIONS else "$SERVER_LAUNCH" $SERVER_OPTIONS fi 2>&1 | awk '"'"'{print strftime("%Y-%m-%d %H:%M:%S"), "-", $0; fflush();}'"'"' >> "$SERVER_LOG" ' sh "$SERVER_HOME" "$SERVER_LAUNCH" "$SERVER_OPTIONS" "$SERVER_LOG" >/dev/null 2>&1 & RUNNER_PID=$! echo "$RUNNER_PID" > "$RUNNER_PID_FILE" echo "$RUNNER_PID" > "$PGID_FILE" show_info "启动器 PID: $RUNNER_PID" show_info "进程组 PGID: $RUNNER_PID" show_info "等待 ETS2 服务端启动,最多等待 60 秒..." WAIT_COUNT=0 SERVER_PID="" LAUNCH_PID="" while [ "$WAIT_COUNT" -lt 60 ]; do SERVER_PID=$(get_server_pid) if [ -z "$SERVER_PID" ]; then SERVER_PID=$(get_ets2_pid_from_ports) fi LAUNCH_PID=$(get_launch_pid) if [ -n "$SERVER_PID" ]; then echo "$SERVER_PID" > "$PID_FILE" show_ok "ETS2 服务器已启动,PID: $SERVER_PID" show_info "最新日志: $LATEST_LOG_LINK" show_info "本次日志: $SERVER_LOG" exit 0 fi if [ -n "$LAUNCH_PID" ]; then show_info "启动脚本仍在运行,PID: $LAUNCH_PID,继续等待..." fi if grep -qiE "Failed to init|Server was terminated|couldn't find an open port|Server packages file not found|SteamAPI_Init.*failed|\*\*\* ERROR \*\*\*" "$SERVER_LOG" 2>/dev/null; then show_error "检测到启动错误,准备清理残留进程..." tail -n 100 "$SERVER_LOG" force_cleanup_server exit 1 fi WAIT_COUNT=$((WAIT_COUNT + 1)) sleep 1 done SERVER_PID=$(get_server_pid) if [ -z "$SERVER_PID" ]; then SERVER_PID=$(get_ets2_pid_from_ports) fi if [ -n "$SERVER_PID" ]; then echo "$SERVER_PID" > "$PID_FILE" show_ok "ETS2 服务器已启动,PID: $SERVER_PID" show_info "最新日志: $LATEST_LOG_LINK" show_info "本次日志: $SERVER_LOG" exit 0 fi show_error "等待 60 秒后仍未检测到 ETS2 主进程,准备清理残留进程..." tail -n 120 "$SERVER_LOG" force_cleanup_server exit 1 ;; debug) show_info "正在以前台调试模式启动 ETS2 服务器..." show_warn "当前不会后台运行,按 Ctrl+C 可停止服务器。" prepare_dirs cleanup_old_logs create_log_file check_required_files OLD_PID=$(get_server_pid) if [ -z "$OLD_PID" ]; then OLD_PID=$(get_ets2_pid_from_ports) fi if [ -n "$OLD_PID" ]; then show_error "检测到已有 ETS2 服务端进程正在运行,PID: $OLD_PID" echo "" echo "请先执行:" echo "$0 stop" echo "" echo "如果仍然无法释放端口,执行:" echo "$0 kill" exit 1 fi check_ports_available || exit 1 trap ' echo "" echo "[WARN] 收到中断信号,正在退出 debug 模式..." force_cleanup_server echo "[OK] debug 残留进程已清理。" exit 130 ' INT TERM HUP echo "" echo "========== 开始前台调试 ==========" echo "执行命令:" echo "$SERVER_LAUNCH $SERVER_OPTIONS" echo "本次日志:" echo "$SERVER_LOG" echo "==================================" echo "" run_server_realtime 2>&1 | \ awk '{print strftime("%Y-%m-%d %H:%M:%S"), "-", $0; fflush();}' | \ tee -a "$SERVER_LOG" trap - INT TERM HUP show_warn "debug 模式已退出,正在清理可能残留的 ETS2 进程..." force_cleanup_server ;; stop) show_info "正在停止 ETS2 服务器..." force_cleanup_server ;; kill) force_cleanup_server ;; restart) show_info "正在重启 ETS2 服务器..." "$0" stop sleep 3 "$0" start ;; update) show_info "正在更新 ETS2 专用服务器..." SERVER_PID=$(get_server_pid) if [ -n "$SERVER_PID" ]; then show_warn "检测到服务器正在运行,先停止服务器..." "$0" stop sleep 3 fi mkdir -p "$STEAMCMD_HOME" "$ETS2_INSTALL_DIR" cd "$STEAMCMD_HOME" || exit 1 show_info "检查 SteamCMD..." curl -sqL "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" | tar zxvf - chmod +x "$STEAMCMD_HOME/steamcmd.sh" show_info "开始更新 ETS2 Dedicated Server..." "$STEAMCMD_HOME/steamcmd.sh" \ +force_install_dir "$ETS2_INSTALL_DIR" \ +login anonymous \ +app_update "$ETS2_APP_ID" validate \ +quit show_ok "ETS2 专用服务器更新完成。" ;; status) prepare_dirs SERVER_PID=$(get_server_pid) if [ -z "$SERVER_PID" ]; then SERVER_PID=$(get_ets2_pid_from_ports) fi LAUNCH_PID=$(get_launch_pid) if [ -n "$SERVER_PID" ]; then show_ok "ETS2 服务器正在运行,PID: $SERVER_PID" if [ -n "$LAUNCH_PID" ]; then show_info "启动脚本进程 PID: $LAUNCH_PID" fi if [ -f "$CURRENT_LOG_PATH_FILE" ]; then show_info "本次日志: $(cat "$CURRENT_LOG_PATH_FILE")" fi show_info "最新日志: $LATEST_LOG_LINK" echo "" show_ports_status else show_warn "ETS2 服务器未运行。" rm -f "$PID_FILE" fi ;; log) prepare_dirs if [ -L "$LATEST_LOG_LINK" ] || [ -f "$LATEST_LOG_LINK" ]; then tail -n 100 -F "$LATEST_LOG_LINK" else show_warn "暂无最新日志文件。" show_info "请先执行:$0 start 或 $0 debug" fi ;; listlog) prepare_dirs echo "最近日志文件:" find "$LOG_ROOT" -type f -name "*.log" 2>/dev/null | sort | tail -n 30 ;; cleanlog) prepare_dirs cleanup_old_logs show_ok "已清理超过 $LOG_KEEP_DAYS 天的日志目录。" ;; check) prepare_dirs check_required_files echo "" check_ports_available || true ;; ports) prepare_dirs check_ports_available || true ;; portstatus) prepare_dirs show_ports_status ;; *) echo "ETS2 服务器管理命令" echo "用法: ets2_sv {start|stop|restart|status|update|debug|log|listlog|cleanlog|check|ports|portstatus|kill}" echo "" echo " start - 后台启动 ETS2 服务器" echo " stop - 停止 ETS2 服务器,并释放端口" echo " restart - 重启 ETS2 服务器" echo " status - 查看 ETS2 服务器状态" echo " update - 更新 ETS2 专用服务器" echo " debug - 前台调试启动,不后台运行,实时输出" echo " log - 实时查看最新日志" echo " listlog - 查看最近日志文件" echo " cleanlog - 清理旧日志" echo " check - 检查配置文件和端口" echo " ports - 检查端口是否可用" echo " portstatus - 查看端口占用详情" echo " kill - 强制清理残留 ETS2 进程" exit 1 ;; esac