Files
Rust-Link/egg/with-sidecar.sh
wtclaude af0e6be9f5
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 4m0s
fix(egg): the launcher drops Carbon's preloader for itself, not just the sidecar
Carbon's entrypoint puts LD_PRELOAD=libdoorstop.so in front of the whole
startup string, and with Carbon's DOORSTOP_* environment that preloader
breaks the launcher shell's own command substitution: the sidecar's
--print-config output went straight to the console and the capture came
back empty. The first Carbon server built from the egg printed its config
JSON, token included, unframed, then "server id 'main', sidecar URL
http://192.168.0.12: (listening on )" and never the once-only banner.
`env -u LD_PRELOAD` on the sidecar alone was not enough: the shell doing
the capturing had the preload too.

The launcher now re-execs itself once without LD_PRELOAD, keeps it aside,
and gives it back to the game in run_game, the only process that needs it.

Reproduced and fixed under Carbon's real environment (environment.sh +
libdoorstop.so): the released launcher dumps the JSON; this one prints the
banner and "server id 'egg-carbon', sidecar URL http://192.168.0.12:21019",
and the game still gets LD_PRELOAD and DOORSTOP_ENABLED. The four
no-preload cases in the game image are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 04:28:05 -05:00

125 lines
6.6 KiB
Bash
Executable File

