Fix: make AutoRelog retries single-owner (#3186)

Login rejections could schedule and execute multiple restarts for one failure while leaving no usable console route during reconnect delays.

Changes:
- Bind failure and restart work to immutable connection attempts
- Coalesce automatic retries while allowing explicit settings replacement until commit
- Preserve held bots and offline command routing across failed logins
- Add deterministic retry, ownership, and routing regression tests

Fixes #3186
This commit is contained in:
Anon 2026-07-27 16:12:10 +02:00
parent 92212d2b95
commit 456a548cbc
11 changed files with 645 additions and 114 deletions

View file

@ -55,7 +55,7 @@ namespace MinecraftClient
private static bool useMcVersionOnce = false;
private static readonly RestartCoordinator restartCoordinator = new(ExecuteRestartAsync, ReportRestartFailure);
private static long connectionAttempt;
private static int offlinePromptActive;
private static readonly AttemptOwnedRoute offlinePromptRoute = new();
private static int exitOnFailurePending;
private static string settingsIniPath = "MinecraftClient.ini";
private static AuthenticationSelection? pendingAuthenticationSelection;
@ -747,12 +747,14 @@ namespace MinecraftClient
private sealed record AuthenticationSelection(LoginType AccountType, LoginMethod Method, string AuthServerUrl);
internal static long CurrentConnectionAttempt => Volatile.Read(ref connectionAttempt);
/// <summary>
/// Start a new Client
/// </summary>
private static void InitializeClient()
{
Interlocked.Increment(ref connectionAttempt);
long attempt = Interlocked.Increment(ref connectionAttempt);
// Ensure that we use the provided Minecraft version if we can't connect automatically.
//
@ -984,7 +986,7 @@ namespace MinecraftClient
try
{
//Start the main TCP client
client = new McClient(session, playerKeyPair, InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion, forgeInfo);
client = new McClient(session, playerKeyPair, InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion, forgeInfo, attempt);
//Update console title
if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle))
@ -1062,7 +1064,22 @@ namespace MinecraftClient
TryRestart(TimeSpan.FromSeconds(Math.Max(0, delaySeconds)), keepAccountAndServerSettings);
}
internal static bool HasRestartPending => restartCoordinator.HasScheduledRestart(Volatile.Read(ref connectionAttempt));
internal static bool HasRestartPending(long sourceConnectionAttempt)
{
return restartCoordinator.HasScheduledRestart(sourceConnectionAttempt);
}
internal static RestartSettingsSnapshot CaptureRestartSettings()
{
return new RestartSettingsSnapshot(InternalConfig.Account, InternalConfig.ServerIP, InternalConfig.ServerPort);
}
internal static void RestoreRestartSettings(RestartSettingsSnapshot settingsSnapshot)
{
InternalConfig.Account = settingsSnapshot.Account;
InternalConfig.ServerIP = settingsSnapshot.ServerIP;
InternalConfig.ServerPort = settingsSnapshot.ServerPort;
}
internal static bool TryRestart(int delaySeconds = 0, bool keepAccountAndServerSettings = false)
{
@ -1070,6 +1087,15 @@ namespace MinecraftClient
}
internal static bool TryRestart(TimeSpan delay, bool keepAccountAndServerSettings = false)
{
return TryRestart(CurrentConnectionAttempt, delay, keepAccountAndServerSettings);
}
internal static bool TryRestart(
long sourceConnectionAttempt,
TimeSpan delay,
bool keepAccountAndServerSettings = false,
bool replaceUntilCommit = false)
{
if (Volatile.Read(ref exitOnFailurePending) != 0)
return false;
@ -1078,18 +1104,25 @@ namespace MinecraftClient
delay = TimeSpan.Zero;
RestartSettingsSnapshot? settingsSnapshot = keepAccountAndServerSettings
? new RestartSettingsSnapshot(InternalConfig.Account, InternalConfig.ServerIP, InternalConfig.ServerPort)
? CaptureRestartSettings()
: null;
return restartCoordinator.TrySchedule(new RestartRequest(
Volatile.Read(ref connectionAttempt),
bool scheduled = restartCoordinator.TrySchedule(new RestartRequest(
sourceConnectionAttempt,
delay,
keepAccountAndServerSettings,
settingsSnapshot));
settingsSnapshot,
replaceUntilCommit));
if (scheduled)
BeginOfflinePrompt(sourceConnectionAttempt);
return scheduled;
}
private static async Task ExecuteRestartAsync(RestartRequest request, CancellationToken cancellationToken)
{
if (request.ConnectionAttempt != CurrentConnectionAttempt)
return;
McClient? disconnectedClient = client;
if (disconnectedClient is not null)
{
@ -1098,7 +1131,6 @@ namespace MinecraftClient
client = null;
}
EndOfflinePrompt();
ConsoleIO.Reset();
if (request.Delay > TimeSpan.Zero)
@ -1108,14 +1140,22 @@ namespace MinecraftClient
}
cancellationToken.ThrowIfCancellationRequested();
if (request.ConnectionAttempt != CurrentConnectionAttempt
|| !restartCoordinator.TryBeginCommit(request, out RestartRequest latestRequest)
|| latestRequest.ConnectionAttempt != CurrentConnectionAttempt)
{
return;
}
ConsoleIO.WriteLine(Translations.mcc_restart);
ReloadSettings(request.KeepAccountAndServerSettings);
if (request.SettingsSnapshot is RestartSettingsSnapshot settingsSnapshot)
ReloadSettings(latestRequest.KeepAccountAndServerSettings);
if (latestRequest.SettingsSnapshot is RestartSettingsSnapshot settingsSnapshot)
{
InternalConfig.Account = settingsSnapshot.Account;
InternalConfig.ServerIP = settingsSnapshot.ServerIP;
InternalConfig.ServerPort = settingsSnapshot.ServerPort;
}
TransferOfflinePrompt(latestRequest.ConnectionAttempt, latestRequest.ConnectionAttempt + 1);
InitializeClient();
}
@ -1197,7 +1237,7 @@ namespace MinecraftClient
if (!string.IsNullOrEmpty(errorMessage) && disconnectReason.HasValue)
{
autoRelogHandled = true;
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage))
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage, CurrentConnectionAttempt))
return;
}
@ -1217,35 +1257,49 @@ namespace MinecraftClient
if (!autoRelogHandled && disconnectReason.HasValue)
{
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage!))
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage!, CurrentConnectionAttempt))
return;
}
BeginOfflinePrompt();
BeginOfflinePrompt(CurrentConnectionAttempt);
}
}
private static void BeginOfflinePrompt()
private static void BeginOfflinePrompt(long connectionAttempt)
{
if (Interlocked.CompareExchange(ref offlinePromptActive, 1, 0) != 0)
return;
ConsoleInputRouter.RouteOffline(HandleOfflineCommand);
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);
offlinePromptRoute.TryActivate(connectionAttempt, () =>
{
ConsoleInputRouter.RouteOffline(HandleOfflineCommand);
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);
});
}
private static void EndOfflinePrompt()
{
if (Interlocked.Exchange(ref offlinePromptActive, 0) == 0)
return;
offlinePromptRoute.TryDeactivate(() =>
{
ConsoleInputRouter.ClearOfflineRoute(HandleOfflineCommand);
ConsoleIO.Reset();
});
}
ConsoleInputRouter.ClearOfflineRoute(HandleOfflineCommand);
ConsoleIO.Reset();
internal static void EndOfflinePrompt(long connectionAttempt)
{
offlinePromptRoute.TryDeactivate(connectionAttempt, () =>
{
ConsoleInputRouter.ClearOfflineRoute(HandleOfflineCommand);
ConsoleIO.Reset();
});
}
private static void TransferOfflinePrompt(long sourceConnectionAttempt, long targetConnectionAttempt)
{
offlinePromptRoute.TryTransfer(sourceConnectionAttempt, targetConnectionAttempt);
}
private static void HandleOfflineCommand(string input)
@ -1269,19 +1323,13 @@ namespace MinecraftClient
{
message = Commands.Reco.DoReconnect(Config.AppVar.ExpandVars(command));
if (message.Length == 0)
{
EndOfflinePrompt();
return;
}
}
else if (command.StartsWith("connect", StringComparison.Ordinal))
{
message = Commands.Connect.DoConnect(Config.AppVar.ExpandVars(command));
if (message.Length == 0)
{
EndOfflinePrompt();
return;
}
}
else if (command.StartsWith("exit", StringComparison.Ordinal)
|| command.StartsWith("quit", StringComparison.Ordinal))