Problems:

1) The AutoRelog ChatBot stopped trying to log in after the 3rd attempt.
2) The AutoRelog ChatBot stopped trying to log in after the internet connection was interrupted. MCC froze.

2 problems have been solved.
This commit is contained in:
Somersault 2026-07-18 21:37:52 +03:00
parent 142fb107db
commit 9e271a2f06
8 changed files with 668 additions and 327 deletions

View file

@ -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));

View file

@ -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()

View file

@ -92,6 +92,29 @@ namespace MinecraftClient
return Backend.RequestImmediateInput();
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Debug routine: print all keys pressed in the console
/// </summary>

View file

@ -232,6 +232,7 @@ namespace MinecraftClient
Tuple<Thread, CancellationTokenSource>? 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
/// </summary>
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();
}
/// <summary>
@ -903,71 +950,95 @@ namespace MinecraftClient
/// </summary>
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<ChatBot> 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<ChatBot> 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);
}
}

View file

@ -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
/// </summary>
public static void Exit(int exitcode = 0)
{
new Thread(() => { DoExit(exitcode); }).Start();
DoExit(exitcode);
}
/// <summary>
@ -1000,143 +1131,158 @@ namespace MinecraftClient
/// <param name="disconnectReason">If set, the error message will be processed by the AutoRelog bot</param>
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);
}
}
/// <summary>

View file

@ -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.
}
}
}
/// <summary>

View file

@ -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;
}
}

View file

@ -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<GlobalConfig>(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<GlobalConfig>(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();
}
}
/// <summary>