From 25bbe35718bb31e02527e880731cb91b7f8d498c Mon Sep 17 00:00:00 2001 From: Anon Date: Fri, 17 Jul 2026 19:16:00 +0200 Subject: [PATCH 1/2] fix: exit on failure instead of prompting --- MinecraftClient/McClient.cs | 61 +++++----- MinecraftClient/Program.cs | 58 ++++++---- docs/guide/configuration.md | 2 +- tools/run-exit-on-failure-test.sh | 178 ++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 49 deletions(-) create mode 100644 tools/run-exit-on-failure-test.sh diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 753de486..82dfa741 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -368,6 +368,13 @@ namespace MinecraftClient timeoutdetector = null; } + if (!InternalConfig.InteractiveMode) + { + StopConsoleSession(); + Program.HandleFailure(null, false, ChatBot.DisconnectReason.ConnectionLost); + return; + } + if (!Config.ChatBot.AutoRelog.Enabled) { if (ReconnectionAttemptsLeft > 0) @@ -376,33 +383,22 @@ namespace MinecraftClient Thread.Sleep(5000); ReconnectionAttemptsLeft--; Program.Restart(); - } - else if (InternalConfig.InteractiveMode) - { - StopConsoleSession(); - Program.HandleFailure(); + return; } - throw new Exception("Initialization failed."); + StopConsoleSession(); + Program.HandleFailure(); + return; } - else - { - // AutoRelog is enabled - invoke its static handler to trigger reconnection. - // Use the same "Connection has been lost" message that OnConnectionLost uses - // for ConnectionLost, so it matches the default Kick_Messages. - if (AutoRelog.OnDisconnectStatic(ChatBot.DisconnectReason.ConnectionLost, Translations.mcc_disconnect_lost)) - return; // AutoRelog is triggering a restart - // AutoRelog chose not to reconnect (e.g., message didn't match - // kick messages and Ignore_Kick_Message is false, or retry limit reached) - if (InternalConfig.InteractiveMode) - { - StopConsoleSession(); - Program.HandleFailure(); - } + // AutoRelog is enabled - invoke its static handler to trigger reconnection. + // Use the same "Connection has been lost" message that OnConnectionLost uses + // for ConnectionLost, so it matches the default Kick_Messages. + if (AutoRelog.OnDisconnectStatic(ChatBot.DisconnectReason.ConnectionLost, Translations.mcc_disconnect_lost)) + return; - throw new Exception("Initialization failed."); - } + StopConsoleSession(); + Program.HandleFailure(); } public void Transfer(string newHost, int newPort) @@ -493,20 +489,24 @@ namespace MinecraftClient timeoutdetector = null; } + if (!InternalConfig.InteractiveMode) + { + StopConsoleSession(); + Program.HandleFailure(null, false, ChatBot.DisconnectReason.ConnectionLost); + return; + } + if (ReconnectionAttemptsLeft > 0) { Log.Info($"Reconnecting... Attempts left: {ReconnectionAttemptsLeft}"); Thread.Sleep(5000); ReconnectionAttemptsLeft--; Program.Restart(); - } - else if (InternalConfig.InteractiveMode) - { - StopConsoleSession(); - Program.HandleFailure(); + return; } - throw new Exception("Transfer failed and reconnection attempts exhausted.", ex); + StopConsoleSession(); + Program.HandleFailure(); } finally { @@ -919,6 +919,7 @@ namespace MinecraftClient timeoutdetector = null; } + bool exitOnFailure = Program.PrepareExitOnFailure(); bool will_restart = false; switch (reason) @@ -950,7 +951,9 @@ namespace MinecraftClient { try { - will_restart |= bot.OnDisconnect(reason, message); + bool botWillRestart = bot.OnDisconnect(reason, message); + if (!exitOnFailure) + will_restart |= botWillRestart; } catch (Exception e) { diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 71ba3142..5408f6b7 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -56,6 +56,7 @@ namespace MinecraftClient private static bool useMcVersionOnce = false; private static Thread? _restartThread = null; private static readonly object _restartLock = new(); + private static int exitOnFailurePending; private static string settingsIniPath = "MinecraftClient.ini"; // [SENTRY] @@ -916,6 +917,9 @@ namespace MinecraftClient { lock (_restartLock) { + if (Volatile.Read(ref exitOnFailurePending) != 0) + return false; + if (HasRestartPendingForAnotherThreadNoLock()) return false; @@ -962,6 +966,19 @@ namespace MinecraftClient && _restartThread != Thread.CurrentThread; } + /// + /// Marks the current failure as terminal when MCC is running under an external supervisor. + /// Further restart requests are rejected so disconnect cleanup cannot revive the process. + /// + internal static bool PrepareExitOnFailure() + { + if (InternalConfig.InteractiveMode) + return false; + + Interlocked.Exchange(ref exitOnFailurePending, 1); + return true; + } + public static void DoExit(int exitcode = 0) { WriteBackSettings(); @@ -1015,13 +1032,19 @@ namespace MinecraftClient catch { } } ConsoleIO.WriteLine(errorMessage); + } - if (disconnectReason.HasValue) - { - autoRelogHandled = true; - if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage)) - return; - } + if (PrepareExitOnFailure()) + { + Exit(GetFailureExitCode(disconnectReason)); + return; + } + + if (!string.IsNullOrEmpty(errorMessage) && disconnectReason.HasValue) + { + autoRelogHandled = true; + if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage)) + return; } if (InternalConfig.InteractiveMode) @@ -1123,20 +1146,17 @@ namespace MinecraftClient offlinePrompt.Item1.Start(); } } - else - { - // Not in interactive mode, just exit and let the calling script handle the failure - if (disconnectReason.HasValue) - { - // Return distinct exit codes for known failures. - if (disconnectReason.Value == ChatBot.DisconnectReason.UserLogout) Exit(1); - if (disconnectReason.Value == ChatBot.DisconnectReason.InGameKick) Exit(2); - if (disconnectReason.Value == ChatBot.DisconnectReason.ConnectionLost) Exit(3); - if (disconnectReason.Value == ChatBot.DisconnectReason.LoginRejected) Exit(4); - } - Exit(); - } + } + private static int GetFailureExitCode(ChatBot.DisconnectReason? disconnectReason) + { + return disconnectReason switch + { + ChatBot.DisconnectReason.InGameKick => 2, + ChatBot.DisconnectReason.ConnectionLost => 3, + ChatBot.DisconnectReason.LoginRejected => 4, + _ => 1, + }; } /// diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 4a1b594b..008f22d3 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -685,7 +685,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Description:** - This setting allows you to define if your want to disable pauses on error, for using MCC in non-interactive scripts + Exit immediately with a nonzero status when a connection or login failure occurs. This bypasses MCC reconnect handling, including AutoRelog, so an external supervisor can restart MCC. - **Type:** `boolean` diff --git a/tools/run-exit-on-failure-test.sh b/tools/run-exit-on-failure-test.sh new file mode 100644 index 00000000..14426045 --- /dev/null +++ b/tools/run-exit-on-failure-test.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +# shellcheck source=tools/mcc-env.sh +source "$REPO_ROOT/tools/mcc-env.sh" + +VERSION="${1:-26.1-Vanilla}" +MC_VERSION="${VERSION%-Vanilla}" +RUN_DIR="${TMPDIR:-/tmp}/mcc-exit-on-failure/$(date +%Y%m%d-%H%M%S)" +PREPARE_CONFIG="$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" +ENSURE_SERVER="$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" +GET_SERVER_PORT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh" +TEST_USERNAME="mcc_exit_failure" +CURRENT_PID="" + +mkdir -p "$RUN_DIR" + +cleanup() { + if [[ -n "$CURRENT_PID" ]] && kill -0 "$CURRENT_PID" 2>/dev/null; then + kill "$CURRENT_PID" 2>/dev/null || true + wait "$CURRENT_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +fail() { + echo "FAIL: $1" >&2 + echo "Run directory: $RUN_DIR" >&2 + exit 1 +} + +wait_for_log() { + local log_file="$1" + local pattern="$2" + local description="$3" + local timeout_seconds="${4:-60}" + local elapsed=0 + + while (( elapsed < timeout_seconds )); do + if [[ -f "$log_file" ]] && grep -Fq "$pattern" "$log_file"; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + fail "Timed out waiting for $description" +} + +wait_for_exit() { + local pid="$1" + local timeout_seconds="$2" + local elapsed=0 + + while kill -0 "$pid" 2>/dev/null; do + if (( elapsed >= timeout_seconds )); then + return 124 + fi + sleep 1 + ((elapsed += 1)) + done + + wait "$pid" +} + +start_mcc() { + local config="$1" + local port="$2" + local log_file="$3" + local input_file="$4" + + : > "$input_file" + MCC_FILE_INPUT=1 MCC_INPUT_FILE="$input_file" \ + _mcc_dotnet_env dotnet run --project "$REPO_ROOT/MinecraftClient" -c Release --no-build -- \ + "$config" "$TEST_USERNAME" "-" "localhost:$port" > "$log_file" 2>&1 & + CURRENT_PID=$! +} + +prepare_config() { + local output="$1" + bash "$PREPARE_CONFIG" "$output" "$MC_VERSION" "$TEST_USERNAME" >/dev/null +} + +enable_exit_on_failure() { + local config="$1" + sed -i 's/^ExitOnFailure = false/ExitOnFailure = true/' "$config" + sed -i '/^\[ChatBot.AutoRelog\]/,/^\[/ s/^Enabled = false/Enabled = true/' "$config" +} + +reserve_unused_port() { + python3 -c 'import socket; sock = socket.socket(); sock.bind(("127.0.0.1", 0)); print(sock.getsockname()[1]); sock.close()' +} + +run_server_command() { + local command="$1" + local attempt + + for attempt in 1 2 3 4 5; do + if mc-rcon "$command"; then + return 0 + fi + sleep 1 + done + + fail "RCON command failed: $command" +} + +bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$VERSION" +bash "$ENSURE_SERVER" "$VERSION" +mcc-build +mc-start "$VERSION" >/dev/null + +SERVER_PORT="$(bash "$GET_SERVER_PORT" "$VERSION")" +[[ -n "$SERVER_PORT" ]] || fail "Unable to resolve the server port" + +for attempt in 1 2 3 4 5; do + if run_server_command "list"; then + break + fi + sleep 1 + if [[ "$attempt" == 5 ]]; then + fail "RCON did not become ready" + fi +done + +echo "[1/3] Server kick exits promptly and bypasses AutoRelog" +KICK_CONFIG="$RUN_DIR/kick.ini" +KICK_LOG="$RUN_DIR/kick.log" +KICK_INPUT="$RUN_DIR/kick.input" +prepare_config "$KICK_CONFIG" +enable_exit_on_failure "$KICK_CONFIG" +start_mcc "$KICK_CONFIG" "$SERVER_PORT" "$KICK_LOG" "$KICK_INPUT" +wait_for_log "$KICK_LOG" "Server was successfully joined." "MCC join" +run_server_command "kick $TEST_USERNAME ExitOnFailure integration test" + +if wait_for_exit "$CURRENT_PID" 15; then + exit_code=0 +else + exit_code=$? +fi +[[ "$exit_code" != 124 ]] || fail "MCC did not exit after the server kick" +CURRENT_PID="" +[[ "$exit_code" == 2 || "$exit_code" == 3 ]] || fail "Expected nonzero kick exit code 2 or 3, got $exit_code" +[[ "$(grep -Fc "Server was successfully joined." "$KICK_LOG")" == 1 ]] || fail "MCC rejoined after a kick" + +echo "[2/3] Refused initial TCP connection exits with code 3" +REFUSED_CONFIG="$RUN_DIR/refused.ini" +REFUSED_LOG="$RUN_DIR/refused.log" +REFUSED_INPUT="$RUN_DIR/refused.input" +prepare_config "$REFUSED_CONFIG" +enable_exit_on_failure "$REFUSED_CONFIG" +UNUSED_PORT="$(reserve_unused_port)" +start_mcc "$REFUSED_CONFIG" "$UNUSED_PORT" "$REFUSED_LOG" "$REFUSED_INPUT" + +if wait_for_exit "$CURRENT_PID" 15; then + exit_code=0 +else + exit_code=$? +fi +[[ "$exit_code" != 124 ]] || fail "MCC did not exit after the refused connection" +CURRENT_PID="" +[[ "$exit_code" == 3 ]] || fail "Expected connection-loss exit code 3, got $exit_code" + +echo "[3/3] Interactive mode remains at the offline prompt after a kick" +INTERACTIVE_CONFIG="$RUN_DIR/interactive.ini" +INTERACTIVE_LOG="$RUN_DIR/interactive.log" +INTERACTIVE_INPUT="$RUN_DIR/interactive.input" +prepare_config "$INTERACTIVE_CONFIG" +start_mcc "$INTERACTIVE_CONFIG" "$SERVER_PORT" "$INTERACTIVE_LOG" "$INTERACTIVE_INPUT" +wait_for_log "$INTERACTIVE_LOG" "Server was successfully joined." "interactive MCC join" +run_server_command "kick $TEST_USERNAME Interactive-mode regression test" +sleep 5 +kill -0 "$CURRENT_PID" 2>/dev/null || fail "Interactive MCC exited after a kick" + +echo "PASS: ExitOnFailure integration test" +echo "Run directory: $RUN_DIR" From fceed9b4d7d7245599caa061f024c3e3334a4bca Mon Sep 17 00:00:00 2001 From: Anon Date: Sun, 19 Jul 2026 11:23:31 +0200 Subject: [PATCH 2/2] chore: remove run-exit-on-failure-test.sh --- tools/run-exit-on-failure-test.sh | 178 ------------------------------ 1 file changed, 178 deletions(-) delete mode 100644 tools/run-exit-on-failure-test.sh diff --git a/tools/run-exit-on-failure-test.sh b/tools/run-exit-on-failure-test.sh deleted file mode 100644 index 14426045..00000000 --- a/tools/run-exit-on-failure-test.sh +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -# shellcheck source=tools/mcc-env.sh -source "$REPO_ROOT/tools/mcc-env.sh" - -VERSION="${1:-26.1-Vanilla}" -MC_VERSION="${VERSION%-Vanilla}" -RUN_DIR="${TMPDIR:-/tmp}/mcc-exit-on-failure/$(date +%Y%m%d-%H%M%S)" -PREPARE_CONFIG="$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" -ENSURE_SERVER="$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" -GET_SERVER_PORT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh" -TEST_USERNAME="mcc_exit_failure" -CURRENT_PID="" - -mkdir -p "$RUN_DIR" - -cleanup() { - if [[ -n "$CURRENT_PID" ]] && kill -0 "$CURRENT_PID" 2>/dev/null; then - kill "$CURRENT_PID" 2>/dev/null || true - wait "$CURRENT_PID" 2>/dev/null || true - fi -} -trap cleanup EXIT - -fail() { - echo "FAIL: $1" >&2 - echo "Run directory: $RUN_DIR" >&2 - exit 1 -} - -wait_for_log() { - local log_file="$1" - local pattern="$2" - local description="$3" - local timeout_seconds="${4:-60}" - local elapsed=0 - - while (( elapsed < timeout_seconds )); do - if [[ -f "$log_file" ]] && grep -Fq "$pattern" "$log_file"; then - return 0 - fi - sleep 1 - ((elapsed += 1)) - done - - fail "Timed out waiting for $description" -} - -wait_for_exit() { - local pid="$1" - local timeout_seconds="$2" - local elapsed=0 - - while kill -0 "$pid" 2>/dev/null; do - if (( elapsed >= timeout_seconds )); then - return 124 - fi - sleep 1 - ((elapsed += 1)) - done - - wait "$pid" -} - -start_mcc() { - local config="$1" - local port="$2" - local log_file="$3" - local input_file="$4" - - : > "$input_file" - MCC_FILE_INPUT=1 MCC_INPUT_FILE="$input_file" \ - _mcc_dotnet_env dotnet run --project "$REPO_ROOT/MinecraftClient" -c Release --no-build -- \ - "$config" "$TEST_USERNAME" "-" "localhost:$port" > "$log_file" 2>&1 & - CURRENT_PID=$! -} - -prepare_config() { - local output="$1" - bash "$PREPARE_CONFIG" "$output" "$MC_VERSION" "$TEST_USERNAME" >/dev/null -} - -enable_exit_on_failure() { - local config="$1" - sed -i 's/^ExitOnFailure = false/ExitOnFailure = true/' "$config" - sed -i '/^\[ChatBot.AutoRelog\]/,/^\[/ s/^Enabled = false/Enabled = true/' "$config" -} - -reserve_unused_port() { - python3 -c 'import socket; sock = socket.socket(); sock.bind(("127.0.0.1", 0)); print(sock.getsockname()[1]); sock.close()' -} - -run_server_command() { - local command="$1" - local attempt - - for attempt in 1 2 3 4 5; do - if mc-rcon "$command"; then - return 0 - fi - sleep 1 - done - - fail "RCON command failed: $command" -} - -bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$VERSION" -bash "$ENSURE_SERVER" "$VERSION" -mcc-build -mc-start "$VERSION" >/dev/null - -SERVER_PORT="$(bash "$GET_SERVER_PORT" "$VERSION")" -[[ -n "$SERVER_PORT" ]] || fail "Unable to resolve the server port" - -for attempt in 1 2 3 4 5; do - if run_server_command "list"; then - break - fi - sleep 1 - if [[ "$attempt" == 5 ]]; then - fail "RCON did not become ready" - fi -done - -echo "[1/3] Server kick exits promptly and bypasses AutoRelog" -KICK_CONFIG="$RUN_DIR/kick.ini" -KICK_LOG="$RUN_DIR/kick.log" -KICK_INPUT="$RUN_DIR/kick.input" -prepare_config "$KICK_CONFIG" -enable_exit_on_failure "$KICK_CONFIG" -start_mcc "$KICK_CONFIG" "$SERVER_PORT" "$KICK_LOG" "$KICK_INPUT" -wait_for_log "$KICK_LOG" "Server was successfully joined." "MCC join" -run_server_command "kick $TEST_USERNAME ExitOnFailure integration test" - -if wait_for_exit "$CURRENT_PID" 15; then - exit_code=0 -else - exit_code=$? -fi -[[ "$exit_code" != 124 ]] || fail "MCC did not exit after the server kick" -CURRENT_PID="" -[[ "$exit_code" == 2 || "$exit_code" == 3 ]] || fail "Expected nonzero kick exit code 2 or 3, got $exit_code" -[[ "$(grep -Fc "Server was successfully joined." "$KICK_LOG")" == 1 ]] || fail "MCC rejoined after a kick" - -echo "[2/3] Refused initial TCP connection exits with code 3" -REFUSED_CONFIG="$RUN_DIR/refused.ini" -REFUSED_LOG="$RUN_DIR/refused.log" -REFUSED_INPUT="$RUN_DIR/refused.input" -prepare_config "$REFUSED_CONFIG" -enable_exit_on_failure "$REFUSED_CONFIG" -UNUSED_PORT="$(reserve_unused_port)" -start_mcc "$REFUSED_CONFIG" "$UNUSED_PORT" "$REFUSED_LOG" "$REFUSED_INPUT" - -if wait_for_exit "$CURRENT_PID" 15; then - exit_code=0 -else - exit_code=$? -fi -[[ "$exit_code" != 124 ]] || fail "MCC did not exit after the refused connection" -CURRENT_PID="" -[[ "$exit_code" == 3 ]] || fail "Expected connection-loss exit code 3, got $exit_code" - -echo "[3/3] Interactive mode remains at the offline prompt after a kick" -INTERACTIVE_CONFIG="$RUN_DIR/interactive.ini" -INTERACTIVE_LOG="$RUN_DIR/interactive.log" -INTERACTIVE_INPUT="$RUN_DIR/interactive.input" -prepare_config "$INTERACTIVE_CONFIG" -start_mcc "$INTERACTIVE_CONFIG" "$SERVER_PORT" "$INTERACTIVE_LOG" "$INTERACTIVE_INPUT" -wait_for_log "$INTERACTIVE_LOG" "Server was successfully joined." "interactive MCC join" -run_server_command "kick $TEST_USERNAME Interactive-mode regression test" -sleep 5 -kill -0 "$CURRENT_PID" 2>/dev/null || fail "Interactive MCC exited after a kick" - -echo "PASS: ExitOnFailure integration test" -echo "Run directory: $RUN_DIR"