From af285866dc6fce79010a57f72a2abf878e0ae9a5 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 23:33:45 +0800 Subject: [PATCH] tools: harden shared server session workflow --- .../mcc-integration-testing/scripts/common.sh | 4 +- .../scripts/ensure_offline_server.sh | 4 +- .../scripts/reset_shared_test_state.sh | 4 +- .../scripts/run_achievements_test.sh | 2 +- .../scripts/run_full_spectrum_test.sh | 37 +++- .../run_parallel_session_smoke_test.sh | 8 +- tools/mcc-debug.sh | 20 +- tools/mcc-env.sh | 202 ++++++++++++++++-- tools/run-creative-e2e.sh | 81 +++++-- tools/test-mcc-env.sh | 33 ++- 10 files changed, 331 insertions(+), 64 deletions(-) diff --git a/.skills/mcc-integration-testing/scripts/common.sh b/.skills/mcc-integration-testing/scripts/common.sh index 973b5da3..ac5c0250 100755 --- a/.skills/mcc-integration-testing/scripts/common.sh +++ b/.skills/mcc-integration-testing/scripts/common.sh @@ -73,7 +73,7 @@ wait_for_server_stop() { ((elapsed += 1)) done - mc-kill "$version" >/dev/null 2>&1 || true + mc-kill "$version" --confirm >/dev/null 2>&1 || true if ! server_running "$version"; then return 0 @@ -104,7 +104,7 @@ remove_stale_stdin_pipe() { local version="$1" 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" fi } diff --git a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh index 38e73978..8ede375b 100755 --- a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh +++ b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh @@ -41,12 +41,12 @@ upsert_property() { if [[ ! -f "$PROPS_FILE" ]]; then mc-start "$VERSION" wait_for_server_ready "$VERSION" - mc-stop "$VERSION" + mc-stop "$VERSION" --confirm wait_for_server_stop "$VERSION" fi if server_running; then - mc-stop "$VERSION" + mc-stop "$VERSION" --confirm wait_for_server_stop "$VERSION" fi diff --git a/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh index f3945e1a..eab7c694 100755 --- a/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh +++ b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh @@ -34,9 +34,7 @@ if [[ $# -eq 0 || "${1:-}" == "--all" ]]; then while IFS= read -r pipe_path; do [[ -z "$pipe_path" ]] && continue - if [[ ! -p "$pipe_path" ]]; then - rm -f "$pipe_path" - fi + rm -f "$pipe_path" done < <(find "$MCC_SERVERS" -maxdepth 2 -name 'stdin.pipe' 2>/dev/null || true) else for version in "$@"; do diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh index df0236e0..4ea0c3ec 100755 --- a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh @@ -124,7 +124,7 @@ cleanup() { wait "$MCC_PID" 2>/dev/null || true 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 ln -sfn "$RUN_DIR" "$LATEST_LINK" write_summary diff --git a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh index 4cfb5ae9..181862a8 100755 --- a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh @@ -18,14 +18,15 @@ RUN_ID="$(date +%Y%m%d-%H%M%S)" RUN_DIR="$RUN_ROOT/$RUN_ID" SERVER_LOG_FILE="$MCC_SERVERS/$VERSION/logs/latest.log" 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")" +PID_FILE="$(_mcc_session_pid_file "$SESSION_NAME")" +MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$SESSION_NAME")" BUILD_LOG="$RUN_DIR/build.log" SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log" SERVER_FILE_LOG="$RUN_DIR/server-latest.log" INPUT_FILE="$(_mcc_session_input_file "$SESSION_NAME")" CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini" -MCC_PID="" mkdir -p "$RUN_DIR" @@ -34,7 +35,7 @@ cleanup() { sleep 2 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 } trap cleanup EXIT @@ -132,6 +133,26 @@ run_mcc_command() { 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/reset_shared_test_state.sh" "$VERSION" >/dev/null "$SCRIPT_DIR/ensure_offline_server.sh" "$VERSION" @@ -140,6 +161,9 @@ echo "Building MCC..." mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed" prepare_config 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")" : > "$INPUT_FILE" @@ -150,12 +174,7 @@ mc-start "$VERSION" >/dev/null wait_for_server_ready "$VERSION" || fail "Server did not become ready" echo "Starting MCC..." -( - 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 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" diff --git a/.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh b/.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh index 86535fe9..9f422d28 100755 --- a/.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh @@ -93,7 +93,7 @@ cleanup() { sleep 1 mcc-kill --session "$SESSION_A" >/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 } trap cleanup EXIT @@ -119,8 +119,10 @@ assert_server_alive() { start_file_input_session() { local session="$1" local username="$2" + local port="$3" bash "$REPO_ROOT/tools/mcc-debug.sh" \ --version "$VERSION" \ + --port "$port" \ --file-input \ --no-build \ --session "$session" \ @@ -145,9 +147,9 @@ if [[ -z "$SERVER_PORT" ]]; then fi 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..." -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_B" "Server was successfully joined." "session B join success" 90 || fail "Session B failed to join" diff --git a/tools/mcc-debug.sh b/tools/mcc-debug.sh index a96bfc7c..d0ca0e78 100644 --- a/tools/mcc-debug.sh +++ b/tools/mcc-debug.sh @@ -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" diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index b3825529..217f18cc 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -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 < "$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() { diff --git a/tools/run-creative-e2e.sh b/tools/run-creative-e2e.sh index b264c9e9..ea25a464 100644 --- a/tools/run-creative-e2e.sh +++ b/tools/run-creative-e2e.sh @@ -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 diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh index 2a6c8d5b..38c50c84 100755 --- a/tools/test-mcc-env.sh +++ b/tools/test-mcc-env.sh @@ -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"