mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Fix Auto Relog reconnect lifecycle
This commit is contained in:
parent
c19fdd6634
commit
64c1dc55e0
20 changed files with 1243 additions and 415 deletions
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using MinecraftClient.Scripting;
|
||||
using Tomlet.Attributes;
|
||||
|
||||
|
|
@ -24,7 +23,7 @@ namespace MinecraftClient.ChatBots
|
|||
public Range Delay = new(3);
|
||||
|
||||
[TomlInlineComment("$ChatBot.AutoRelog.Retries$")]
|
||||
public int Retries = 3;
|
||||
public int Retries = -1;
|
||||
|
||||
[TomlInlineComment("$ChatBot.AutoRelog.Ignore_Kick_Message$")]
|
||||
public bool Ignore_Kick_Message = false;
|
||||
|
|
@ -32,27 +31,27 @@ namespace MinecraftClient.ChatBots
|
|||
[TomlPrecedingComment("$ChatBot.AutoRelog.Kick_Messages$")]
|
||||
public string[] Kick_Messages = new string[] { "Connection has been lost", "Server is restarting", "Server is full", "Too Many people" };
|
||||
|
||||
[NonSerialized]
|
||||
public static int _BotRecoAttempts = 0;
|
||||
|
||||
public void OnSettingUpdate()
|
||||
{
|
||||
Kick_Messages ??= Array.Empty<string>();
|
||||
|
||||
if (!double.IsFinite(Delay.min))
|
||||
Delay.min = 0.1;
|
||||
if (!double.IsFinite(Delay.max))
|
||||
Delay.max = 0.1;
|
||||
|
||||
Delay.min = Math.Max(0.1, Delay.min);
|
||||
Delay.max = Math.Max(0.1, Delay.max);
|
||||
|
||||
double maxDelaySeconds = int.MaxValue / (double)Settings.ClientTicksPerSecond;
|
||||
double maxDelaySeconds = (uint.MaxValue - 1) / 1000D;
|
||||
Delay.min = Math.Min(maxDelaySeconds, Delay.min);
|
||||
Delay.max = Math.Min(maxDelaySeconds, Delay.max);
|
||||
|
||||
if (Delay.min > Delay.max)
|
||||
(Delay.min, Delay.max) = (Delay.max, Delay.min);
|
||||
|
||||
if (Retries == -1)
|
||||
Retries = int.MaxValue;
|
||||
|
||||
if (Enabled)
|
||||
for (int i = 0; i < Kick_Messages.Length; i++)
|
||||
Kick_Messages[i] = Kick_Messages[i].ToLower();
|
||||
if (Retries < -1)
|
||||
Retries = -1;
|
||||
}
|
||||
|
||||
public struct Range
|
||||
|
|
@ -78,9 +77,7 @@ namespace MinecraftClient.ChatBots
|
|||
}
|
||||
}
|
||||
|
||||
private static readonly Lock s_reconnectStateLock = new();
|
||||
private static readonly TimeSpan s_stableJoinBeforeRetryReset = TimeSpan.FromSeconds(60);
|
||||
private static DateTime? s_lastJoinUtc;
|
||||
private static readonly AutoRelogRetryPolicy s_retryPolicy = new(TimeProvider.System);
|
||||
|
||||
/// <summary>
|
||||
/// This bot automatically re-join the server if kick message contains predefined string
|
||||
|
|
@ -100,8 +97,7 @@ namespace MinecraftClient.ChatBots
|
|||
|
||||
public override void AfterGameJoined()
|
||||
{
|
||||
lock (s_reconnectStateLock)
|
||||
s_lastJoinUtc = DateTime.UtcNow;
|
||||
s_retryPolicy.MarkJoined();
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
|
|
@ -123,96 +119,40 @@ namespace MinecraftClient.ChatBots
|
|||
if (reason == DisconnectReason.UserLogout)
|
||||
{
|
||||
LogDebugToConsole(Translations.bot_autoRelog_ignore_user_logout);
|
||||
return false;
|
||||
}
|
||||
else if (Program.HasRestartPendingForAnotherThread)
|
||||
|
||||
message = GetVerbatim(message);
|
||||
LogDebugToConsole(string.Format(Translations.bot_autoRelog_disconnect_msg, message));
|
||||
|
||||
if (!AutoRelogRetryPolicy.ShouldReconnect(
|
||||
reason,
|
||||
message,
|
||||
Config.Ignore_Kick_Message,
|
||||
Config.Kick_Messages,
|
||||
out string? matchedMessage))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (CanReconnect())
|
||||
{
|
||||
message = GetVerbatim(message);
|
||||
string comp = message.ToLower();
|
||||
|
||||
LogDebugToConsole(string.Format(Translations.bot_autoRelog_disconnect_msg, message));
|
||||
|
||||
if (Config.Ignore_Kick_Message)
|
||||
{
|
||||
return LaunchDelayedReconnection(null);
|
||||
}
|
||||
|
||||
foreach (string msg in Config.Kick_Messages)
|
||||
{
|
||||
if (comp.Contains(msg))
|
||||
{
|
||||
return LaunchDelayedReconnection(msg);
|
||||
}
|
||||
}
|
||||
|
||||
LogDebugToConsole(Translations.bot_autoRelog_reconnect_ignore);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool CanReconnect()
|
||||
{
|
||||
lock (s_reconnectStateLock)
|
||||
return Config.Retries < 0 || Configs._BotRecoAttempts < Config.Retries;
|
||||
return LaunchDelayedReconnection(matchedMessage);
|
||||
}
|
||||
|
||||
private static void ResetRetriesAfterStableJoin()
|
||||
{
|
||||
lock (s_reconnectStateLock)
|
||||
{
|
||||
if (Configs._BotRecoAttempts <= 0 || s_lastJoinUtc is not DateTime lastJoinUtc)
|
||||
return;
|
||||
|
||||
if (DateTime.UtcNow - lastJoinUtc < s_stableJoinBeforeRetryReset)
|
||||
return;
|
||||
|
||||
Configs._BotRecoAttempts = 0;
|
||||
s_lastJoinUtc = null;
|
||||
if (s_retryPolicy.ResetAfterStableConnection())
|
||||
McClient.ReconnectionAttemptsLeft = Config.Retries;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryConsumeReconnectAttempt(out int retriesLeft)
|
||||
{
|
||||
lock (s_reconnectStateLock)
|
||||
{
|
||||
bool unlimitedRetries = HasUnlimitedRetries();
|
||||
if (!unlimitedRetries && Configs._BotRecoAttempts >= Config.Retries)
|
||||
{
|
||||
retriesLeft = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
Configs._BotRecoAttempts++;
|
||||
s_lastJoinUtc = null;
|
||||
retriesLeft = unlimitedRetries ? int.MaxValue : Config.Retries - Configs._BotRecoAttempts;
|
||||
if (retriesLeft < 0)
|
||||
retriesLeft = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasUnlimitedRetries()
|
||||
{
|
||||
return Config.Retries < 0 || Config.Retries == int.MaxValue;
|
||||
}
|
||||
|
||||
private static void RollBackReconnectAttempt()
|
||||
{
|
||||
lock (s_reconnectStateLock)
|
||||
{
|
||||
if (Configs._BotRecoAttempts > 0)
|
||||
Configs._BotRecoAttempts--;
|
||||
}
|
||||
return Config.Retries == -1;
|
||||
}
|
||||
|
||||
private bool LaunchDelayedReconnection(string? msg)
|
||||
{
|
||||
if (!TryConsumeReconnectAttempt(out int retriesLeft))
|
||||
if (!s_retryPolicy.TryReserveAttempt(Config.Retries, out int retriesLeft))
|
||||
return false;
|
||||
|
||||
double delay = Random.Shared.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min;
|
||||
|
|
@ -223,14 +163,14 @@ namespace MinecraftClient.ChatBots
|
|||
: retriesLeft.ToString();
|
||||
|
||||
McClient.ReconnectionAttemptsLeft = retriesLeft;
|
||||
if (Program.TryRestart((int)Math.Floor(delay), true))
|
||||
if (Program.TryRestart(TimeSpan.FromSeconds(delay), true))
|
||||
{
|
||||
LogToConsole(string.Format(Translations.bot_autoRelog_wait_with_retries, delay, retriesDisplay));
|
||||
return true;
|
||||
}
|
||||
|
||||
RollBackReconnectAttempt();
|
||||
return true;
|
||||
s_retryPolicy.RollBackReservedAttempt();
|
||||
return Program.HasRestartPending;
|
||||
}
|
||||
|
||||
public static bool OnDisconnectStatic(DisconnectReason reason, string message)
|
||||
|
|
|
|||
108
MinecraftClient/ChatBots/AutoRelogRetryPolicy.cs
Normal file
108
MinecraftClient/ChatBots/AutoRelogRetryPolicy.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using MinecraftClient.Scripting;
|
||||
|
||||
namespace MinecraftClient.ChatBots
|
||||
{
|
||||
internal sealed class AutoRelogRetryPolicy
|
||||
{
|
||||
internal static readonly TimeSpan StableConnectionThreshold = TimeSpan.FromSeconds(60);
|
||||
|
||||
private readonly Lock stateLock = new();
|
||||
private readonly TimeProvider timeProvider;
|
||||
private int attempts;
|
||||
private DateTimeOffset? joinedAt;
|
||||
|
||||
internal AutoRelogRetryPolicy(TimeProvider timeProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||
this.timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
internal int Attempts
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (stateLock)
|
||||
return attempts;
|
||||
}
|
||||
}
|
||||
|
||||
internal void MarkJoined()
|
||||
{
|
||||
lock (stateLock)
|
||||
joinedAt = timeProvider.GetUtcNow();
|
||||
}
|
||||
|
||||
internal bool ResetAfterStableConnection()
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
if (attempts == 0 || joinedAt is not DateTimeOffset connectionStart)
|
||||
return false;
|
||||
|
||||
if (timeProvider.GetUtcNow() - connectionStart < StableConnectionThreshold)
|
||||
return false;
|
||||
|
||||
attempts = 0;
|
||||
joinedAt = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryReserveAttempt(int retryLimit, out int retriesLeft)
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
bool unlimited = retryLimit == -1;
|
||||
if (!unlimited && attempts >= retryLimit)
|
||||
{
|
||||
retriesLeft = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
joinedAt = null;
|
||||
retriesLeft = unlimited ? -1 : Math.Max(0, retryLimit - attempts);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal void RollBackReservedAttempt()
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
if (attempts > 0)
|
||||
attempts--;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool ShouldReconnect(
|
||||
ChatBot.DisconnectReason reason,
|
||||
string message,
|
||||
bool ignoreKickMessage,
|
||||
ReadOnlySpan<string> kickMessages,
|
||||
out string? matchedMessage)
|
||||
{
|
||||
matchedMessage = null;
|
||||
|
||||
if (reason == ChatBot.DisconnectReason.UserLogout)
|
||||
return false;
|
||||
|
||||
if (reason == ChatBot.DisconnectReason.ConnectionLost || ignoreKickMessage)
|
||||
return true;
|
||||
|
||||
foreach (string candidate in kickMessages)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(candidate)
|
||||
&& message.Contains(candidate, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
matchedMessage = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -77,6 +77,22 @@ namespace MinecraftClient
|
|||
/// </summary>
|
||||
public static string? ReadPassword()
|
||||
{
|
||||
if (ConsoleInputRouter.IsStarted)
|
||||
{
|
||||
if (BasicIO || Backend is null)
|
||||
return ConsoleInputRouter.ReadLine();
|
||||
|
||||
Backend.SetInputVisible(false);
|
||||
try
|
||||
{
|
||||
return ConsoleInputRouter.ReadLine();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Backend.SetInputVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (BasicIO)
|
||||
return Console.ReadLine();
|
||||
return Backend.ReadPassword();
|
||||
|
|
@ -87,6 +103,9 @@ namespace MinecraftClient
|
|||
/// </summary>
|
||||
public static string ReadLine()
|
||||
{
|
||||
if (ConsoleInputRouter.IsStarted)
|
||||
return ConsoleInputRouter.ReadLine();
|
||||
|
||||
if (BasicIO)
|
||||
return Console.ReadLine() ?? String.Empty;
|
||||
return Backend.RequestImmediateInput();
|
||||
|
|
|
|||
187
MinecraftClient/ConsoleInputRouter.cs
Normal file
187
MinecraftClient/ConsoleInputRouter.cs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
internal static class ConsoleInputRouter
|
||||
{
|
||||
private static readonly Lock StateLock = new();
|
||||
private static readonly CancellationTokenSource Shutdown = new();
|
||||
private static bool started;
|
||||
private static Action<string>? messageRoute;
|
||||
private static EventHandler<ConsoleInputBuffer>? inputChangeRoute;
|
||||
private static TaskCompletionSource<string>? pendingRead;
|
||||
|
||||
internal static bool IsStarted
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (StateLock)
|
||||
return started;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void EnsureStarted()
|
||||
{
|
||||
lock (StateLock)
|
||||
{
|
||||
if (started)
|
||||
return;
|
||||
|
||||
started = true;
|
||||
if (ConsoleIO.BasicIO || ConsoleIO.Backend is null)
|
||||
{
|
||||
var readThread = new Thread(() => ReadBasicInput(Shutdown.Token))
|
||||
{
|
||||
Name = "MCC console input router",
|
||||
};
|
||||
readThread.Start();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ConsoleIO.Backend.MessageReceived += OnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange += OnInputChanged;
|
||||
ConsoleIO.Backend.BeginReadThread();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ConsoleIO.Backend.MessageReceived -= OnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange -= OnInputChanged;
|
||||
started = false;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void RouteToClient(McClient client)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(client);
|
||||
EnsureStarted();
|
||||
|
||||
lock (StateLock)
|
||||
{
|
||||
messageRoute = client.RouteConsoleInput;
|
||||
inputChangeRoute = ConsoleIO.AutocompleteHandler;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ClearClient(McClient client)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(client);
|
||||
|
||||
lock (StateLock)
|
||||
{
|
||||
if (messageRoute == client.RouteConsoleInput)
|
||||
{
|
||||
messageRoute = null;
|
||||
inputChangeRoute = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void RouteOffline(Action<string> handler)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
EnsureStarted();
|
||||
|
||||
lock (StateLock)
|
||||
{
|
||||
messageRoute = handler;
|
||||
inputChangeRoute = ConsoleIO.OfflineAutocompleteHandler;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ClearOfflineRoute(Action<string> handler)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
|
||||
lock (StateLock)
|
||||
{
|
||||
if (messageRoute == handler)
|
||||
{
|
||||
messageRoute = null;
|
||||
inputChangeRoute = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static string ReadLine()
|
||||
{
|
||||
EnsureStarted();
|
||||
|
||||
TaskCompletionSource<string> readCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
lock (StateLock)
|
||||
{
|
||||
if (pendingRead is not null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
pendingRead = readCompletion;
|
||||
}
|
||||
|
||||
return readCompletion.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
internal static void ShutdownRouter()
|
||||
{
|
||||
lock (StateLock)
|
||||
{
|
||||
messageRoute = null;
|
||||
inputChangeRoute = null;
|
||||
pendingRead?.TrySetCanceled();
|
||||
pendingRead = null;
|
||||
Shutdown.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadBasicInput(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
string? input = Console.ReadLine();
|
||||
if (input is null)
|
||||
return;
|
||||
|
||||
DispatchMessage(input);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnMessageReceived(object? sender, string message)
|
||||
{
|
||||
DispatchMessage(message);
|
||||
}
|
||||
|
||||
private static void OnInputChanged(object? sender, ConsoleInputBuffer input)
|
||||
{
|
||||
EventHandler<ConsoleInputBuffer>? route;
|
||||
lock (StateLock)
|
||||
route = inputChangeRoute;
|
||||
|
||||
route?.Invoke(sender, input);
|
||||
}
|
||||
|
||||
private static void DispatchMessage(string message)
|
||||
{
|
||||
TaskCompletionSource<string>? readCompletion;
|
||||
Action<string>? route;
|
||||
|
||||
lock (StateLock)
|
||||
{
|
||||
readCompletion = pendingRead;
|
||||
if (readCompletion is not null)
|
||||
pendingRead = null;
|
||||
route = messageRoute;
|
||||
}
|
||||
|
||||
if (readCompletion is not null)
|
||||
{
|
||||
readCompletion.TrySetResult(message);
|
||||
return;
|
||||
}
|
||||
|
||||
route?.Invoke(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System.Net;
|
|||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Brigadier.NET;
|
||||
using Brigadier.NET.Exceptions;
|
||||
using MinecraftClient.ChatBots;
|
||||
|
|
@ -228,12 +229,11 @@ namespace MinecraftClient
|
|||
TcpClient client = null!;
|
||||
IMinecraftCom handler = null!;
|
||||
SessionToken _sessionToken;
|
||||
CancellationTokenSource? cmdprompt = null;
|
||||
Tuple<Thread, CancellationTokenSource>? timeoutdetector = null;
|
||||
private Thread? basicIOReadThread;
|
||||
private int transferInProgress = 0;
|
||||
private bool consoleReadThreadOwned = false;
|
||||
private bool consoleHandlersAttached = false;
|
||||
private int disconnectState;
|
||||
private int disconnectOwnerThreadId;
|
||||
private readonly TaskCompletionSource<bool> disconnectCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public ILogger Log;
|
||||
public DialogManager Dialogs { get; }
|
||||
|
|
@ -530,75 +530,12 @@ namespace MinecraftClient
|
|||
|
||||
private void StartConsoleSession()
|
||||
{
|
||||
cmdprompt = new CancellationTokenSource();
|
||||
|
||||
if (ConsoleIO.BasicIO || ConsoleIO.Backend is null)
|
||||
{
|
||||
if (!consoleReadThreadOwned)
|
||||
{
|
||||
CancellationToken token = cmdprompt.Token;
|
||||
basicIOReadThread = new Thread(() => BasicIOReadLoop(token))
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "MCC BasicIO read thread"
|
||||
};
|
||||
basicIOReadThread.Start();
|
||||
consoleReadThreadOwned = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!consoleReadThreadOwned)
|
||||
{
|
||||
ConsoleIO.Backend.BeginReadThread();
|
||||
consoleReadThreadOwned = true;
|
||||
}
|
||||
|
||||
if (!consoleHandlersAttached)
|
||||
{
|
||||
ConsoleIO.Backend.MessageReceived += ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange += ConsoleIO.AutocompleteHandler;
|
||||
consoleHandlersAttached = true;
|
||||
}
|
||||
ConsoleInputRouter.RouteToClient(this);
|
||||
}
|
||||
|
||||
private void StopConsoleSession()
|
||||
{
|
||||
if (ConsoleIO.BasicIO || ConsoleIO.Backend is null)
|
||||
{
|
||||
cmdprompt?.Cancel();
|
||||
basicIOReadThread = null;
|
||||
consoleReadThreadOwned = false;
|
||||
consoleHandlersAttached = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (consoleHandlersAttached)
|
||||
{
|
||||
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
consoleHandlersAttached = false;
|
||||
}
|
||||
|
||||
if (consoleReadThreadOwned)
|
||||
{
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
consoleReadThreadOwned = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void BasicIOReadLoop(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
string? input = Console.ReadLine();
|
||||
if (input is null)
|
||||
return;
|
||||
|
||||
if (!token.IsCancellationRequested)
|
||||
ConsoleReaderOnMessageReceived(this, input);
|
||||
}
|
||||
ConsoleInputRouter.ClearClient(this);
|
||||
}
|
||||
|
||||
private void ResetStateForTransfer()
|
||||
|
|
@ -866,36 +803,21 @@ namespace MinecraftClient
|
|||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
instance = 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)
|
||||
if (!TryBeginDisconnect())
|
||||
{
|
||||
handler.Disconnect();
|
||||
handler.Dispose();
|
||||
if (Volatile.Read(ref disconnectOwnerThreadId) != Environment.CurrentManagedThreadId)
|
||||
disconnectCompletion.Task.GetAwaiter().GetResult();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmdprompt is not null)
|
||||
try
|
||||
{
|
||||
cmdprompt.Cancel();
|
||||
cmdprompt = null;
|
||||
DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, string.Empty));
|
||||
}
|
||||
|
||||
if (timeoutdetector is not null)
|
||||
finally
|
||||
{
|
||||
timeoutdetector.Item2.Cancel();
|
||||
timeoutdetector = null;
|
||||
CompleteDisconnect(sendDisconnectPacket: true);
|
||||
}
|
||||
|
||||
if (client is not null)
|
||||
client.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -903,74 +825,133 @@ namespace MinecraftClient
|
|||
/// </summary>
|
||||
public void OnConnectionLost(ChatBot.DisconnectReason reason, string message)
|
||||
{
|
||||
instance = null;
|
||||
if (reason == ChatBot.DisconnectReason.UserLogout)
|
||||
throw new InvalidOperationException(Translations.exception_user_logout);
|
||||
|
||||
ConsoleIO.CancelAutocomplete();
|
||||
if (!TryBeginDisconnect())
|
||||
return;
|
||||
|
||||
handler.Dispose();
|
||||
|
||||
world.Clear();
|
||||
ClearKnownSigns();
|
||||
|
||||
if (timeoutdetector is not null)
|
||||
bool restartScheduled = false;
|
||||
try
|
||||
{
|
||||
if (timeoutdetector is not null && Thread.CurrentThread != timeoutdetector.Item1)
|
||||
timeoutdetector.Item2.Cancel();
|
||||
timeoutdetector = null;
|
||||
}
|
||||
ConsoleIO.CancelAutocomplete();
|
||||
|
||||
bool exitOnFailure = Program.PrepareExitOnFailure();
|
||||
bool will_restart = false;
|
||||
world.Clear();
|
||||
ClearKnownSigns();
|
||||
|
||||
switch (reason)
|
||||
{
|
||||
case ChatBot.DisconnectReason.ConnectionLost:
|
||||
message = Translations.mcc_disconnect_lost;
|
||||
Log.Info(message);
|
||||
break;
|
||||
bool exitOnFailure = Program.PrepareExitOnFailure();
|
||||
|
||||
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
|
||||
switch (reason)
|
||||
{
|
||||
bool botWillRestart = bot.OnDisconnect(reason, message);
|
||||
if (!exitOnFailure)
|
||||
will_restart |= botWillRestart;
|
||||
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;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
// Process AutoRelog last so every other bot can complete cleanup first.
|
||||
List<ChatBot> onDisconnectBotList = bots.Where(bot => bot is not AutoRelog).ToList();
|
||||
onDisconnectBotList.AddRange(bots.Where(bot => bot is AutoRelog));
|
||||
|
||||
foreach (ChatBot bot in onDisconnectBotList)
|
||||
{
|
||||
if (e is not ThreadAbortException)
|
||||
try
|
||||
{
|
||||
Log.Warn("OnDisconnect: Got error from " + bot.ToString() + ": " + e.ToString());
|
||||
_ = bot.OnDisconnect(reason, message);
|
||||
}
|
||||
catch (Exception exception) when (exception is not ThreadAbortException)
|
||||
{
|
||||
Log.Warn("OnDisconnect: Got error from " + bot + ": " + exception);
|
||||
}
|
||||
else throw; //ThreadAbortException should not be caught
|
||||
}
|
||||
|
||||
restartScheduled = !exitOnFailure && Program.HasRestartPending;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CompleteDisconnect(sendDisconnectPacket: false);
|
||||
}
|
||||
|
||||
SentrySdk.EndSession();
|
||||
|
||||
if (!will_restart)
|
||||
{
|
||||
StopConsoleSession();
|
||||
if (!restartScheduled)
|
||||
Program.HandleFailure(null, false, reason);
|
||||
}
|
||||
|
||||
private bool TryBeginDisconnect()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref disconnectState, 1, 0) != 0)
|
||||
return false;
|
||||
|
||||
Volatile.Write(ref disconnectOwnerThreadId, Environment.CurrentManagedThreadId);
|
||||
instance = null;
|
||||
StopConsoleSession();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CompleteDisconnect(bool sendDisconnectPacket)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (ChatBot bot in bots.Where(bot => bot.ScriptOwnerKey is not null).ToList())
|
||||
{
|
||||
try
|
||||
{
|
||||
BotUnLoad(bot);
|
||||
}
|
||||
catch (Exception exception) when (exception is not ThreadAbortException)
|
||||
{
|
||||
Log.Warn(exception.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
botsOnHold.Clear();
|
||||
botsOnHold.AddRange(bots.Where(bot => bot.ScriptOwnerKey is null));
|
||||
|
||||
if (timeoutdetector is not null)
|
||||
{
|
||||
CancellationTokenSource timeoutCancellation = timeoutdetector.Item2;
|
||||
timeoutdetector = null;
|
||||
RunDisconnectCleanup(timeoutCancellation.Cancel);
|
||||
}
|
||||
|
||||
if (handler is not null)
|
||||
{
|
||||
if (sendDisconnectPacket)
|
||||
RunDisconnectCleanup(handler.Disconnect);
|
||||
RunDisconnectCleanup(handler.Dispose);
|
||||
}
|
||||
|
||||
if (client is not null)
|
||||
RunDisconnectCleanup(client.Close);
|
||||
ClearTasks();
|
||||
RunDisconnectCleanup(() => SentrySdk.EndSession());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Volatile.Write(ref disconnectState, 2);
|
||||
Volatile.Write(ref disconnectOwnerThreadId, 0);
|
||||
disconnectCompletion.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void RunDisconnectCleanup(Action cleanup)
|
||||
{
|
||||
try
|
||||
{
|
||||
cleanup();
|
||||
}
|
||||
catch (Exception exception) when (exception is not ThreadAbortException)
|
||||
{
|
||||
Log.Warn(exception.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -980,19 +961,16 @@ namespace MinecraftClient
|
|||
|
||||
private void ConsoleReaderOnMessageReceived(object? sender, string e)
|
||||
{
|
||||
|
||||
if (client.Client is null)
|
||||
return;
|
||||
|
||||
if (client.Client.Connected)
|
||||
{
|
||||
new Thread(() =>
|
||||
{
|
||||
InvokeOnMainThread(() => HandleCommandPromptText(e));
|
||||
}).Start();
|
||||
}
|
||||
else
|
||||
return;
|
||||
InvokeOnMainThreadAsync(() => HandleCommandPromptText(e));
|
||||
}
|
||||
|
||||
internal void RouteConsoleInput(string input)
|
||||
{
|
||||
ConsoleReaderOnMessageReceived(this, input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1221,6 +1199,23 @@ namespace MinecraftClient
|
|||
InvokeOnMainThread(() => { task(); return true; });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue work for the network main thread without blocking the calling thread.
|
||||
/// </summary>
|
||||
internal void InvokeOnMainThreadAsync(Action task)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(task);
|
||||
|
||||
if (!InvokeRequired)
|
||||
{
|
||||
task();
|
||||
return;
|
||||
}
|
||||
|
||||
lock (threadTasksLock)
|
||||
threadTasks.Enqueue(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all tasks
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -51,11 +51,11 @@ namespace MinecraftClient
|
|||
public const string MCHighestVersion = "26.2";
|
||||
public static readonly string? BuildInfo = null;
|
||||
|
||||
private static Tuple<Thread, CancellationTokenSource>? offlinePrompt = null;
|
||||
private static IDisposable? _sentrySdk = null;
|
||||
private static bool useMcVersionOnce = false;
|
||||
private static Thread? _restartThread = null;
|
||||
private static readonly object _restartLock = new();
|
||||
private static readonly RestartCoordinator restartCoordinator = new(ExecuteRestartAsync, ReportRestartFailure);
|
||||
private static long connectionAttempt;
|
||||
private static int offlinePromptActive;
|
||||
private static int exitOnFailurePending;
|
||||
private static string settingsIniPath = "MinecraftClient.ini";
|
||||
|
||||
|
|
@ -608,6 +608,8 @@ namespace MinecraftClient
|
|||
/// </summary>
|
||||
private static void InitializeClient()
|
||||
{
|
||||
Interlocked.Increment(ref connectionAttempt);
|
||||
|
||||
// Ensure that we use the provided Minecraft version if we can't connect automatically.
|
||||
//
|
||||
// useMcVersionOnce is set to true on HandleFailure()
|
||||
|
|
@ -737,6 +739,8 @@ namespace MinecraftClient
|
|||
Config.Main.SetServerIP(new MainConfigHelper.MainConfig.ServerInfoConfig(addressInput), true);
|
||||
}
|
||||
|
||||
ConsoleInputRouter.EnsureStarted();
|
||||
|
||||
//Get server version
|
||||
int protocolversion = 0;
|
||||
ForgeInfo? forgeInfo = null;
|
||||
|
|
@ -901,69 +905,60 @@ namespace MinecraftClient
|
|||
/// <param name="keepAccountAndServerSettings">Optional, keep account and server settings</param>
|
||||
public static void Restart(int delaySeconds = 0, bool keepAccountAndServerSettings = false)
|
||||
{
|
||||
TryRestart(delaySeconds, keepAccountAndServerSettings);
|
||||
TryRestart(TimeSpan.FromSeconds(Math.Max(0, delaySeconds)), keepAccountAndServerSettings);
|
||||
}
|
||||
|
||||
internal static bool HasRestartPendingForAnotherThread
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_restartLock)
|
||||
return HasRestartPendingForAnotherThreadNoLock();
|
||||
}
|
||||
}
|
||||
internal static bool HasRestartPending => restartCoordinator.HasScheduledRestart(Volatile.Read(ref connectionAttempt));
|
||||
|
||||
internal static bool TryRestart(int delaySeconds = 0, bool keepAccountAndServerSettings = false)
|
||||
{
|
||||
lock (_restartLock)
|
||||
{
|
||||
if (Volatile.Read(ref exitOnFailurePending) != 0)
|
||||
return false;
|
||||
|
||||
if (HasRestartPendingForAnotherThreadNoLock())
|
||||
return false;
|
||||
|
||||
ConsoleIO.Backend?.StopReadThread();
|
||||
var thread = new Thread(new ThreadStart(delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
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(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset();
|
||||
}
|
||||
if (delaySeconds > 0)
|
||||
{
|
||||
ConsoleIO.WriteLine(string.Format(Translations.mcc_restart_delay, delaySeconds));
|
||||
Thread.Sleep(delaySeconds * 1000);
|
||||
}
|
||||
ConsoleIO.WriteLine(Translations.mcc_restart);
|
||||
ReloadSettings(keepAccountAndServerSettings);
|
||||
InitializeClient();
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_restartLock)
|
||||
{
|
||||
if (_restartThread == Thread.CurrentThread)
|
||||
_restartThread = null;
|
||||
}
|
||||
}
|
||||
}));
|
||||
_restartThread = thread;
|
||||
thread.Start();
|
||||
return true;
|
||||
}
|
||||
return TryRestart(TimeSpan.FromSeconds(Math.Max(0, delaySeconds)), keepAccountAndServerSettings);
|
||||
}
|
||||
|
||||
private static bool HasRestartPendingForAnotherThreadNoLock()
|
||||
internal static bool TryRestart(TimeSpan delay, bool keepAccountAndServerSettings = false)
|
||||
{
|
||||
return _restartThread is not null
|
||||
&& _restartThread.IsAlive
|
||||
&& _restartThread != Thread.CurrentThread;
|
||||
if (Volatile.Read(ref exitOnFailurePending) != 0)
|
||||
return false;
|
||||
|
||||
if (delay < TimeSpan.Zero)
|
||||
delay = TimeSpan.Zero;
|
||||
|
||||
return restartCoordinator.TrySchedule(new RestartRequest(
|
||||
Volatile.Read(ref connectionAttempt),
|
||||
delay,
|
||||
keepAccountAndServerSettings));
|
||||
}
|
||||
|
||||
private static async Task ExecuteRestartAsync(RestartRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
McClient? disconnectedClient = client;
|
||||
if (disconnectedClient is not null)
|
||||
{
|
||||
disconnectedClient.Disconnect();
|
||||
if (ReferenceEquals(client, disconnectedClient))
|
||||
client = null;
|
||||
}
|
||||
|
||||
EndOfflinePrompt();
|
||||
ConsoleIO.Reset();
|
||||
|
||||
if (request.Delay > TimeSpan.Zero)
|
||||
{
|
||||
ConsoleIO.WriteLine(string.Format(Translations.mcc_restart_delay, request.Delay.TotalSeconds));
|
||||
await Task.Delay(request.Delay, TimeProvider.System, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ConsoleIO.WriteLine(Translations.mcc_restart);
|
||||
ReloadSettings(request.KeepAccountAndServerSettings);
|
||||
InitializeClient();
|
||||
}
|
||||
|
||||
private static void ReportRestartFailure(Exception exception)
|
||||
{
|
||||
SentrySdk.CaptureException(exception);
|
||||
ConsoleIO.WriteLine(exception.ToString());
|
||||
HandleFailure();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -976,6 +971,7 @@ namespace MinecraftClient
|
|||
return false;
|
||||
|
||||
Interlocked.Exchange(ref exitOnFailurePending, 1);
|
||||
restartCoordinator.Stop();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -984,17 +980,10 @@ namespace MinecraftClient
|
|||
WriteBackSettings();
|
||||
ConsoleIO.WriteLineFormatted("§a" + string.Format(Translations.config_saving, settingsIniPath));
|
||||
|
||||
restartCoordinator.Stop();
|
||||
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();
|
||||
}
|
||||
EndOfflinePrompt();
|
||||
ConsoleInputRouter.ShutdownRouter();
|
||||
if (Config.Main.Advanced.PlayerHeadAsIcon && OperatingSystem.IsWindows()) { ConsoleIcon.RevertToMCCIcon(); }
|
||||
ConsoleIO.Backend?.Shutdown();
|
||||
Environment.Exit(exitcode);
|
||||
|
|
@ -1022,7 +1011,7 @@ namespace MinecraftClient
|
|||
if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
ConsoleIO.Reset();
|
||||
if (ConsoleIO.Backend is not Tui.TuiConsoleBackend)
|
||||
if (!ConsoleInputRouter.IsStarted && ConsoleIO.Backend is not Tui.TuiConsoleBackend)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -1067,85 +1056,89 @@ namespace MinecraftClient
|
|||
return;
|
||||
}
|
||||
|
||||
if (offlinePrompt is null)
|
||||
BeginOfflinePrompt();
|
||||
}
|
||||
}
|
||||
|
||||
private static void BeginOfflinePrompt()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
private static void EndOfflinePrompt()
|
||||
{
|
||||
if (Interlocked.Exchange(ref offlinePromptActive, 0) == 0)
|
||||
return;
|
||||
|
||||
ConsoleInputRouter.ClearOfflineRoute(HandleOfflineCommand);
|
||||
ConsoleIO.Reset();
|
||||
}
|
||||
|
||||
private static void HandleOfflineCommand(string input)
|
||||
{
|
||||
string command = input.Trim();
|
||||
if (command.Length == 0)
|
||||
{
|
||||
if (ConsoleIO.Backend is not Tui.TuiConsoleBackend)
|
||||
Commands.Exit.DoExit(Config.AppVar.ExpandVars(command));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' '
|
||||
&& command[0] == Config.Main.Advanced.InternalCmdChar.ToChar())
|
||||
{
|
||||
command = command[1..];
|
||||
}
|
||||
|
||||
string message = string.Empty;
|
||||
if (command.StartsWith("reco", StringComparison.Ordinal))
|
||||
{
|
||||
message = Commands.Reco.DoReconnect(Config.AppVar.ExpandVars(command));
|
||||
if (message.Length == 0)
|
||||
{
|
||||
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.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);
|
||||
}
|
||||
})), cancellationTokenSource);
|
||||
offlinePrompt.Item1.Start();
|
||||
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))
|
||||
{
|
||||
message = Commands.Exit.DoExit(Config.AppVar.ExpandVars(command));
|
||||
}
|
||||
else if (command.StartsWith("help", StringComparison.Ordinal))
|
||||
{
|
||||
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.Length != 0)
|
||||
ConsoleIO.WriteLineFormatted("§8MCC: " + message);
|
||||
}
|
||||
|
||||
private static int GetFailureExitCode(ChatBot.DisconnectReason? disconnectReason)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("MinecraftClient.Tests")]
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
|
|
|
|||
|
|
@ -4082,20 +4082,19 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
{
|
||||
try
|
||||
{
|
||||
if (netMain is not null)
|
||||
netMain?.Item2.Cancel();
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
netMain.Item2.Cancel();
|
||||
netReader?.Item2.Cancel();
|
||||
}
|
||||
|
||||
if (netReader is not null)
|
||||
finally
|
||||
{
|
||||
netReader.Item2.Cancel();
|
||||
socketWrapper.Disconnect();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using MinecraftClient.Crypto;
|
||||
|
||||
|
|
@ -38,7 +39,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <returns>TRUE if data is available to read</returns>
|
||||
public bool HasDataAvailable()
|
||||
{
|
||||
return c.Client.Available > 0;
|
||||
return c.Client.Available > 0 || c.Client.Poll(0, SelectMode.SelectRead);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -61,10 +62,16 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
int read = 0;
|
||||
while (read < offset)
|
||||
{
|
||||
int bytesRead;
|
||||
if (encrypted)
|
||||
read += s!.Read(buffer, start + read, offset - read);
|
||||
bytesRead = s!.Read(buffer, start + read, offset - read);
|
||||
else
|
||||
read += c.Client.Receive(buffer, start + read, offset - read, f);
|
||||
bytesRead = c.Client.Receive(buffer, start + read, offset - read, f);
|
||||
|
||||
if (bytesRead == 0)
|
||||
throw new EndOfStreamException();
|
||||
|
||||
read += bytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -663,7 +663,7 @@ namespace MinecraftClient {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to When set to true, autorelog will reconnect regardless of kick messages..
|
||||
/// Looks up a localized string similar to Reconnect after any server kick or login rejection. Network interruptions always trigger Auto Relog..
|
||||
/// </summary>
|
||||
internal static string ChatBot_AutoRelog_Ignore_Kick_Message {
|
||||
get {
|
||||
|
|
@ -672,7 +672,7 @@ namespace MinecraftClient {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to If the kickout message matches any of the strings, then autorelog will be triggered..
|
||||
/// Looks up a localized string similar to Case-insensitive text fragments that trigger Auto Relog for server kicks and login rejections..
|
||||
/// </summary>
|
||||
internal static string ChatBot_AutoRelog_Kick_Messages {
|
||||
get {
|
||||
|
|
@ -681,7 +681,7 @@ namespace MinecraftClient {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Retries when failing to relog to the server. use -1 for unlimited retries..
|
||||
/// Looks up a localized string similar to Exact retry limit. Use 0 to disable retries or -1 for unlimited retries. The count resets after 60 seconds online..
|
||||
/// </summary>
|
||||
internal static string ChatBot_AutoRelog_Retries {
|
||||
get {
|
||||
|
|
|
|||
|
|
@ -352,13 +352,13 @@ You can use "/fish" to control the bot manually.
|
|||
<value>The delay time before joining the server. (in seconds)</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoRelog.Ignore_Kick_Message" xml:space="preserve">
|
||||
<value>When set to true, autorelog will reconnect regardless of kick messages.</value>
|
||||
<value>Reconnect after any server kick or login rejection. Network interruptions always trigger Auto Relog.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoRelog.Kick_Messages" xml:space="preserve">
|
||||
<value>If the kickout message matches any of the strings, then autorelog will be triggered.</value>
|
||||
<value>Case-insensitive text fragments that trigger Auto Relog for server kicks and login rejections.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoRelog.Retries" xml:space="preserve">
|
||||
<value>Retries when failing to relog to the server. use -1 for unlimited retries.</value>
|
||||
<value>Exact retry limit. Use 0 to disable retries or -1 for unlimited retries. The count resets after 60 seconds online.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoRespond" xml:space="preserve">
|
||||
<value>Run commands or send messages automatically when a specified pattern is detected in chat
|
||||
|
|
|
|||
119
MinecraftClient/RestartCoordinator.cs
Normal file
119
MinecraftClient/RestartCoordinator.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
internal readonly record struct RestartRequest(
|
||||
long ConnectionAttempt,
|
||||
TimeSpan Delay,
|
||||
bool KeepAccountAndServerSettings);
|
||||
|
||||
internal sealed class RestartCoordinator : IDisposable
|
||||
{
|
||||
private readonly Lock stateLock = new();
|
||||
private readonly Channel<RestartRequest> requests;
|
||||
private readonly CancellationTokenSource shutdown = new();
|
||||
private readonly Func<RestartRequest, CancellationToken, Task> restart;
|
||||
private readonly Action<Exception> reportFailure;
|
||||
private readonly Task worker;
|
||||
private readonly HashSet<long> pendingAttempts = [];
|
||||
private long highestScheduledAttempt = -1;
|
||||
private bool stopped;
|
||||
|
||||
internal RestartCoordinator(
|
||||
Func<RestartRequest, CancellationToken, Task> restart,
|
||||
Action<Exception> reportFailure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(restart);
|
||||
ArgumentNullException.ThrowIfNull(reportFailure);
|
||||
|
||||
this.restart = restart;
|
||||
this.reportFailure = reportFailure;
|
||||
requests = Channel.CreateUnbounded<RestartRequest>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
AllowSynchronousContinuations = false,
|
||||
});
|
||||
worker = ProcessRequestsAsync();
|
||||
}
|
||||
|
||||
internal bool HasScheduledRestart(long connectionAttempt)
|
||||
{
|
||||
lock (stateLock)
|
||||
return !stopped && pendingAttempts.Contains(connectionAttempt);
|
||||
}
|
||||
|
||||
internal bool TrySchedule(RestartRequest request)
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
if (stopped || request.ConnectionAttempt <= highestScheduledAttempt)
|
||||
return false;
|
||||
|
||||
highestScheduledAttempt = request.ConnectionAttempt;
|
||||
pendingAttempts.Add(request.ConnectionAttempt);
|
||||
if (requests.Writer.TryWrite(request))
|
||||
return true;
|
||||
|
||||
pendingAttempts.Remove(request.ConnectionAttempt);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal void Stop()
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
if (stopped)
|
||||
return;
|
||||
|
||||
stopped = true;
|
||||
pendingAttempts.Clear();
|
||||
requests.Writer.TryComplete();
|
||||
shutdown.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessRequestsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (RestartRequest request in requests.Reader.ReadAllAsync(shutdown.Token).ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
await restart(request, shutdown.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (shutdown.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
reportFailure(exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (stateLock)
|
||||
pendingAttempts.Remove(request.ConnectionAttempt);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (shutdown.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
worker.GetAwaiter().GetResult();
|
||||
shutdown.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue