tools: harden shared server session workflow

This commit is contained in:
BruceChen 2026-04-12 23:33:45 +08:00
parent b665ebfeac
commit af285866dc
10 changed files with 331 additions and 64 deletions

View file

@ -41,6 +41,8 @@ USERNAME=""
DO_BUILD=true
DEBUG_ON=false
FILE_INPUT=false
BUILD_ROOT="$(_mcc_build_root)"
BUILD_ROOT_ENV_PREFIX=""
while [[ $# -gt 0 ]]; do
case "$1" in
@ -98,6 +100,12 @@ if [[ -z "$USERNAME" ]]; then
USERNAME="$(_mcc_resolve_username "$SESSION")"
fi
if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then
mkdir -p "$BUILD_ROOT"
printf -v BUILD_ROOT_QUOTED '%q' "$BUILD_ROOT"
BUILD_ROOT_ENV_PREFIX="MCC_BUILD_ROOT=$BUILD_ROOT_QUOTED "
fi
SESSION_ROOT="$(_mcc_session_root "$SESSION")"
CFG="$SESSION_ROOT/MinecraftClient.debug.ini"
MCC_LOG="$(_mcc_session_log_file "$SESSION")"
@ -118,6 +126,7 @@ echo " Server: $VERSION (port $PORT)"
echo " Session: $SESSION"
echo " User: $USERNAME"
echo " Mode: $MODE"
echo " Build: $BUILD_ROOT"
echo " Root: $SESSION_ROOT"
echo " Config: $CFG"
echo " Log: $MCC_LOG"
@ -132,7 +141,7 @@ bash "$PREFLIGHT_SCRIPT" "$VERSION" >/dev/null
# --- Build ---
if $DO_BUILD; then
echo "[1/4] Building MCC..."
dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release -v quiet --nologo
_mcc_dotnet_env dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release -v quiet --nologo
echo " Build OK"
else
echo "[1/4] Build skipped (--no-build)"
@ -219,7 +228,7 @@ if [[ "$MODE" == "tui" ]]; then
# TUI mode: needs a real tty - no pipes or redirects allowed
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD; echo '=== MCC EXITED ==='; sleep 600"
"cd '$REPO_ROOT' && ${BUILD_ROOT_ENV_PREFIX}dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD; echo '=== MCC EXITED ==='; sleep 600"
echo ""
echo " TUI mode started in tmux session '$MCC_TMUX_SESSION'"
echo " (TUI mode uses a real terminal; log file is not available, use MCC's /debug command)"
@ -232,7 +241,7 @@ elif $FILE_INPUT; then
# FileInput mode: run in detached tmux, drive via session-specific input file
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && printf '%s\n' \"\$\$\" > '$PID_FILE' && exec env MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD > '$MCC_LOG' 2>&1"
"cd '$REPO_ROOT' && printf '%s\n' \"\$\$\" > '$PID_FILE' && exec env ${BUILD_ROOT_ENV_PREFIX}MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD > '$MCC_LOG' 2>&1"
for _ in $(seq 1 25); do
if [[ -s "$PID_FILE" ]]; then
@ -274,12 +283,13 @@ elif $FILE_INPUT; then
echo " Attach (optional): tmux attach -t $MCC_TMUX_SESSION"
echo " Stop MCC: echo 'quit' >> $INPUT_FILE"
echo " Stop server: mc-stop $VERSION"
echo " shared servers stay up by default; rerun with --confirm only if you really need to stop it"
echo ""
else
# Interactive classic mode: run in tmux (no pipe - ConsoleInteractive also needs tty)
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD; echo '=== MCC EXITED ==='; sleep 600"
"cd '$REPO_ROOT' && ${BUILD_ROOT_ENV_PREFIX}dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD; echo '=== MCC EXITED ==='; sleep 600"
echo ""
echo " Classic mode started in tmux session '$MCC_TMUX_SESSION'"
echo ""
@ -293,4 +303,4 @@ fi
echo "Quick commands:"
echo " mc-rcon 'op $USERNAME' # Give operator"
echo " mc-rcon 'gamemode creative' # Creative mode"
echo " mc-stop $VERSION # Stop server"
echo " mc-stop $VERSION # shared server stays up by default; rerun with --confirm only when needed"

View file

@ -136,16 +136,108 @@ _mcc_dotnet_env() {
# Helper: convert version to tmux session name (dots -> underscores)
_mc-session() { echo "mc-${1//\./_}"; }
_mc_requires_confirm() {
local action="$1"
local rerun_command="$2"
cat >&2 <<EOF
Refusing to ${action} without --confirm.
Keep shared servers running by default. Only stop or reset them when the user explicitly asks, or when you need to switch server versions.
If you really intend to do this, rerun: ${rerun_command} --confirm
EOF
return 1
}
# --- Minecraft Server Management ---
mc-start() { bash "$MCC_REPO/tools/start-server.sh" "${1:-1.20.6}"; }
mc-stop() { local v="${1:-1.20.6}"; echo "stop" > "$MCC_SERVERS/$v/stdin.pipe"; }
mc-stop() {
local v="1.20.6"
local version_set=false
local confirm=false
local -a rerun=(mc-stop)
while [[ $# -gt 0 ]]; do
case "$1" in
--confirm) confirm=true; shift ;;
*)
if [[ "$version_set" == false ]]; then
v="$1"
version_set=true
rerun+=("$1")
shift
else
echo "mc-stop: unexpected argument: $1" >&2
return 1
fi
;;
esac
done
if [[ ${#rerun[@]} -eq 1 ]]; then
rerun+=("$v")
fi
if [[ "$confirm" != true ]]; then
_mc_requires_confirm "stop shared server '$v'" "${rerun[*]}"
return 1
fi
echo "stop" > "$MCC_SERVERS/$v/stdin.pipe"
}
mc-cmd() { local v="${2:-1.20.6}"; echo "$1" > "$MCC_SERVERS/$v/stdin.pipe"; }
mc-log() { local s; s=$(_mc-session "${1:-1.20.6}"); tmux capture-pane -t "$s" -p -S "-${2:-50}"; }
mc-kill() { local v="${1:-1.20.6}" s; s=$(_mc-session "$v"); tmux kill-session -t "$s" 2>/dev/null; rm -f "$MCC_SERVERS/$v/stdin.pipe"; echo "Killed $s"; }
mc-kill() {
local v="1.20.6"
local version_set=false
local confirm=false
local -a rerun=(mc-kill)
while [[ $# -gt 0 ]]; do
case "$1" in
--confirm) confirm=true; shift ;;
*)
if [[ "$version_set" == false ]]; then
v="$1"
version_set=true
rerun+=("$1")
shift
else
echo "mc-kill: unexpected argument: $1" >&2
return 1
fi
;;
esac
done
if [[ ${#rerun[@]} -eq 1 ]]; then
rerun+=("$v")
fi
if [[ "$confirm" != true ]]; then
_mc_requires_confirm "force-kill shared server '$v'" "${rerun[*]}"
return 1
fi
local s
s=$(_mc-session "$v")
tmux kill-session -t "$s" 2>/dev/null
rm -f "$MCC_SERVERS/$v/stdin.pipe"
echo "Killed $s"
}
mc-list() { tmux list-sessions 2>/dev/null | grep "^mc-" || echo "No running MC servers"; }
mc-wait-ready() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "${1:-1.20.6}" >/dev/null && source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_ready "${1:-1.20.6}" "${2:-60}"; }
mc-wait-stop() { source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_stop "${1:-1.20.6}" "${2:-60}"; }
mc-reset-test-env() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "$@"; }
mc-reset-test-env() {
local confirm=false
local -a args=()
local -a rerun=(mc-reset-test-env)
while [[ $# -gt 0 ]]; do
case "$1" in
--confirm) confirm=true; shift ;;
*)
args+=("$1")
rerun+=("$1")
shift
;;
esac
done
if [[ "$confirm" != true ]]; then
_mc_requires_confirm "reset shared server test state" "${rerun[*]}"
return 1
fi
bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "${args[@]}"
}
# --- RCON ---
mc-rcon() { bash "$MCC_REPO/tools/mc-rcon.sh" "$@"; }
@ -167,9 +259,52 @@ mcc-build-clean() {
dotnet clean "$(_mcc_repo_root)/MinecraftClient.sln" -c Release
}
mcc-run() {
local port="${1:-25565}"
shift || true
cd "$MCC_REPO" && _mcc_dotnet_env env MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot - "localhost:${port}" "$@" 2>&1
local session="" username="" port="25565"
while [[ $# -gt 0 ]]; do
case "$1" in
--session)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-run: --session requires a value" >&2
return 1
fi
session="$1"
shift
;;
--username)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-run: --username requires a value" >&2
return 1
fi
username="$1"
shift
;;
--port)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-run: --port requires a value" >&2
return 1
fi
port="$1"
shift
;;
*)
echo "Unknown option: $1" >&2
return 1
;;
esac
done
local -a args=(--file-input --no-build --port "$port")
if [[ -n "$session" ]]; then
args+=(--session "$session")
fi
if [[ -n "$username" ]]; then
args+=(--username "$username")
fi
bash "$MCC_REPO/tools/mcc-debug.sh" "${args[@]}"
}
mcc-cmd() {
local session=""
@ -270,21 +405,52 @@ mcc-reload() {
# --- TUI Mode ---
mcc-tui() {
local port="${1:-25565}"
local cmd_prefix=""
shift || true
local session="" username="" port="25565"
while [[ $# -gt 0 ]]; do
case "$1" in
--session)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-tui: --session requires a value" >&2
return 1
fi
session="$1"
shift
;;
--username)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-tui: --username requires a value" >&2
return 1
fi
username="$1"
shift
;;
--port)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-tui: --port requires a value" >&2
return 1
fi
port="$1"
shift
;;
*)
echo "Unknown option: $1" >&2
return 1
;;
esac
done
if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then
local build_root
build_root="$(_mcc_build_root)"
mkdir -p "$build_root"
cmd_prefix="MCC_BUILD_ROOT='$build_root' "
local -a args=(-m tui --no-build --port "$port")
if [[ -n "$session" ]]; then
args+=(--session "$session")
fi
if [[ -n "$username" ]]; then
args+=(--username "$username")
fi
_mcc_dotnet_env tmux new-session -d -s mcc-debug -x 160 -y 50 \
"cd '$MCC_REPO' && ${cmd_prefix}dotnet run --project MinecraftClient -c Release -- CursorBot - localhost:${port} $* 2>&1; echo '=== MCC EXITED ==='; sleep 600"
echo "TUI mode launched in tmux session 'mcc-debug'"
echo "Attach: tmux attach -t mcc-debug"
bash "$MCC_REPO/tools/mcc-debug.sh" "${args[@]}"
}
_mcc_session_log_tail() {

View file

@ -37,11 +37,12 @@ SESSION_NAME="mc-${SERVER_DIR//./_}"
TEST_ROOT="${TMPDIR:-/tmp}/mcc-creative-e2e/${SERVER_DIR//\//_}"
CFG="$TEST_ROOT/MinecraftClient.$MC_VERSION.ini"
MCC_SESSION="creative-e2e-${SERVER_DIR//[^a-zA-Z0-9]/_}-${PROFILE}"
TEST_USERNAME="CursorBot"
TEST_USERNAME="$(_mcc_resolve_username "$MCC_SESSION")"
MCC_LOG="$(_mcc_session_log_file "$MCC_SESSION")"
PID_FILE="$(_mcc_session_pid_file "$MCC_SESSION")"
MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$MCC_SESSION")"
SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log"
INPUT_FILE="$(_mcc_session_input_file "$MCC_SESSION")"
MCC_PID=""
SERVER_PORT="25565"
mkdir -p "$TEST_ROOT"
@ -65,12 +66,41 @@ wait_for_file_pattern() {
return 1
}
port_is_listening() {
local port="$1"
if ! command -v python3 >/dev/null 2>&1; then
return 1
fi
python3 - "$port" <<'PY'
import socket
import sys
port = int(sys.argv[1])
for addrinfo in socket.getaddrinfo("localhost", port, 0, socket.SOCK_STREAM):
family, socktype, proto, _, sockaddr = addrinfo
try:
sock = socket.socket(family, socktype, proto)
sock.settimeout(0.2)
if sock.connect_ex(sockaddr) == 0:
sock.close()
raise SystemExit(0)
sock.close()
except OSError:
continue
raise SystemExit(1)
PY
}
wait_for_rcon_port_free() {
local timeout="${1:-30}"
local elapsed=0
while (( elapsed < timeout )); do
if ! ss -ltn '( sport = :25575 )' 2>/dev/null | grep -Fq ':25575'; then
if ! port_is_listening 25575; then
return 0
fi
sleep 1
@ -81,17 +111,16 @@ wait_for_rcon_port_free() {
return 1
}
cleanup() {
if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then
mcc-cmd --session "$MCC_SESSION" "quit" >/dev/null 2>&1 || true
sleep 2
fi
mcc-cmd --session "$MCC_SESSION" "quit" >/dev/null 2>&1 || true
sleep 2
mcc-kill --session "$MCC_SESSION" >/dev/null 2>&1 || true
if [[ -p "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" ]]; then
echo "stop" > "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" 2>/dev/null || true
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
tmux send-keys -t "$SESSION_NAME" "stop" C-m >/dev/null 2>&1 || true
wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true
fi
rm -f "$MCC_SERVERS/$SERVER_DIR/stdin.pipe"
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
wait_for_rcon_port_free 30 || true
}
@ -148,6 +177,27 @@ assert_log_contains() {
wait_for_file_pattern "$file" "$pattern" "$description" "$timeout"
}
start_mcc_session() {
local -a mcc_args=("$CFG" "$TEST_USERNAME" "-" "localhost:$SERVER_PORT")
local mcc_args_cmd
mcc_args_cmd="$(printf '%q ' "${mcc_args[@]}")"
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
rm -f "$PID_FILE"
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && printf '%s\n' \"\$\$\" > '$PID_FILE' && exec env MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $mcc_args_cmd > '$MCC_LOG' 2>&1"
for _ in $(seq 1 25); do
if [[ -s "$PID_FILE" ]]; then
return 0
fi
sleep 0.2
done
echo "Failed to capture MCC PID for session $MCC_SESSION" >&2
return 1
}
legacy_server_setup() {
run_server_command "gamerule sendCommandFeedback true"
run_server_command "time set day"
@ -184,7 +234,7 @@ modern_mob_and_effects() {
}
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$SERVER_DIR" >/dev/null
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" --all >/dev/null
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "$SERVER_DIR" >/dev/null
mcc-reset-session --session "$MCC_SESSION" >/dev/null
wait_for_rcon_port_free 30 || true
mkdir -p "$(dirname "$MCC_LOG")" "$(dirname "$INPUT_FILE")"
@ -202,16 +252,7 @@ prepare_config
: > "$INPUT_FILE"
(
cd "$REPO_ROOT"
MCC_FILE_INPUT=1 MCC_INPUT_FILE="$INPUT_FILE" dotnet run --project MinecraftClient -c Release --no-build -- \
"$CFG" \
"$TEST_USERNAME" \
- \
"localhost:$SERVER_PORT" \
> "$MCC_LOG" 2>&1
) &
MCC_PID=$!
start_mcc_session
assert_log_contains "$MCC_LOG" "Server was successfully joined." "MCC join success" 90
assert_log_contains "$SERVER_LOG_FILE" "$TEST_USERNAME joined the game" "server join entry" 30

View file

@ -93,7 +93,7 @@ assert_eq "debug state" "$input_contents" "session input command is intact"
mcc-reset-session --session "$session"
[[ ! -e "$(_mcc_session_root "$session")" ]]
malformed_log="${TMPDIR:-/tmp}/mcc-env-session-hang-test.log"
for func in mcc-cmd mcc-reset-session mcc-state mcc-log-mcc; do
for func in mcc-cmd mcc-reset-session mcc-state mcc-log-mcc mcc-run mcc-tui; do
set +e
"$func" --session >"$malformed_log" 2>&1
status=$?
@ -106,4 +106,35 @@ for func in mcc-cmd mcc-reset-session mcc-state mcc-log-mcc; do
grep -Fq -- "--session requires a value" "$malformed_log"
done
guard_root="$(mktemp -d "${TMPDIR:-/tmp}/mcc-env-guard.XXXXXX")"
MCC_SERVERS="$guard_root"
mkdir -p "$MCC_SERVERS/testver"
printf 'unchanged\n' > "$MCC_SERVERS/testver/stdin.pipe"
confirm_log="${TMPDIR:-/tmp}/mcc-env-confirm-guard.log"
for cmd in \
"mc-stop testver" \
"mc-kill testver" \
"mc-reset-test-env testver"
do
set +e
eval "$cmd" >"$confirm_log" 2>&1
status=$?
set -e
if [[ $status -eq 0 ]]; then
echo "FAIL: $cmd ran without --confirm" >&2
cat "$confirm_log" >&2
exit 1
fi
grep -Fq -- "--confirm" "$confirm_log"
grep -Fq "Keep shared servers running by default" "$confirm_log"
done
stdin_contents="$(cat "$MCC_SERVERS/testver/stdin.pipe")"
assert_eq "unchanged" "$stdin_contents" "mc-stop without confirm does not touch stdin pipe"
: > "$MCC_SERVERS/testver/stdin.pipe"
mc-stop testver --confirm
assert_eq "stop" "$(cat "$MCC_SERVERS/testver/stdin.pipe")" "mc-stop with confirm writes to stdin pipe"
echo "PASS"