diff --git a/MinecraftClient/ChatBots/AutoRelog.cs b/MinecraftClient/ChatBots/AutoRelog.cs
index 1c975b38..b56c922a 100644
--- a/MinecraftClient/ChatBots/AutoRelog.cs
+++ b/MinecraftClient/ChatBots/AutoRelog.cs
@@ -131,7 +131,7 @@ namespace MinecraftClient.ChatBots
else if (CanReconnect())
{
message = GetVerbatim(message);
- string comp = message.ToLower();
+ string comp = message.ToLowerInvariant();
LogDebugToConsole(string.Format(Translations.bot_autoRelog_disconnect_msg, message));
diff --git a/MinecraftClient/ClassicConsoleBackend.cs b/MinecraftClient/ClassicConsoleBackend.cs
index 38411413..222e9ff1 100644
--- a/MinecraftClient/ClassicConsoleBackend.cs
+++ b/MinecraftClient/ClassicConsoleBackend.cs
@@ -1,5 +1,6 @@
using System;
using System.Text.RegularExpressions;
+using System.Threading;
namespace MinecraftClient
{
@@ -109,9 +110,32 @@ namespace MinecraftClient
public void StopReadThread()
{
- ConsoleInteractive.ConsoleReader.StopReadThread();
- ConsoleInteractive.ConsoleReader.MessageReceived -= ForwardMessage;
- ConsoleInteractive.ConsoleReader.OnInputChange -= ForwardInputChange;
+ Thread stopThread = new(() =>
+ {
+ try
+ {
+ ConsoleInteractive.ConsoleReader.StopReadThread();
+ }
+ catch
+ {
+ // Best-effort shutdown; do not block the caller thread during reconnect/restart.
+ }
+
+ try
+ {
+ ConsoleInteractive.ConsoleReader.MessageReceived -= ForwardMessage;
+ ConsoleInteractive.ConsoleReader.OnInputChange -= ForwardInputChange;
+ }
+ catch
+ {
+ // Ignore detach failures.
+ }
+ })
+ {
+ IsBackground = true,
+ Name = "ClassicConsoleBackend.StopReadThread"
+ };
+ stopThread.Start();
}
public string RequestImmediateInput()
diff --git a/MinecraftClient/ConsoleIO.cs b/MinecraftClient/ConsoleIO.cs
index 2c5a168c..ac75207a 100644
--- a/MinecraftClient/ConsoleIO.cs
+++ b/MinecraftClient/ConsoleIO.cs
@@ -92,6 +92,29 @@ namespace MinecraftClient
return Backend.RequestImmediateInput();
}
+ ///
+ /// Read a line from the standard input but return quickly if the backend stalls.
+ /// This is used by the offline prompt so disconnect/reconnect handling cannot hang forever.
+ ///
+ public static string ReadLineWithTimeout(TimeSpan timeout)
+ {
+ if (BasicIO)
+ return Console.ReadLine() ?? String.Empty;
+
+ try
+ {
+ var inputTask = Task.Run(() => Backend.RequestImmediateInput());
+ if (inputTask.Wait(timeout))
+ return inputTask.Result;
+ }
+ catch
+ {
+ // Ignore backend stalls and fall back to an empty input so the reconnect flow can continue.
+ }
+
+ return string.Empty;
+ }
+
///
/// Debug routine: print all keys pressed in the console
///
diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs
index 753de486..2c4d5446 100644
--- a/MinecraftClient/McClient.cs
+++ b/MinecraftClient/McClient.cs
@@ -232,6 +232,7 @@ namespace MinecraftClient
Tuple? timeoutdetector = null;
private Thread? basicIOReadThread;
private int transferInProgress = 0;
+ private int disconnectInProgress = 0;
private bool consoleReadThreadOwned = false;
private bool consoleHandlersAttached = false;
@@ -568,6 +569,7 @@ namespace MinecraftClient
if (ConsoleIO.BasicIO || ConsoleIO.Backend is null)
{
cmdprompt?.Cancel();
+ cmdprompt = null;
basicIOReadThread = null;
consoleReadThreadOwned = false;
consoleHandlersAttached = false;
@@ -576,15 +578,36 @@ namespace MinecraftClient
if (consoleHandlersAttached)
{
- ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
- ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
+ try
+ {
+ ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
+ ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
+ }
+ catch
+ {
+ // Best effort; a stale subscription should not block teardown.
+ }
consoleHandlersAttached = false;
}
if (consoleReadThreadOwned)
+ {
+ try
+ {
+ ConsoleIO.Backend.StopReadThread();
+ }
+ catch
+ {
+ // Best effort; a stalled read thread should not block disconnect handling.
+ }
+ finally
+ {
+ consoleReadThreadOwned = false;
+ }
+ }
+ else
{
ConsoleIO.Backend.StopReadThread();
- consoleReadThreadOwned = false;
}
}
@@ -866,36 +889,60 @@ namespace MinecraftClient
///
public void Disconnect()
{
- instance = null;
+ if (Interlocked.Exchange(ref disconnectInProgress, 1) != 0)
+ return;
- DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, ""));
-
- foreach (ChatBot bot in bots.Where(bot => bot.ScriptOwnerKey is not null).ToList())
- BotUnLoad(bot);
-
- botsOnHold.Clear();
- botsOnHold.AddRange(bots.Where(bot => bot.ScriptOwnerKey is null));
-
- if (handler is not null)
+ try
{
- handler.Disconnect();
- handler.Dispose();
- }
+ try
+ {
+ StopConsoleSession();
+ }
+ catch
+ {
+ // Best-effort; disconnect should not be blocked by console teardown failures.
+ }
- if (cmdprompt is not null)
+ bool clearSharedState = ReferenceEquals(instance, this);
+ if (clearSharedState)
+ instance = null;
+
+ if (ReferenceEquals(CmdResult.currentHandler, this))
+ CmdResult.currentHandler = null;
+
+ DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, ""));
+
+ foreach (ChatBot bot in bots.Where(bot => bot.ScriptOwnerKey is not null).ToList())
+ BotUnLoad(bot);
+
+ botsOnHold.Clear();
+ botsOnHold.AddRange(bots.Where(bot => bot.ScriptOwnerKey is null));
+
+ if (handler is not null)
+ {
+ handler.Disconnect();
+ handler.Dispose();
+ }
+
+ if (cmdprompt is not null)
+ {
+ cmdprompt.Cancel();
+ cmdprompt = null;
+ }
+
+ if (timeoutdetector is not null)
+ {
+ timeoutdetector.Item2.Cancel();
+ timeoutdetector = null;
+ }
+
+ if (client is not null)
+ client.Close();
+ }
+ finally
{
- cmdprompt.Cancel();
- cmdprompt = null;
+ Interlocked.Exchange(ref disconnectInProgress, 0);
}
-
- if (timeoutdetector is not null)
- {
- timeoutdetector.Item2.Cancel();
- timeoutdetector = null;
- }
-
- if (client is not null)
- client.Close();
}
///
@@ -903,71 +950,95 @@ namespace MinecraftClient
///
public void OnConnectionLost(ChatBot.DisconnectReason reason, string message)
{
- instance = null;
+ if (Interlocked.Exchange(ref disconnectInProgress, 1) != 0)
+ return;
- ConsoleIO.CancelAutocomplete();
-
- handler.Dispose();
-
- world.Clear();
- ClearKnownSigns();
-
- if (timeoutdetector is not null)
- {
- if (timeoutdetector is not null && Thread.CurrentThread != timeoutdetector.Item1)
- timeoutdetector.Item2.Cancel();
- timeoutdetector = null;
- }
-
- bool will_restart = false;
-
- switch (reason)
- {
- case ChatBot.DisconnectReason.ConnectionLost:
- message = Translations.mcc_disconnect_lost;
- Log.Info(message);
- break;
-
- case ChatBot.DisconnectReason.InGameKick:
- Log.Info(Translations.mcc_disconnect_server);
- Log.Info(message);
- break;
-
- case ChatBot.DisconnectReason.LoginRejected:
- Log.Info(Translations.mcc_disconnect_login);
- Log.Info(message);
- break;
-
- case ChatBot.DisconnectReason.UserLogout:
- throw new InvalidOperationException(Translations.exception_user_logout);
- }
-
- //Process AutoRelog last to make sure other bots can perform their cleanup tasks first (issue #1517)
- List onDisconnectBotList = bots.Where(bot => bot is not AutoRelog).ToList();
- onDisconnectBotList.AddRange(bots.Where(bot => bot is AutoRelog));
-
- foreach (ChatBot bot in onDisconnectBotList)
+ try
{
try
{
- will_restart |= bot.OnDisconnect(reason, message);
+ StopConsoleSession();
}
- catch (Exception e)
+ catch
{
- if (e is not ThreadAbortException)
+ // Best-effort; disconnect should not be blocked by console teardown failures.
+ }
+
+ bool clearSharedState = ReferenceEquals(instance, this);
+ if (clearSharedState)
+ instance = null;
+
+ if (ReferenceEquals(CmdResult.currentHandler, this))
+ CmdResult.currentHandler = null;
+
+ ConsoleIO.CancelAutocomplete();
+
+ handler.Dispose();
+
+ world.Clear();
+ ClearKnownSigns();
+
+ if (timeoutdetector is not null)
+ {
+ if (timeoutdetector is not null && Thread.CurrentThread != timeoutdetector.Item1)
+ timeoutdetector.Item2.Cancel();
+ timeoutdetector = null;
+ }
+
+ bool will_restart = false;
+
+ switch (reason)
+ {
+ case ChatBot.DisconnectReason.ConnectionLost:
+ message = Translations.mcc_disconnect_lost;
+ Log.Info(message);
+ break;
+
+ case ChatBot.DisconnectReason.InGameKick:
+ Log.Info(Translations.mcc_disconnect_server);
+ Log.Info(message);
+ break;
+
+ case ChatBot.DisconnectReason.LoginRejected:
+ Log.Info(Translations.mcc_disconnect_login);
+ Log.Info(message);
+ break;
+
+ case ChatBot.DisconnectReason.UserLogout:
+ throw new InvalidOperationException(Translations.exception_user_logout);
+ }
+
+ //Process AutoRelog last to make sure other bots can perform their cleanup tasks first (issue #1517)
+ List onDisconnectBotList = bots.Where(bot => bot is not AutoRelog).ToList();
+ onDisconnectBotList.AddRange(bots.Where(bot => bot is AutoRelog));
+
+ foreach (ChatBot bot in onDisconnectBotList)
+ {
+ try
{
- Log.Warn("OnDisconnect: Got error from " + bot.ToString() + ": " + e.ToString());
+ will_restart |= bot.OnDisconnect(reason, message);
}
- else throw; //ThreadAbortException should not be caught
+ catch (Exception e)
+ {
+ if (e is not ThreadAbortException)
+ {
+ Log.Warn("OnDisconnect: Got error from " + bot.ToString() + ": " + e.ToString());
+ }
+ else throw; //ThreadAbortException should not be caught
+ }
+ }
+
+ SentrySdk.EndSession();
+
+ if (!will_restart)
+ {
+ StopConsoleSession();
+ Program.HandleFailure(null, false, reason);
}
}
-
- SentrySdk.EndSession();
-
- if (!will_restart)
+ finally
{
- StopConsoleSession();
- Program.HandleFailure(null, false, reason);
+ Interlocked.Exchange(ref disconnectInProgress, 0);
}
}
diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs
index 71ba3142..00056e50 100644
--- a/MinecraftClient/Program.cs
+++ b/MinecraftClient/Program.cs
@@ -56,6 +56,13 @@ namespace MinecraftClient
private static bool useMcVersionOnce = false;
private static Thread? _restartThread = null;
private static readonly object _restartLock = new();
+ private static int _exitInProgress = 0;
+ private static int _restartInProgress = 0;
+ private static int _failureInProgress = 0;
+ private static int _restartAttemptGeneration = 0;
+ private static int _restartPending = 0;
+ private static int _pendingRestartDelaySeconds = 0;
+ private static bool _pendingRestartKeepAccountAndServerSettings = false;
private static string settingsIniPath = "MinecraftClient.ini";
// [SENTRY]
@@ -914,22 +921,91 @@ namespace MinecraftClient
internal static bool TryRestart(int delaySeconds = 0, bool keepAccountAndServerSettings = false)
{
+ if (Interlocked.Exchange(ref _restartInProgress, 1) != 0)
+ {
+ lock (_restartLock)
+ {
+ Interlocked.Exchange(ref _restartPending, 1);
+ _pendingRestartDelaySeconds = delaySeconds;
+ _pendingRestartKeepAccountAndServerSettings = keepAccountAndServerSettings;
+ }
+ return true;
+ }
+
+ int attemptGeneration = Interlocked.Increment(ref _restartAttemptGeneration);
+
lock (_restartLock)
{
if (HasRestartPendingForAnotherThreadNoLock())
+ {
+ Interlocked.Exchange(ref _restartInProgress, 0);
+ Interlocked.Decrement(ref _restartAttemptGeneration);
return false;
+ }
+
+ try
+ {
+ if (ConsoleIO.Backend is not null)
+ ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
+ }
+ catch { }
+
+ try
+ {
+ ConsoleIO.Backend?.StopReadThread();
+ }
+ catch { }
+
+ try
+ {
+ ConsoleIO.CancelAutocomplete();
+ }
+ catch { }
+
+ try
+ {
+ ConsoleIO.Reset();
+ }
+ catch { }
- ConsoleIO.Backend?.StopReadThread();
var thread = new Thread(new ThreadStart(delegate
{
try
{
- if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); }
+ McClient? activeClient = client;
+ if (activeClient is not null)
+ {
+ activeClient.Disconnect();
+ if (ReferenceEquals(client, activeClient))
+ client = null;
+ ConsoleIO.Reset();
+ }
+ try
+ {
+ ConsoleIO.CancelAutocomplete();
+ ConsoleIO.Reset();
+ }
+ catch { }
if (offlinePrompt is not null)
{
- if (ConsoleIO.Backend is not null)
- ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
- offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset();
+ try
+ {
+ offlinePrompt.Item2.Cancel();
+ if (offlinePrompt.Item1.IsAlive)
+ {
+ if (!offlinePrompt.Item1.Join(TimeSpan.FromSeconds(1)))
+ offlinePrompt.Item1.Interrupt();
+ }
+ }
+ catch (Exception)
+ {
+ // Best-effort cleanup; the reconnect loop must continue.
+ }
+ finally
+ {
+ offlinePrompt = null;
+ ConsoleIO.Reset();
+ }
}
if (delaySeconds > 0)
{
@@ -942,11 +1018,30 @@ namespace MinecraftClient
}
finally
{
+ Interlocked.Exchange(ref _restartInProgress, 0);
lock (_restartLock)
{
if (_restartThread == Thread.CurrentThread)
_restartThread = null;
}
+
+ if (Volatile.Read(ref _restartPending) != 0)
+ {
+ int nextDelaySeconds = 0;
+ bool nextKeepAccountAndServerSettings = false;
+ lock (_restartLock)
+ {
+ if (Volatile.Read(ref _restartPending) != 0)
+ {
+ nextDelaySeconds = _pendingRestartDelaySeconds;
+ nextKeepAccountAndServerSettings = _pendingRestartKeepAccountAndServerSettings;
+ Interlocked.Exchange(ref _restartPending, 0);
+ }
+ }
+
+ if (Volatile.Read(ref _restartPending) == 0)
+ TryRestart(nextDelaySeconds, nextKeepAccountAndServerSettings);
+ }
}
}));
_restartThread = thread;
@@ -957,29 +1052,65 @@ namespace MinecraftClient
private static bool HasRestartPendingForAnotherThreadNoLock()
{
- return _restartThread is not null
- && _restartThread.IsAlive
- && _restartThread != Thread.CurrentThread;
+ return Volatile.Read(ref _restartPending) != 0
+ || (_restartThread is not null
+ && _restartThread.IsAlive
+ && _restartThread != Thread.CurrentThread);
}
public static void DoExit(int exitcode = 0)
{
- WriteBackSettings();
- ConsoleIO.WriteLineFormatted("§a" + string.Format(Translations.config_saving, settingsIniPath));
+ if (Interlocked.Exchange(ref _exitInProgress, 1) != 0)
+ return;
+
+ Interlocked.Exchange(ref _failureInProgress, 1);
+ Interlocked.Exchange(ref _restartInProgress, 1);
+
+ try
+ {
+ WriteBackSettings();
+ ConsoleIO.WriteLineFormatted("§a" + string.Format(Translations.config_saving, settingsIniPath));
+ }
+ catch { }
+
+ try
+ {
+ McClient? activeClient = client;
+ if (activeClient is not null)
+ {
+ activeClient.Disconnect();
+ if (ReferenceEquals(client, activeClient))
+ client = null;
+ ConsoleIO.Reset();
+ }
+ }
+ catch { }
- if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); }
if (offlinePrompt is not null)
{
- if (ConsoleIO.Backend is not null)
- ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
- offlinePrompt.Item2.Cancel();
- if (Thread.CurrentThread != offlinePrompt.Item1)
- offlinePrompt.Item1.Join(1000);
- offlinePrompt = null;
- ConsoleIO.Reset();
+ try
+ {
+ if (ConsoleIO.Backend is not null)
+ ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
+ offlinePrompt.Item2.Cancel();
+ if (offlinePrompt.Item1.IsAlive)
+ {
+ if (!offlinePrompt.Item1.Join(TimeSpan.FromSeconds(1)))
+ offlinePrompt.Item1.Interrupt();
+ }
+ }
+ catch { }
+ finally
+ {
+ offlinePrompt = null;
+ ConsoleIO.Reset();
+ }
}
if (Config.Main.Advanced.PlayerHeadAsIcon && OperatingSystem.IsWindows()) { ConsoleIcon.RevertToMCCIcon(); }
- ConsoleIO.Backend?.Shutdown();
+ try { ConsoleIO.Backend?.Shutdown(); } catch { }
+ Interlocked.Exchange(ref _failureInProgress, 0);
+ Interlocked.Exchange(ref _restartInProgress, 0);
+ Interlocked.Exchange(ref _exitInProgress, 0);
Environment.Exit(exitcode);
}
@@ -988,7 +1119,7 @@ namespace MinecraftClient
///
public static void Exit(int exitcode = 0)
{
- new Thread(() => { DoExit(exitcode); }).Start();
+ DoExit(exitcode);
}
///
@@ -1000,143 +1131,158 @@ namespace MinecraftClient
/// If set, the error message will be processed by the AutoRelog bot
public static void HandleFailure(string? errorMessage = null, bool versionError = false, ChatBot.DisconnectReason? disconnectReason = null)
{
- bool autoRelogHandled = false;
+ if (Volatile.Read(ref _restartInProgress) != 0 || Volatile.Read(ref _restartPending) != 0)
+ return;
- if (!string.IsNullOrEmpty(errorMessage))
+ if (Interlocked.Exchange(ref _failureInProgress, 1) != 0)
+ return;
+
+ try
{
- ConsoleIO.Reset();
- if (ConsoleIO.Backend is not Tui.TuiConsoleBackend)
+ bool autoRelogHandled = false;
+
+ if (!string.IsNullOrEmpty(errorMessage))
{
- try
+ ConsoleIO.Reset();
+ if (ConsoleIO.Backend is not Tui.TuiConsoleBackend)
{
- while (Console.KeyAvailable)
- Console.ReadKey(true);
- }
- catch { }
- }
- ConsoleIO.WriteLine(errorMessage);
-
- if (disconnectReason.HasValue)
- {
- autoRelogHandled = true;
- if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage))
- return;
- }
- }
-
- if (InternalConfig.InteractiveMode)
- {
- if (versionError)
- {
- ConsoleIO.WriteLine(Translations.mcc_server_version);
- InternalConfig.MinecraftVersion = ConsoleIO.ReadLine();
- if (InternalConfig.MinecraftVersion != "")
- {
- useMcVersionOnce = true;
- Restart(0, true);
- return;
- }
- }
-
- if (!autoRelogHandled && disconnectReason.HasValue)
- {
- if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage!))
- return;
- }
-
- if (offlinePrompt is null)
- {
- ConsoleIO.Backend?.StopReadThread();
- if (ConsoleIO.Backend is not null)
- ConsoleIO.Backend.OnInputChange += ConsoleIO.OfflineAutocompleteHandler;
-
- var cancellationTokenSource = new CancellationTokenSource();
- offlinePrompt = new(new Thread(new ThreadStart(delegate
- {
- bool exitThread = false;
- string command = " ";
- ConsoleIO.WriteLine(string.Empty);
- ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString()));
- if (ConsoleIO.Backend is Tui.TuiConsoleBackend)
- ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString()));
- else
- ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
-
- while (!cancellationTokenSource.IsCancellationRequested)
+ try
{
- if (exitThread)
- return;
-
- command = ConsoleIO.ReadLine().Trim();
-
- if (command.Length == 0)
- {
- if (ConsoleIO.Backend is not Tui.TuiConsoleBackend)
- Commands.Exit.DoExit(Config.AppVar.ExpandVars(command));
- continue;
- }
-
- string message = "";
-
- if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' '
- && command[0] == Config.Main.Advanced.InternalCmdChar.ToChar())
- command = command[1..];
-
- if (command.StartsWith("reco"))
- {
- message = Commands.Reco.DoReconnect(Config.AppVar.ExpandVars(command));
- if (message == "")
- {
- exitThread = true;
- continue;
- }
- }
- else if (command.StartsWith("connect"))
- {
- message = Commands.Connect.DoConnect(Config.AppVar.ExpandVars(command));
- if (message == "")
- {
- exitThread = true;
- continue;
- }
- }
- else if (command.StartsWith("exit") || command.StartsWith("quit"))
- {
- message = Commands.Exit.DoExit(Config.AppVar.ExpandVars(command));
- }
- else if (command.StartsWith("help"))
- {
- ConsoleIO.WriteLineFormatted("§8MCC: " +
- Config.Main.Advanced.InternalCmdChar.ToLogString() +
- new Commands.Reco().GetCmdDescTranslated());
- ConsoleIO.WriteLineFormatted("§8MCC: " +
- Config.Main.Advanced.InternalCmdChar.ToLogString() +
- new Commands.Connect().GetCmdDescTranslated());
- }
- else
- ConsoleIO.WriteLineFormatted(string.Format(Translations.icmd_unknown, command.Split(' ')[0]));
-
- if (message != "")
- ConsoleIO.WriteLineFormatted("§8MCC: " + message);
+ while (Console.KeyAvailable)
+ Console.ReadKey(true);
}
- })), cancellationTokenSource);
- 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();
- }
+ catch { }
+ }
+ ConsoleIO.WriteLine(errorMessage);
+ if (disconnectReason.HasValue)
+ {
+ autoRelogHandled = true;
+ if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage))
+ return;
+ }
+ }
+
+ if (InternalConfig.InteractiveMode)
+ {
+ if (versionError)
+ {
+ ConsoleIO.WriteLine(Translations.mcc_server_version);
+ InternalConfig.MinecraftVersion = ConsoleIO.ReadLine();
+ if (InternalConfig.MinecraftVersion != "")
+ {
+ useMcVersionOnce = true;
+ Restart(0, true);
+ return;
+ }
+ }
+
+ if (!autoRelogHandled && disconnectReason.HasValue)
+ {
+ if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage!))
+ return;
+ }
+
+ if (offlinePrompt is null)
+ {
+ ConsoleIO.Backend?.StopReadThread();
+ if (ConsoleIO.Backend is not null)
+ ConsoleIO.Backend.OnInputChange += ConsoleIO.OfflineAutocompleteHandler;
+
+ var cancellationTokenSource = new CancellationTokenSource();
+ offlinePrompt = new(new Thread(new ThreadStart(delegate
+ {
+ bool exitThread = false;
+ string command = " ";
+ ConsoleIO.WriteLine(string.Empty);
+ ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString()));
+ if (ConsoleIO.Backend is Tui.TuiConsoleBackend)
+ ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString()));
+ else
+ ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
+
+ while (!cancellationTokenSource.IsCancellationRequested)
+ {
+ if (exitThread)
+ return;
+
+ command = ConsoleIO.ReadLineWithTimeout(TimeSpan.FromSeconds(1)).Trim();
+
+ if (command.Length == 0)
+ {
+ if (ConsoleIO.Backend is not Tui.TuiConsoleBackend)
+ {
+ try { Commands.Exit.DoExit(Config.AppVar.ExpandVars(command)); }
+ catch { }
+ }
+ continue;
+ }
+
+ string message = "";
+
+ if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' '
+ && command[0] == Config.Main.Advanced.InternalCmdChar.ToChar())
+ command = command[1..];
+
+ if (command.StartsWith("reco"))
+ {
+ message = Commands.Reco.DoReconnect(Config.AppVar.ExpandVars(command));
+ if (message == "")
+ {
+ exitThread = true;
+ continue;
+ }
+ }
+ else if (command.StartsWith("connect"))
+ {
+ message = Commands.Connect.DoConnect(Config.AppVar.ExpandVars(command));
+ if (message == "")
+ {
+ exitThread = true;
+ continue;
+ }
+ }
+ else if (command.StartsWith("exit") || command.StartsWith("quit"))
+ {
+ message = Commands.Exit.DoExit(Config.AppVar.ExpandVars(command));
+ }
+ else if (command.StartsWith("help"))
+ {
+ ConsoleIO.WriteLineFormatted("§8MCC: " +
+ Config.Main.Advanced.InternalCmdChar.ToLogString() +
+ new Commands.Reco().GetCmdDescTranslated());
+ ConsoleIO.WriteLineFormatted("§8MCC: " +
+ Config.Main.Advanced.InternalCmdChar.ToLogString() +
+ new Commands.Connect().GetCmdDescTranslated());
+ }
+ else
+ ConsoleIO.WriteLineFormatted(string.Format(Translations.icmd_unknown, command.Split(' ')[0]));
+
+ if (message != "")
+ ConsoleIO.WriteLineFormatted("§8MCC: " + message);
+ }
+ })), cancellationTokenSource);
+ 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();
+ }
+ }
+ finally
+ {
+ Interlocked.Exchange(ref _failureInProgress, 0);
+ }
}
///
diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs
index 2d0bb40f..951be73e 100644
--- a/MinecraftClient/Protocol/Handlers/Protocol18.cs
+++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs
@@ -4085,17 +4085,38 @@ namespace MinecraftClient.Protocol.Handlers
if (netMain is not null)
{
netMain.Item2.Cancel();
+ netMain = null;
}
if (netReader is not null)
{
netReader.Item2.Cancel();
- socketWrapper.Disconnect();
+ netReader = null;
}
}
catch
{
}
+ finally
+ {
+ try
+ {
+ socketWrapper.Disconnect();
+ }
+ catch
+ {
+ // Best effort; the old protocol loop should not block shutdown.
+ }
+
+ try
+ {
+ packetQueue.CompleteAdding();
+ }
+ catch
+ {
+ // Best effort; the queue may already be completed.
+ }
+ }
}
///
diff --git a/MinecraftClient/Protocol/Handlers/SocketWrapper.cs b/MinecraftClient/Protocol/Handlers/SocketWrapper.cs
index a4f451b1..4bf54198 100644
--- a/MinecraftClient/Protocol/Handlers/SocketWrapper.cs
+++ b/MinecraftClient/Protocol/Handlers/SocketWrapper.cs
@@ -1,4 +1,5 @@
using System;
+using System.IO;
using System.Net.Sockets;
using MinecraftClient.Crypto;
@@ -61,10 +62,31 @@ namespace MinecraftClient.Protocol.Handlers
int read = 0;
while (read < offset)
{
- if (encrypted)
- read += s!.Read(buffer, start + read, offset - read);
- else
- read += c.Client.Receive(buffer, start + read, offset - read, f);
+ int received;
+ try
+ {
+ if (encrypted)
+ received = s!.Read(buffer, start + read, offset - read);
+ else
+ received = c.Client.Receive(buffer, start + read, offset - read, f);
+ }
+ catch (SocketException)
+ {
+ throw;
+ }
+ catch (ObjectDisposedException)
+ {
+ throw;
+ }
+ catch (IOException)
+ {
+ throw;
+ }
+
+ if (received <= 0)
+ throw new IOException("Socket closed while reading from the server.");
+
+ read += received;
}
}
diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs
index a5ce88af..947bccbf 100644
--- a/MinecraftClient/Settings.cs
+++ b/MinecraftClient/Settings.cs
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
@@ -151,6 +152,15 @@ namespace MinecraftClient
public string? LegacyBackupPath { get; init; }
}
+ private static Mutex AcquireSettingsFileMutex(string filepath)
+ {
+ string fullPath = Path.GetFullPath(filepath);
+ string normalizedPath = fullPath.Replace(Path.DirectorySeparatorChar, '/').ToLowerInvariant();
+ byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(normalizedPath));
+ string mutexName = "MCC.Settings." + Convert.ToHexString(hashBytes)[..16];
+ return new Mutex(false, mutexName);
+ }
+
public static ConfigLoadResult LoadFromFile(string filepath, bool keepAccountAndServerSettings = false)
{
bool keepAccountSettings = InternalConfig.KeepAccountSettings;
@@ -158,118 +168,142 @@ namespace MinecraftClient
if (keepAccountAndServerSettings)
InternalConfig.KeepAccountSettings = InternalConfig.KeepServerSettings = true;
- Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
- TomlDocument document;
+ using var settingsMutex = AcquireSettingsFileMutex(filepath);
+ settingsMutex.WaitOne();
try
{
- document = TomlParser.ParseFile(filepath);
- Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
-
- Config = TomletMain.To(document);
- }
- catch (Exception ex)
- {
- Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
+ Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
+ TomlDocument document;
try
{
- string configString = File.ReadAllText(filepath);
- if (configString.Contains("Some settings missing here after an upgrade?"))
- {
- string newFilePath = Path.ChangeExtension(filepath, ".old.ini");
- File.Copy(filepath, newFilePath, true);
- return new ConfigLoadResult
- {
- Success = false,
- NeedWriteDefault = true,
- IsLegacyUpgrade = true,
- LegacyBackupPath = newFilePath
- };
- }
+ document = TomlParser.ParseFile(filepath);
+ Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
+
+ Config = TomletMain.To(document);
}
- catch { }
- return new ConfigLoadResult
+ catch (Exception ex)
{
- Success = false,
- NeedWriteDefault = false,
- ErrorMessage = ex.GetFullMessage()
- };
+ Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
+ try
+ {
+ string configString = File.ReadAllText(filepath);
+ if (configString.Contains("Some settings missing here after an upgrade?"))
+ {
+ string newFilePath = Path.ChangeExtension(filepath, ".old.ini");
+ File.Copy(filepath, newFilePath, true);
+ return new ConfigLoadResult
+ {
+ Success = false,
+ NeedWriteDefault = true,
+ IsLegacyUpgrade = true,
+ LegacyBackupPath = newFilePath
+ };
+ }
+ }
+ catch { }
+ return new ConfigLoadResult
+ {
+ Success = false,
+ NeedWriteDefault = false,
+ ErrorMessage = ex.GetFullMessage()
+ };
+ }
+ finally
+ {
+ if (!keepAccountSettings)
+ InternalConfig.KeepAccountSettings = false;
+ if (!keepServerSettings)
+ InternalConfig.KeepServerSettings = false;
+ }
+ return new ConfigLoadResult { Success = true, NeedWriteDefault = false };
}
finally
{
- if (!keepAccountSettings)
- InternalConfig.KeepAccountSettings = false;
- if (!keepServerSettings)
- InternalConfig.KeepServerSettings = false;
+ settingsMutex.ReleaseMutex();
}
- return new ConfigLoadResult { Success = true, NeedWriteDefault = false };
}
public static void WriteToFile(string filepath, bool backupOldFile)
{
- Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
- string tomlString = TomletMain.TomlStringFrom(Config);
- Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
-
- string[] tomlList = tomlString.Split('\n');
- StringBuilder newConfig = new();
- foreach (string line in tomlList)
+ using var settingsMutex = AcquireSettingsFileMutex(filepath);
+ settingsMutex.WaitOne();
+ try
{
- Match matchComment = CommentRegex.Match(line);
- if (matchComment.Success && matchComment.Groups.Count == 3)
+ Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
+ string tomlString = TomletMain.TomlStringFrom(Config);
+ Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
+
+ string[] tomlList = tomlString.Split('\n');
+ StringBuilder newConfig = new();
+ foreach (string line in tomlList)
{
- string config = matchComment.Groups[1].Value, comment = matchComment.Groups[2].Value;
- if (config.Length > 0)
- newConfig.Append(config).Append(' ', Math.Max(1, CommentsAlignPosition - config.Length) - 1);
- string? comment_trans = ConfigComments.ResourceManager.GetString(comment);
- if (string.IsNullOrEmpty(comment_trans))
- newConfig.Append("# ").AppendLine(comment.ReplaceLineEndings());
+ Match matchComment = CommentRegex.Match(line);
+ if (matchComment.Success && matchComment.Groups.Count == 3)
+ {
+ string config = matchComment.Groups[1].Value, comment = matchComment.Groups[2].Value;
+ if (config.Length > 0)
+ newConfig.Append(config).Append(' ', Math.Max(1, CommentsAlignPosition - config.Length) - 1);
+ string? comment_trans = ConfigComments.ResourceManager.GetString(comment);
+ if (string.IsNullOrEmpty(comment_trans))
+ newConfig.Append("# ").AppendLine(comment.ReplaceLineEndings());
+ else
+ newConfig.Append("# ").AppendLine(comment_trans.Replace("\n", "\n# ").ReplaceLineEndings());
+ }
else
- newConfig.Append("# ").AppendLine(comment_trans.Replace("\n", "\n# ").ReplaceLineEndings());
- }
- else
- {
- newConfig.AppendLine(line);
- }
- }
-
- bool needUpdate = true;
- byte[] newConfigByte = Encoding.UTF8.GetBytes(newConfig.ToString());
- if (File.Exists(filepath))
- {
- try
- {
- if (new FileInfo(filepath).Length == newConfigByte.Length)
- if (File.ReadAllBytes(filepath).SequenceEqual(newConfigByte))
- needUpdate = false;
- }
- catch { }
- }
-
- if (needUpdate)
- {
- bool backupSuccessed = true;
- if (backupOldFile && File.Exists(filepath))
- {
- string backupFilePath = Path.ChangeExtension(filepath, ".backup.ini");
- try { File.Copy(filepath, backupFilePath, true); }
- catch (Exception ex)
{
- backupSuccessed = false;
- ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.config_backup_fail, backupFilePath));
- ConsoleIO.WriteLine(ex.Message);
+ newConfig.AppendLine(line);
}
}
- if (backupSuccessed)
+ bool needUpdate = true;
+ byte[] newConfigByte = Encoding.UTF8.GetBytes(newConfig.ToString());
+ if (File.Exists(filepath))
{
- try { File.WriteAllBytes(filepath, newConfigByte); }
- catch (Exception ex)
+ try
{
- ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.config_write_fail, filepath));
- ConsoleIO.WriteLine(ex.Message);
+ if (new FileInfo(filepath).Length == newConfigByte.Length)
+ if (File.ReadAllBytes(filepath).SequenceEqual(newConfigByte))
+ needUpdate = false;
+ }
+ catch { }
+ }
+
+ if (needUpdate)
+ {
+ bool backupSuccessed = true;
+ if (backupOldFile && File.Exists(filepath))
+ {
+ string backupFilePath = Path.ChangeExtension(filepath, ".backup.ini");
+ try { File.Copy(filepath, backupFilePath, true); }
+ catch (Exception ex)
+ {
+ backupSuccessed = false;
+ ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.config_backup_fail, backupFilePath));
+ ConsoleIO.WriteLine(ex.Message);
+ }
+ }
+
+ if (backupSuccessed)
+ {
+ string tempFilePath = filepath + ".tmp";
+ try
+ {
+ File.WriteAllBytes(tempFilePath, newConfigByte);
+ File.Move(tempFilePath, filepath, true);
+ }
+ catch (Exception ex)
+ {
+ try { File.Delete(tempFilePath); } catch { }
+ ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.config_write_fail, filepath));
+ ConsoleIO.WriteLine(ex.Message);
+ }
}
}
}
+ finally
+ {
+ settingsMutex.ReleaseMutex();
+ }
}
///