og-cod-plutainer/scripts/rcon-cli

221 lines
6.2 KiB
Text
Raw Permalink Normal View History

#!/usr/bin/env python3
"""
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
rcon-cli — interactive and one-shot remote console for Plutainer.
Usage:
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
docker exec -i <container> rcon-cli # interactive
docker exec <container> rcon-cli <command> # one-shot
"RCON" is the name people search for, so the command keeps it, but the protocol
underneath depends on the game: Quake3 RCON over UDP for the Call of Duty
families, Valve's RCON over TCP for Source games, a telnet console for 7 Days to
Die. The shell library decides which; this script only speaks them.
"""
import os
import subprocess
import sys
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
# Resolve symlinks so SCRIPT_DIR points at the real location rather than the
# symlink's directory (this is linked into /usr/local/bin).
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, SCRIPT_DIR)
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
def resolve_endpoint():
"""Ask the shell library how to reach this game's console."""
script = f"""
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
source "{SCRIPT_DIR}/lib/core.sh"
detect_game_type || exit 1
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
resolve_admin_endpoint || exit 1
echo "$GAME_NAME"
echo "$GAME_TYPE"
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
echo "$ADMIN_PROTOCOL"
echo "$ADMIN_PORT"
echo "$ADMIN_PASSWORD"
echo "${{ADMIN_DISABLED_HINT:-}}"
echo "$ADMIN_HOSTS"
"""
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
result = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
if result.returncode != 0:
print(result.stderr.strip(), file=sys.stderr)
sys.exit(1)
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
lines = result.stdout.split("\n")
if len(lines) < 7:
print("Error: failed to resolve the server's console endpoint.", file=sys.stderr)
if result.stderr.strip():
print(result.stderr.strip(), file=sys.stderr)
sys.exit(1)
return {
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
"game_name": lines[0].strip(),
"game_type": lines[1].strip(),
"protocol": lines[2].strip(),
"port": lines[3].strip(),
"password": lines[4],
"hint": lines[5].strip(),
# Source servers answer only on the container's own address; everything
# else answers on loopback. Try in order rather than encoding which.
"hosts": lines[6].split() or ["127.0.0.1"],
}
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
# --- one class per protocol, same three methods -----------------------------
class Quake3Client:
"""CoD families. Connectionless UDP; every command is its own packet."""
def __init__(self, cfg):
from protocols import quake3
if not cfg["password"]:
raise SystemExit(
"No rcon_password is set in this server's config, so RCON is disabled.\n"
"Set PLUTAINER_RCON_PASSWORD and restart, or add the line yourself."
)
self.server = quake3.Quake3Server(
"127.0.0.1:%s" % cfg["port"], rcon_password=cfg["password"]
)
def open(self):
pass
def send(self, cmd):
_kind, response = self.server.rcon(cmd)
return response.strip()
def close(self):
pass
class SourceRconClient:
"""Source games. Stateful TCP session, so the connection is held open."""
def __init__(self, cfg):
from protocols import source_rcon
self.hosts = cfg["hosts"]
self.port = cfg["port"]
self.password = cfg["password"]
self.module = source_rcon
self.conn = None
def open(self):
self.conn = _first_reachable(
self.hosts,
lambda host: self.module.SourceRcon(host, self.port, self.password),
)
def send(self, cmd):
return self.conn.command(cmd).strip()
def close(self):
if self.conn is not None:
self.conn.close()
class TelnetClient:
"""7 Days to Die. Console stream; a reply is whatever arrives next."""
def __init__(self, cfg):
from protocols import telnet_admin
self.hosts = cfg["hosts"]
self.port = cfg["port"]
self.password = cfg["password"]
self.module = telnet_admin
self.conn = None
def open(self):
self.conn = _first_reachable(
self.hosts,
lambda host: self.module.TelnetAdmin(host, self.port, self.password),
)
def send(self, cmd):
return self.conn.command(cmd).strip()
def close(self):
if self.conn is not None:
self.conn.close()
def _first_reachable(hosts, build):
"""Connect to the first host that answers, else raise the last error."""
last = None
for host in hosts:
conn = build(host)
try:
conn.connect()
return conn
except Exception as err:
last = err
raise last or OSError("no address answered")
CLIENTS = {
"quake3": Quake3Client,
"source-rcon": SourceRconClient,
"telnet": TelnetClient,
}
def build_client(cfg):
protocol = cfg["protocol"]
if protocol == "disabled":
raise SystemExit(
"%s has a remote console, but it is turned off in this server's config.\n%s"
% (cfg["game_name"], cfg["hint"] or "")
)
if protocol == "none" or protocol not in CLIENTS:
raise SystemExit(
"%s has no remote console that Plutainer can talk to." % cfg["game_name"]
)
return CLIENTS[protocol](cfg)
def main():
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
cfg = resolve_endpoint()
client = build_client(cfg)
try:
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
client.open()
except Exception as err:
raise SystemExit("Could not open the console: %s" % err)
try:
# One-shot: everything after the program name is the command.
if len(sys.argv) > 1:
print(client.send(" ".join(sys.argv[1:])))
return
print(
"Connected to %s (%s, %s) on port %s"
% (cfg["game_name"], cfg["game_type"], cfg["protocol"], cfg["port"])
)
print("Type 'quit' or 'exit' to close. Ctrl+C also works.")
print()
while True:
try:
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
cmd = input("> ").strip()
except (EOFError, KeyboardInterrupt):
print()
break
if not cmd:
continue
if cmd.lower() in ("quit", "exit"):
break
SteamCMD family: generalise 7DTD support, add CS2/L4D2/HL2:DM, unify the CoD side (#9) * feat: add 7 Days to Die server support * refactor(scripts): two table-driven families behind one entry script each Plutainer had five "families" that were really four Call of Duty engines plus a platform, and a separate entry script per engine. Adding 7 Days to Die made the mismatch obvious: a SteamCMD game shares nothing with a Quake-derived one, while a Plutonium T6 and a CoD4x server differ far less from each other than either does from a Steam install. There are now two families, and they are platforms: cod servers Plutainer installs and runs from game files you supply steam servers SteamCMD installs Engine (plutonium/iw4x/alterware/cod4x, unity/srcds) drops to a field in that family's game table, where it belongs. Each family gets one entry script driven by that table, so the four CoD entry scripts collapse into games/codentry.sh at 92 lines, and adding a game is a table row rather than a new script. entrypoint.sh has two branches and gains none per game. Per-engine behaviour is expressed as hooks resolved most-specific-first: cod_<hook>_<game> -> cod_<hook>_<base game> -> cod_<hook>_<engine> so a game inherits its engine and overrides only what genuinely differs. The same mechanism serves both families, which removed fifteen one-line pass-through functions on the Steam side. Hook names are strings, so plutainer_require_hooks verifies the mandatory ones resolve at startup and prints what it tried. That is not theoretical: during this refactor a comma-separated suffix list was passed where an array was wanted, and every server died with no output whatsoever until plutainer_hook was made to say what it could not find. The library splits along the same line — core/fs/cod/steam — so a CoD change never requires reading the Steam helpers. Audited: zero cross-family calls in either direction. Anything both needed (the symlink helpers) moved to fs.sh. Renamed from *-config.sh, which was inaccurate once the files held install logic and launch arguments. Verified on all ten CoD games (T4/T5/T6 MP+ZM, IW5, IW4x, T7x MP+ZM, CoD4x): launch commands byte-identical to the originals, correct config symlinks per engine, health and RCON working. * feat(protocols): A2S, Source RCON and telnet, split by protocol pyquake3.py was the only wire protocol Plutainer spoke, which was fine while every game was Quake-derived. The SteamCMD games are not: 7DTD has a telnet console and no RCON at all, Source games speak Valve's RCON over TCP, and none of them answer getstatus. Protocols now live in scripts/protocols/, split by protocol rather than by game, because the mapping is many-to-many — Quake3 covers seven CoD titles, A2S covers four Steam games across three engines, and querying is a different concern from administering. healthcheck.sh picks its probe from the family table (STEAM_QUERY) rather than a hardcoded branch, and keeps the same bar for every game: the server must name a map it is running. A2S reports one, so "healthy" does not degrade to "a TCP connect succeeded" for the new family. rcon-cli keeps its name because that is what people search for, but is now one small class per protocol behind a dispatch table. It also distinguishes "this game has no remote console" from "it has one and you have it switched off", and tells you which setting to change in the second case. Both try loopback and then the container's own address. Source 1 servers answer only on the latter: an identical A2S query times out on 127.0.0.1 and replies immediately on the container IP, with the server healthy throughout. Rather than encode which engines behave which way, try both. pyquake3.py moves to protocols/quake3.py with its GPL attribution intact. * fix(logs): let a family opt out of rotation, and skip huge install trees Rotation is copy-truncate, which is only valid against a writer that opened its log with O_APPEND — true of every CoD engine, and the reason the existing implementation is safe. A Unity dedicated server's -logfile writer keeps its own offset instead, so truncating would leave a sparse hole and the file's apparent size would snap straight back over the limit, re-triggering rotation on every poll. That has not been measured against a running 7DTD, so PLUTAINER_LOG_ROTATE lets the SteamCMD family turn rotation off. Not rotating is the safe failure — an unbounded log, which is what the game does unmanaged — rather than a rotation loop copying gigabytes every two seconds. PLUTAINER_LOG_PRUNE_DIRS keeps the poller out of a SteamCMD install, which is tens of thousands of files that would otherwise be walked every two seconds for one log that does not live there. * build: add SteamCMD, keep STOPSIGNAL SIGKILL SteamCMD is fetched but deliberately not bootstrapped at build time. Running `steamcmd.sh +quit` in the image pulls a few hundred MB of Steam client into $HOME that every Call of Duty user would then carry forever, and 340 MB of Xvfb was removed for exactly that reason. It also does not reliably prevent the first-contact failure it appears to fix, which is handled with a retry instead. Placed after everything the CoD families need, so adding or bumping it never invalidates their layers. STOPSIGNAL stays SIGKILL. A game with world state to flush cannot be served by a signal that cannot be trapped, but changing the image default would alter shutdown for seven servers already running in production to benefit one new family. Those games ask for SIGTERM per service instead: stop_signal: SIGTERM stop_grace_period: 90s Measured: 7DTD forwards the signal, saves, and exits 0 in 3.5s; the same image with no stop_signal stops in 254ms as before. The hang case needs no code — Docker sends SIGKILL itself when the grace period expires. chmod now covers every *.sh by find rather than a hand-maintained list that silently rots as scripts move between directories. * feat(steam): add CS2, L4D2 and Half-Life 2: Deathmatch Three more games through the SteamCMD family, chosen to stress the table in different directions rather than to pad the list. Each needed a table row and, at most, one hook. HL2:DM is the plain srcds case. L4D2 shares its hooks entirely. CS2 shares seed, configure, stage and admin with them and overrides only its launch arguments, because Source 2 has no srcds_run wrapper. L4D2 needs a two-phase install and it is not optional. Valve restricted anonymous Linux installation of app 222860: every depot including the 9.5 GB content one is flagged windows, so a plain app_update on Linux fails with "Invalid platform". This is an open upstream issue (ValveSoftware/steam-for-linux#11522) that takes LinuxGSM down with it, so "this used to work" is true and not a local fault. Pulling the content as Windows and re-running as Linux overlays the native binaries depot on top. STEAM_INSTALL_PLATFORMS expresses that generically. The result is a genuine native server — srcds_linux, no Wine, reports "os: Linux Dedicated". CS2 exposed a real bug in the config handling. link_configs refuses to replace a real file at the engine path, because for the CoD families that file is the strongest signal of user intent. Inside a SteamCMD install the same signal means the opposite: the directory is Steam's, so a real file is a depot default that the next update restores anyway. CS2 ships a 33-byte game/csgo/cfg/server.cfg reading "// Defaults in server_default.cfg", so the fan-out skipped it with a warning and the server ran on stock settings while app/configs/server.cfg sat there looking correct. Measured before and after: hostname went from "Counter-Strike 2" to the configured name, and the name a server browser shows changed with it. SteamCMD's first contact in a fresh container is also unreliable — it downloads its own client, re-execs, and an app_update issued before that settles fails with "Missing configuration". Measured on both 7DTD and HL2:DM, with the next attempt succeeding, so installs are retried. Seed configs are hand-written rather than vendored: Valve ships no server.cfg for any of these. All three ship empty rcon_password and sv_password, like every other seed here. * docs: rewrite for two families and eleven games The docs described five families and a script-per-game layout that no longer exists, and the 7DTD material from the original PR assumed it was a one-off rather than the first member of a family. Reworked around the two-platform model, with per-game specifics where a reader looks for them: disk footprints (CS2 is 67 GB and will exceed the health grace period on first start), the GSLT and masterserver-token equivalence, why clean shutdown is opt-in per service, and what the depot-config takeover does. Audited rather than assumed: all fourteen game tags now appear in the README table, the games reference, the configuration reference and a working compose example; every environment variable the code reads is in the reference table; every script is described in CLAUDE.md; no stale references to the deleted entry scripts remain; all internal doc links resolve. That audit found two undocumented variables, now added. * chore: remove the EXAMPLE-docker-compose.yml signpost The file had already been reduced to a stub pointing at examples/, kept on the theory that older guides and forum posts link to it. Nothing in the repo references it any more: every doc that shows a compose file points at examples/ directly, so the stub was carrying its own rationale and nothing else. Also drops the two places that still named it — its .dockerignore entry (examples/ and *.md already cover everything it excluded) and its paths-ignore entry in the publish workflow, which was suppressing builds for a file that no longer exists. --------- Co-authored-by: Keyboard Sped <93077330+CoreyUK@users.noreply.github.com>
2026-08-16 17:12:31 +01:00
try:
print(client.send(cmd))
except Exception as err:
print("Console error: %s" % err, file=sys.stderr)
finally:
client.close()
if __name__ == "__main__":
main()