#!/bin/sh
# with-sidecar.sh — start the rust-link sidecar beside a Rust server, then become the server.
#
# The egg's startup is this script followed by the game's own command line:
#
# ./rust-link/with-sidecar.sh ./RustDedicated -batchmode …
#
# It ships in Rust-Link's release rather than inside the egg, so a fix here reaches a server at its
# next reinstall without anybody re-importing the egg (docs/modules/rust/PLAN.md §34.2.6, §34.4).
# The shape is docs/rust-link/INSTALL_RIG.md's, proven on the rigs; see that file for why each line
# that looks optional is not.
#
# POSIX sh and no jq: the game image (ghcr.io/pterodactyl/games:rust) has grep and sed, not jq.
#
# Deliberately NOT `set -e`. Every step before the last line is the bridge's, and the last line is
# the game's: nothing the bridge gets wrong — an unwritable log, a sidecar that will not start — may
# keep the server from booting. Each step reports its own failure and the script goes on to `exec`.
# Carbon's entrypoint puts LD_PRELOAD=libdoorstop.so in front of the WHOLE startup string, and with
# Carbon's DOORSTOP_* environment that preloader breaks this shell's own `$(...)`: the child's output
# goes straight to the console and the capture comes back empty. The phase 18 walk saw exactly that
# on the first Carbon server built from the egg — the config JSON, token included, dumped raw to the
# console, and an empty "server id 'main', sidecar URL http://<ip>:" line. Dropping LD_PRELOAD for
# the sidecar alone is not enough; the shell doing the capturing has it too. So the script runs
# once more without it, keeps it aside, and gives it back to the game — the one process that needs
# it — in `run_game` below.
if [ -n "${LD_PRELOAD-}" ] && [ -z "${RUNICGATEWAY_LAUNCHER_CLEAN-}" ]; then
exec env -u LD_PRELOAD RUNICGATEWAY_GAME_PRELOAD="$LD_PRELOAD" RUNICGATEWAY_LAUNCHER_CLEAN=1 \
sh "$0" "$@"
fi
# The game BECOMES this process: the panel console keeps its stdin and stdout, and stop still
# stops the server, which takes the sidecar down with the container.
run_game() {
if [ -n "${RUNICGATEWAY_GAME_PRELOAD-}" ]; then
export LD_PRELOAD="$RUNICGATEWAY_GAME_PRELOAD"
fi
unset RUNICGATEWAY_GAME_PRELOAD RUNICGATEWAY_LAUNCHER_CLEAN
exec "$@"
}
RL=/home/container/rust-link
mkdir -p "$RL" 2>/dev/null
export RUSTLINK_CONFIG="$RL/sidecar.toml"
# Fixed, and never a panel variable: the install script moves this directory aside around its own
# `rm -rf ${REMOVE_FILES}`, which is what keeps a wipe from reaching the store (§34.1).
export RUSTLINK_DB_PATH="$RL/rust-link.db"
# An EMPTY panel variable is exported as `VAR=""`. The sidecar treats that as unset, and so does
# this script, so a blank field always means "the default" (or, for the token, "the saved one").
for v in RUSTLINK_SERVER_ID RUSTLINK_WEB_TOKEN RUSTLINK_RETAIN_DAYS RUSTLINK_WEB_PORT RUSTLINK_GAME_BIND RUSTLINK_WEB_BIND; do
eval "val=\${$v-}"
if [ -z "$val" ]; then unset "$v"; fi
done
# The server id follows the PLUGIN's config once it exists. The plugin reads RUSTLINK_SERVER_ID only
# when it writes its first config; after that the file is canonical and the website locks it (D150).
# The sidecar refuses a plugin that names another server (D155), so a variable edited after the
# first boot would otherwise take the bridge down instead of changing nothing. Stock framework paths,
# the same ones the installer writes. `sed`, because the game image has no jq.
for f in /home/container/oxide/config/RunicGateway.json /home/container/carbon/configs/RunicGateway.json; do
[ -f "$f" ] || continue
ID="$(sed -n 's/^[[:space:]]*"ServerId":[[:space:]]*"\([^"]*\)".*$/\1/p' "$f" | head -n 1)"
if [ -n "$ID" ]; then
if [ -n "${RUSTLINK_SERVER_ID-}" ] && [ "$RUSTLINK_SERVER_ID" != "$ID" ]; then
echo "[rust-link] RUSTLINK_SERVER_ID is '$RUSTLINK_SERVER_ID' but the plugin's config names '$ID'; using '$ID', the id the website knows. The variable is read once, at the first boot."
fi
export RUSTLINK_SERVER_ID="$ID"
fi
break
done
# The website-facing bind. Pterodactyl tells a container nothing about its extra allocations, so
# the port is typed into the egg, and it must be one of this server's allocations — a port that is
# not fails as a bind the website never reaches, which the lines below make visible (§34.1).
if [ -n "${RUSTLINK_WEB_PORT-}" ]; then
export RUSTLINK_WEB_BIND="0.0.0.0:${RUSTLINK_WEB_PORT}"
fi
SIDECAR="$RL/rust-link-sidecar"
if [ ! -x "$SIDECAR" ]; then
echo "[rust-link] $SIDECAR is missing - reinstall the server to fetch the bridge. Starting the game without it."
run_game "$@"
fi
# Provision first, so the token can be shown. `--print-config` resolves the configuration exactly as
# a start does, writing sidecar.toml with a generated token when there is none; the JSON it prints
# says whether it generated one on THIS call, which is what makes "print once" true (D152).
# Tracing is off on that path, so stdout is only the document. LD_PRELOAD is already gone from this
# shell (see the top); `env -u` stays so a preload set some other way cannot reach the sidecar.
if CFG="$(env -u LD_PRELOAD "$SIDECAR" --print-config 2>&1)"; then
# The Nth `"key": "value"` line of the pretty-printed JSON. `bind` appears twice — game, then web.
field() { printf '%s\n' "$CFG" | sed -n "s/^ *\"$1\": \"\([^\"]*\)\",\{0,1\}\$/\1/p" | sed -n "${2:-1}p"; }
WEB_BIND="$(field bind 2)"
SERVER_ID="$(field server_id)"
if printf '%s\n' "$CFG" | grep -q '"token_generated": true'; then
TOKEN="$(field auth_token)"
echo "[rust-link] ================================================================"
echo "[rust-link] A new sidecar token was generated. It is shown ONCE, here:"
echo "[rust-link] ${TOKEN}"
echo "[rust-link] It is kept in rust-link/sidecar.toml; read it there if you lose it."
echo "[rust-link] ================================================================"
fi
PORT="${WEB_BIND##*:}"
HOST="${SERVER_IP-}"
case "$HOST" in ""|0.0.0.0) HOST="<this node's address>" ;; esac
echo "[rust-link] Admin -> Rust -> Servers: server id '${SERVER_ID:-main}', sidecar URL http://${HOST}:${PORT} (listening on ${WEB_BIND})"
else
echo "[rust-link] the sidecar could not read its configuration:"
printf '%s\n' "$CFG" | sed 's/^/[rust-link] /'
fi
# A background job's redirection fails in the child, invisibly, so writability is asked first. A
# directory the log cannot be written to is one the store and the token cannot be written to either.
if touch "$RL/sidecar.log" 2>/dev/null; then
env -u LD_PRELOAD "$SIDECAR" >> "$RL/sidecar.log" 2>&1 &
else
echo "[rust-link] $RL is not writable - the game starts without the sidecar."
fi
run_game "$@"