diff --git a/CLAUDE.md b/CLAUDE.md index a8d7907..0568dc9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,11 +41,13 @@ Everything runs as the `plutainer` user from `/home/plutainer/.plutainer`. 5. **`game-config.sh`** — Shared shell library sourced by all other scripts. Single source of truth for game detection, port defaults, config path resolution, and RCON password extraction. -6. **`healthcheck.sh`** — Sources `game-config.sh`, then uses `pyquake3.py` to send an RCON `status` command. Can be disabled with `PLUTO_HEALTHCHECK=true`, `IW4X_HEALTHCHECK=true`, or `ALTER_HEALTHCHECK=true`. +6. **`log-watcher.sh`** — Background poller started by each entrypoint before `exec wine`. Maintains stable symlinks at `/home/plutainer/app/logs/` pointing at the active game log (e.g. `games_mp.log`, `games_zm.log`). Uses container boot time as a mtime cutoff so stale logs from prior sessions and abandoned mod dirs are ignored. Disable with `PLUTAINER_LOG_SYMLINKS=false`; poll interval via `PLUTAINER_LOG_POLL_INTERVAL` (default 2s). -7. **`rcon-cli`** — Python script providing interactive and one-shot RCON access via `docker exec`. Calls `game-config.sh` to resolve port/credentials. Supports Plutonium, IW4x, and Alterware. +7. **`healthcheck.sh`** — Sources `game-config.sh`, then uses `pyquake3.py` to send an RCON `status` command. Can be disabled with `PLUTO_HEALTHCHECK=true`, `IW4X_HEALTHCHECK=true`, or `ALTER_HEALTHCHECK=true`. -7. **`pyquake3.py`** — Python 3 Quake 3 protocol library (UDP). Used by the health check and `rcon-cli` for RCON queries. +8. **`rcon-cli`** — Python script providing interactive and one-shot RCON access via `docker exec`. Calls `game-config.sh` to resolve port/credentials. Supports Plutonium, IW4x, and Alterware. + +9. **`pyquake3.py`** — Python 3 Quake 3 protocol library (UDP). Used by the health check and `rcon-cli` for RCON queries. ## Game-Specific Behavior @@ -60,4 +62,5 @@ For Plutonium, `BASE_GAME` is derived by stripping the last two chars from `PLUT - `/home/plutainer/gamefiles` — bind-mounted read-only game files from host - `/home/plutainer/app` — persistent volume (gamefiles symlinks, plutonium data, configs, logs) +- `/home/plutainer/app/logs` — stable symlinks to active game logs, maintained by `log-watcher.sh`. Host-side IW4MAdmin should bind-mount this dir and read logs from there instead of reaching into nested mod/storage paths. - `/home/plutainer/.plutainer` — working directory containing scripts, updaters, and pyquake3 diff --git a/Dockerfile b/Dockerfile index 38b50ff..f7de3f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,7 +69,7 @@ RUN IW4X_URL=$(wget -qO- https://api.github.com/repos/iw4x/launcher/releases/lat # Copy all scripts and the python module into the image COPY --chown=plutainer:plutainer scripts/ . -RUN chmod +x entrypoint.sh healthcheck.sh plutoentry.sh iw4xentry.sh alterentry.sh rcon-cli game-config.sh +RUN chmod +x entrypoint.sh healthcheck.sh plutoentry.sh iw4xentry.sh alterentry.sh log-watcher.sh rcon-cli game-config.sh # Add rcon-cli to PATH so it can be invoked without a full path via docker exec USER root diff --git a/scripts/alterentry.sh b/scripts/alterentry.sh index bd1aeff..95dd706 100644 --- a/scripts/alterentry.sh +++ b/scripts/alterentry.sh @@ -109,6 +109,8 @@ rm -f /tmp/.X99-lock Xvfb :99 -screen 0 320x240x24 & sleep 1 +/home/plutainer/.plutainer/log-watcher.sh & + echo "Starting T7x Server: ${ALTER_SERVER_NAME}" echo "EXECUTING: wine t7x.exe ${CMD_ARGS[@]}" exec wine t7x.exe "${CMD_ARGS[@]}" diff --git a/scripts/iw4xentry.sh b/scripts/iw4xentry.sh index 8e1ced8..ba3598a 100644 --- a/scripts/iw4xentry.sh +++ b/scripts/iw4xentry.sh @@ -92,6 +92,8 @@ fi CMD_ARGS+=(+map_rotate) # --- Step 5: Launch the iw4x Server --- +/home/plutainer/.plutainer/log-watcher.sh & + echo "Starting ${IW4X_GAME} Server: ${IW4X_SERVER_NAME}" echo "EXECUTING: wine iw4x.exe ${CMD_ARGS[@]}" exec wine iw4x.exe "${CMD_ARGS[@]}" diff --git a/scripts/log-watcher.sh b/scripts/log-watcher.sh new file mode 100644 index 0000000..0616f66 --- /dev/null +++ b/scripts/log-watcher.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# +# Maintains stable symlinks at /home/plutainer/app/logs/ pointing at the +# game log currently being written. Game logs move around per game/mod (e.g. +# plutonium/storage/t5/mods//logs/games_zm.log), which makes IW4MAdmin +# configuration brittle. This watcher surfaces them in one predictable dir. +# +# Strategy: +# - Record container boot time. +# - Poll app/ for files matching known log names. +# - The active log is the one with mtime >= boot time (stale logs from prior +# sessions keep their old mtime and are ignored). Ties broken by newest. +# - Update symlink only when target changes (idempotent). +# - Create empty stub files at startup so host-side IW4MAdmin never sees a +# dangling symlink during the window before the first write event. +# + +APP_DIR=/home/plutainer/app +STABLE_DIR="$APP_DIR/logs" +POLL_INTERVAL="${PLUTAINER_LOG_POLL_INTERVAL:-2}" +LOG_NAMES=(games_mp.log games_zm.log console_mp.log console_zm.log) + +if [[ "${PLUTAINER_LOG_SYMLINKS}" == "false" ]]; then + echo "[log-watcher] disabled via PLUTAINER_LOG_SYMLINKS=false" + exit 0 +fi + +mkdir -p "$STABLE_DIR" + +for name in "${LOG_NAMES[@]}"; do + target="$STABLE_DIR/$name" + if [[ -L "$target" ]]; then + # Leave existing symlinks from prior run in place until a fresh target is + # identified. Preserves continuity for IW4MAdmin across container restarts + # where the old log path is still valid. + : + elif [[ ! -e "$target" ]]; then + touch "$target" + fi +done + +BOOT_TS=$(date +%s) + +declare -A CURRENT_TARGET + +echo "[log-watcher] started; boot_ts=$BOOT_TS stable_dir=$STABLE_DIR" + +while true; do + for name in "${LOG_NAMES[@]}"; do + newest_path="" + newest_mtime=0 + + while IFS= read -r -d '' path; do + mtime=$(stat -c %Y "$path" 2>/dev/null) || continue + (( mtime < BOOT_TS )) && continue + if (( mtime > newest_mtime )); then + newest_mtime=$mtime + newest_path=$path + fi + done < <(find "$APP_DIR" -path "$STABLE_DIR" -prune -o -type f -name "$name" -print0 2>/dev/null) + + if [[ -n "$newest_path" && "$newest_path" != "${CURRENT_TARGET[$name]:-}" ]]; then + link="$STABLE_DIR/$name" + rm -f "$link" + ln -s "$newest_path" "$link" + CURRENT_TARGET[$name]=$newest_path + echo "[log-watcher] $name -> $newest_path" + fi + done + + sleep "$POLL_INTERVAL" +done diff --git a/scripts/plutoentry.sh b/scripts/plutoentry.sh index de170c5..4438e6b 100644 --- a/scripts/plutoentry.sh +++ b/scripts/plutoentry.sh @@ -132,6 +132,8 @@ else fi # --- Step 5: Launch the Plutonium Server --- +/home/plutainer/.plutainer/log-watcher.sh & + echo "Starting Plutonium ${PLUTO_GAME} Server: ${PLUTO_SERVER_NAME}" echo "EXECUTING: wine bin/plutonium-bootstrapper-win32.exe ${CMD_ARGS[@]}" exec wine bin/plutonium-bootstrapper-win32.exe "${CMD_ARGS[@]}"