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

@ -73,7 +73,7 @@ wait_for_server_stop() {
((elapsed += 1)) ((elapsed += 1))
done done
mc-kill "$version" >/dev/null 2>&1 || true mc-kill "$version" --confirm >/dev/null 2>&1 || true
if ! server_running "$version"; then if ! server_running "$version"; then
return 0 return 0
@ -104,7 +104,7 @@ remove_stale_stdin_pipe() {
local version="$1" local version="$1"
local pipe_path="$MCC_SERVERS/$version/stdin.pipe" local pipe_path="$MCC_SERVERS/$version/stdin.pipe"
if [[ -e "$pipe_path" && ! -p "$pipe_path" ]]; then if [[ -e "$pipe_path" ]] && ! server_running "$version"; then
rm -f "$pipe_path" rm -f "$pipe_path"
fi fi
} }

View file

@ -41,12 +41,12 @@ upsert_property() {
if [[ ! -f "$PROPS_FILE" ]]; then if [[ ! -f "$PROPS_FILE" ]]; then
mc-start "$VERSION" mc-start "$VERSION"
wait_for_server_ready "$VERSION" wait_for_server_ready "$VERSION"
mc-stop "$VERSION" mc-stop "$VERSION" --confirm
wait_for_server_stop "$VERSION" wait_for_server_stop "$VERSION"
fi fi
if server_running; then if server_running; then
mc-stop "$VERSION" mc-stop "$VERSION" --confirm
wait_for_server_stop "$VERSION" wait_for_server_stop "$VERSION"
fi fi

View file

@ -34,9 +34,7 @@ if [[ $# -eq 0 || "${1:-}" == "--all" ]]; then
while IFS= read -r pipe_path; do while IFS= read -r pipe_path; do
[[ -z "$pipe_path" ]] && continue [[ -z "$pipe_path" ]] && continue
if [[ ! -p "$pipe_path" ]]; then
rm -f "$pipe_path" rm -f "$pipe_path"
fi
done < <(find "$MCC_SERVERS" -maxdepth 2 -name 'stdin.pipe' 2>/dev/null || true) done < <(find "$MCC_SERVERS" -maxdepth 2 -name 'stdin.pipe' 2>/dev/null || true)
else else
for version in "$@"; do for version in "$@"; do

View file

@ -124,7 +124,7 @@ cleanup() {
wait "$MCC_PID" 2>/dev/null || true wait "$MCC_PID" 2>/dev/null || true
fi fi
mc-stop "$SERVER_DIR" >/dev/null 2>&1 || true mc-stop "$SERVER_DIR" --confirm >/dev/null 2>&1 || true
wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true
ln -sfn "$RUN_DIR" "$LATEST_LINK" ln -sfn "$RUN_DIR" "$LATEST_LINK"
write_summary write_summary

View file

@ -18,14 +18,15 @@ RUN_ID="$(date +%Y%m%d-%H%M%S)"
RUN_DIR="$RUN_ROOT/$RUN_ID" RUN_DIR="$RUN_ROOT/$RUN_ID"
SERVER_LOG_FILE="$MCC_SERVERS/$VERSION/logs/latest.log" SERVER_LOG_FILE="$MCC_SERVERS/$VERSION/logs/latest.log"
SESSION_NAME="full-spectrum-${MC_VERSION//[^a-zA-Z0-9]/_}" SESSION_NAME="full-spectrum-${MC_VERSION//[^a-zA-Z0-9]/_}"
TEST_USERNAME="CursorBot" TEST_USERNAME="$(_mcc_resolve_username "$SESSION_NAME")"
MCC_LOG="$(_mcc_session_log_file "$SESSION_NAME")" MCC_LOG="$(_mcc_session_log_file "$SESSION_NAME")"
PID_FILE="$(_mcc_session_pid_file "$SESSION_NAME")"
MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$SESSION_NAME")"
BUILD_LOG="$RUN_DIR/build.log" BUILD_LOG="$RUN_DIR/build.log"
SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log" SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log"
SERVER_FILE_LOG="$RUN_DIR/server-latest.log" SERVER_FILE_LOG="$RUN_DIR/server-latest.log"
INPUT_FILE="$(_mcc_session_input_file "$SESSION_NAME")" INPUT_FILE="$(_mcc_session_input_file "$SESSION_NAME")"
CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini" CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini"
MCC_PID=""
mkdir -p "$RUN_DIR" mkdir -p "$RUN_DIR"
@ -34,7 +35,7 @@ cleanup() {
sleep 2 sleep 2
mcc-kill --session "$SESSION_NAME" >/dev/null 2>&1 || true mcc-kill --session "$SESSION_NAME" >/dev/null 2>&1 || true
mc-stop "$VERSION" >/dev/null 2>&1 || true mc-stop "$VERSION" --confirm >/dev/null 2>&1 || true
wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true
} }
trap cleanup EXIT trap cleanup EXIT
@ -132,6 +133,26 @@ run_mcc_command() {
sleep 2 sleep 2
} }
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
fail "Failed to capture MCC PID for session $SESSION_NAME"
}
bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null
bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null
"$SCRIPT_DIR/ensure_offline_server.sh" "$VERSION" "$SCRIPT_DIR/ensure_offline_server.sh" "$VERSION"
@ -140,6 +161,9 @@ echo "Building MCC..."
mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed" mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed"
prepare_config prepare_config
SERVER_PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$VERSION")" SERVER_PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$VERSION")"
if [[ -z "$SERVER_PORT" ]]; then
fail "Failed to resolve server port"
fi
mkdir -p "$(dirname "$INPUT_FILE")" "$(dirname "$MCC_LOG")" mkdir -p "$(dirname "$INPUT_FILE")" "$(dirname "$MCC_LOG")"
: > "$INPUT_FILE" : > "$INPUT_FILE"
@ -150,12 +174,7 @@ mc-start "$VERSION" >/dev/null
wait_for_server_ready "$VERSION" || fail "Server did not become ready" wait_for_server_ready "$VERSION" || fail "Server did not become ready"
echo "Starting MCC..." echo "Starting MCC..."
( start_mcc_session
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=$!
wait_for_file_pattern "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 || fail "MCC failed to join" wait_for_file_pattern "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 || fail "MCC failed to join"
wait_for_server_log_pattern "$TEST_USERNAME joined the game" "server join entry" 30 || fail "Server never logged the join" wait_for_server_log_pattern "$TEST_USERNAME joined the game" "server join entry" 30 || fail "Server never logged the join"

View file

@ -93,7 +93,7 @@ cleanup() {
sleep 1 sleep 1
mcc-kill --session "$SESSION_A" >/dev/null 2>&1 || true mcc-kill --session "$SESSION_A" >/dev/null 2>&1 || true
mcc-kill --session "$SESSION_B" >/dev/null 2>&1 || true mcc-kill --session "$SESSION_B" >/dev/null 2>&1 || true
mc-stop "$VERSION" >/dev/null 2>&1 || true mc-stop "$VERSION" --confirm >/dev/null 2>&1 || true
wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true
} }
trap cleanup EXIT trap cleanup EXIT
@ -119,8 +119,10 @@ assert_server_alive() {
start_file_input_session() { start_file_input_session() {
local session="$1" local session="$1"
local username="$2" local username="$2"
local port="$3"
bash "$REPO_ROOT/tools/mcc-debug.sh" \ bash "$REPO_ROOT/tools/mcc-debug.sh" \
--version "$VERSION" \ --version "$VERSION" \
--port "$port" \
--file-input \ --file-input \
--no-build \ --no-build \
--session "$session" \ --session "$session" \
@ -145,9 +147,9 @@ if [[ -z "$SERVER_PORT" ]]; then
fi fi
echo "Starting MCC session A..." echo "Starting MCC session A..."
start_file_input_session "$SESSION_A" "$USERNAME_A" start_file_input_session "$SESSION_A" "$USERNAME_A" "$SERVER_PORT"
echo "Starting MCC session B..." echo "Starting MCC session B..."
start_file_input_session "$SESSION_B" "$USERNAME_B" start_file_input_session "$SESSION_B" "$USERNAME_B" "$SERVER_PORT"
wait_for_file_pattern "$LOG_A" "Server was successfully joined." "session A join success" 90 || fail "Session A failed to join" wait_for_file_pattern "$LOG_A" "Server was successfully joined." "session A join success" 90 || fail "Session A failed to join"
wait_for_file_pattern "$LOG_B" "Server was successfully joined." "session B join success" 90 || fail "Session B failed to join" wait_for_file_pattern "$LOG_B" "Server was successfully joined." "session B join success" 90 || fail "Session B failed to join"

View file

@ -41,6 +41,8 @@ USERNAME=""
DO_BUILD=true DO_BUILD=true
DEBUG_ON=false DEBUG_ON=false
FILE_INPUT=false FILE_INPUT=false
BUILD_ROOT="$(_mcc_build_root)"
BUILD_ROOT_ENV_PREFIX=""
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@ -98,6 +100,12 @@ if [[ -z "$USERNAME" ]]; then
USERNAME="$(_mcc_resolve_username "$SESSION")" USERNAME="$(_mcc_resolve_username "$SESSION")"
fi 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")" SESSION_ROOT="$(_mcc_session_root "$SESSION")"
CFG="$SESSION_ROOT/MinecraftClient.debug.ini" CFG="$SESSION_ROOT/MinecraftClient.debug.ini"
MCC_LOG="$(_mcc_session_log_file "$SESSION")" MCC_LOG="$(_mcc_session_log_file "$SESSION")"
@ -118,6 +126,7 @@ echo " Server: $VERSION (port $PORT)"
echo " Session: $SESSION" echo " Session: $SESSION"
echo " User: $USERNAME" echo " User: $USERNAME"
echo " Mode: $MODE" echo " Mode: $MODE"
echo " Build: $BUILD_ROOT"
echo " Root: $SESSION_ROOT" echo " Root: $SESSION_ROOT"
echo " Config: $CFG" echo " Config: $CFG"
echo " Log: $MCC_LOG" echo " Log: $MCC_LOG"
@ -132,7 +141,7 @@ bash "$PREFLIGHT_SCRIPT" "$VERSION" >/dev/null
# --- Build --- # --- Build ---
if $DO_BUILD; then if $DO_BUILD; then
echo "[1/4] Building MCC..." 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" echo " Build OK"
else else
echo "[1/4] Build skipped (--no-build)" 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 # TUI mode: needs a real tty - no pipes or redirects allowed
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \ 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 ""
echo " TUI mode started in tmux session '$MCC_TMUX_SESSION'" 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)" 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 # FileInput mode: run in detached tmux, drive via session-specific input file
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \ 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 for _ in $(seq 1 25); do
if [[ -s "$PID_FILE" ]]; then if [[ -s "$PID_FILE" ]]; then
@ -274,12 +283,13 @@ elif $FILE_INPUT; then
echo " Attach (optional): tmux attach -t $MCC_TMUX_SESSION" echo " Attach (optional): tmux attach -t $MCC_TMUX_SESSION"
echo " Stop MCC: echo 'quit' >> $INPUT_FILE" echo " Stop MCC: echo 'quit' >> $INPUT_FILE"
echo " Stop server: mc-stop $VERSION" 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 "" echo ""
else else
# Interactive classic mode: run in tmux (no pipe - ConsoleInteractive also needs tty) # Interactive classic mode: run in tmux (no pipe - ConsoleInteractive also needs tty)
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \ 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 ""
echo " Classic mode started in tmux session '$MCC_TMUX_SESSION'" echo " Classic mode started in tmux session '$MCC_TMUX_SESSION'"
echo "" echo ""
@ -293,4 +303,4 @@ fi
echo "Quick commands:" echo "Quick commands:"
echo " mc-rcon 'op $USERNAME' # Give operator" echo " mc-rcon 'op $USERNAME' # Give operator"
echo " mc-rcon 'gamemode creative' # Creative mode" 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) # Helper: convert version to tmux session name (dots -> underscores)
_mc-session() { echo "mc-${1//\./_}"; } _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 --- # --- Minecraft Server Management ---
mc-start() { bash "$MCC_REPO/tools/start-server.sh" "${1:-1.20.6}"; } 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-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-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-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-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-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 --- # --- RCON ---
mc-rcon() { bash "$MCC_REPO/tools/mc-rcon.sh" "$@"; } 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 dotnet clean "$(_mcc_repo_root)/MinecraftClient.sln" -c Release
} }
mcc-run() { mcc-run() {
local port="${1:-25565}" local session="" username="" port="25565"
shift || true while [[ $# -gt 0 ]]; do
cd "$MCC_REPO" && _mcc_dotnet_env env MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot - "localhost:${port}" "$@" 2>&1 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() { mcc-cmd() {
local session="" local session=""
@ -270,21 +405,52 @@ mcc-reload() {
# --- TUI Mode --- # --- TUI Mode ---
mcc-tui() { mcc-tui() {
local port="${1:-25565}" local session="" username="" port="25565"
local cmd_prefix="" while [[ $# -gt 0 ]]; do
shift || true 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 -a args=(-m tui --no-build --port "$port")
local build_root if [[ -n "$session" ]]; then
build_root="$(_mcc_build_root)" args+=(--session "$session")
mkdir -p "$build_root" fi
cmd_prefix="MCC_BUILD_ROOT='$build_root' " if [[ -n "$username" ]]; then
args+=(--username "$username")
fi fi
_mcc_dotnet_env tmux new-session -d -s mcc-debug -x 160 -y 50 \ bash "$MCC_REPO/tools/mcc-debug.sh" "${args[@]}"
"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"
} }
_mcc_session_log_tail() { _mcc_session_log_tail() {

View file

@ -37,11 +37,12 @@ SESSION_NAME="mc-${SERVER_DIR//./_}"
TEST_ROOT="${TMPDIR:-/tmp}/mcc-creative-e2e/${SERVER_DIR//\//_}" TEST_ROOT="${TMPDIR:-/tmp}/mcc-creative-e2e/${SERVER_DIR//\//_}"
CFG="$TEST_ROOT/MinecraftClient.$MC_VERSION.ini" CFG="$TEST_ROOT/MinecraftClient.$MC_VERSION.ini"
MCC_SESSION="creative-e2e-${SERVER_DIR//[^a-zA-Z0-9]/_}-${PROFILE}" 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")" 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" SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log"
INPUT_FILE="$(_mcc_session_input_file "$MCC_SESSION")" INPUT_FILE="$(_mcc_session_input_file "$MCC_SESSION")"
MCC_PID=""
SERVER_PORT="25565" SERVER_PORT="25565"
mkdir -p "$TEST_ROOT" mkdir -p "$TEST_ROOT"
@ -65,12 +66,41 @@ wait_for_file_pattern() {
return 1 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() { wait_for_rcon_port_free() {
local timeout="${1:-30}" local timeout="${1:-30}"
local elapsed=0 local elapsed=0
while (( elapsed < timeout )); do while (( elapsed < timeout )); do
if ! ss -ltn '( sport = :25575 )' 2>/dev/null | grep -Fq ':25575'; then if ! port_is_listening 25575; then
return 0 return 0
fi fi
sleep 1 sleep 1
@ -81,17 +111,16 @@ wait_for_rcon_port_free() {
return 1 return 1
} }
cleanup() { cleanup() {
if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then
mcc-cmd --session "$MCC_SESSION" "quit" >/dev/null 2>&1 || true mcc-cmd --session "$MCC_SESSION" "quit" >/dev/null 2>&1 || true
sleep 2 sleep 2
fi
mcc-kill --session "$MCC_SESSION" >/dev/null 2>&1 || true mcc-kill --session "$MCC_SESSION" >/dev/null 2>&1 || true
if [[ -p "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" ]]; then if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
echo "stop" > "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" 2>/dev/null || true 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 wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true
fi fi
rm -f "$MCC_SERVERS/$SERVER_DIR/stdin.pipe"
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
wait_for_rcon_port_free 30 || true wait_for_rcon_port_free 30 || true
} }
@ -148,6 +177,27 @@ assert_log_contains() {
wait_for_file_pattern "$file" "$pattern" "$description" "$timeout" 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() { legacy_server_setup() {
run_server_command "gamerule sendCommandFeedback true" run_server_command "gamerule sendCommandFeedback true"
run_server_command "time set day" 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/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 mcc-reset-session --session "$MCC_SESSION" >/dev/null
wait_for_rcon_port_free 30 || true wait_for_rcon_port_free 30 || true
mkdir -p "$(dirname "$MCC_LOG")" "$(dirname "$INPUT_FILE")" mkdir -p "$(dirname "$MCC_LOG")" "$(dirname "$INPUT_FILE")"
@ -202,16 +252,7 @@ prepare_config
: > "$INPUT_FILE" : > "$INPUT_FILE"
( start_mcc_session
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=$!
assert_log_contains "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 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 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" mcc-reset-session --session "$session"
[[ ! -e "$(_mcc_session_root "$session")" ]] [[ ! -e "$(_mcc_session_root "$session")" ]]
malformed_log="${TMPDIR:-/tmp}/mcc-env-session-hang-test.log" 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 set +e
"$func" --session >"$malformed_log" 2>&1 "$func" --session >"$malformed_log" 2>&1
status=$? 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" grep -Fq -- "--session requires a value" "$malformed_log"
done 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" echo "PASS"