mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
fix: isolate queued restart settings
This commit is contained in:
parent
1686d00f30
commit
9b90b535d8
5 changed files with 99 additions and 21 deletions
|
|
@ -3,20 +3,27 @@ namespace MinecraftClient.Tests;
|
||||||
public sealed class RestartCoordinatorTests
|
public sealed class RestartCoordinatorTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CoalescesSameAttemptAndQueuesNewerAttempt()
|
public async Task ReplacesQueuedSameAttemptAndQueuesNewerAttempt()
|
||||||
{
|
{
|
||||||
var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var replacementCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
var secondCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
var secondCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var executedAccounts = new List<string>();
|
||||||
|
|
||||||
using var coordinator = new RestartCoordinator(
|
using var coordinator = new RestartCoordinator(
|
||||||
async (request, cancellationToken) =>
|
async (request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
if (request.ConnectionAttempt == 10)
|
if (request.ConnectionAttempt == 9)
|
||||||
{
|
{
|
||||||
firstStarted.SetResult();
|
firstStarted.SetResult();
|
||||||
await releaseFirst.Task.WaitAsync(cancellationToken);
|
await releaseFirst.Task.WaitAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
else if (request.ConnectionAttempt == 10)
|
||||||
|
{
|
||||||
|
executedAccounts.Add(request.SettingsSnapshot?.Account.Login ?? string.Empty);
|
||||||
|
replacementCompleted.SetResult();
|
||||||
|
}
|
||||||
else if (request.ConnectionAttempt == 11)
|
else if (request.ConnectionAttempt == 11)
|
||||||
{
|
{
|
||||||
secondCompleted.SetResult();
|
secondCompleted.SetResult();
|
||||||
|
|
@ -24,15 +31,19 @@ public sealed class RestartCoordinatorTests
|
||||||
},
|
},
|
||||||
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
||||||
|
|
||||||
Assert.True(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true)));
|
Assert.True(coordinator.TrySchedule(new RestartRequest(9, TimeSpan.Zero, true)));
|
||||||
await firstStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
await firstStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
Assert.False(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true)));
|
Assert.True(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true, CreateSettingsSnapshot("first"))));
|
||||||
|
Assert.True(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true, CreateSettingsSnapshot("replacement"))));
|
||||||
Assert.True(coordinator.TrySchedule(new RestartRequest(11, TimeSpan.Zero, true)));
|
Assert.True(coordinator.TrySchedule(new RestartRequest(11, TimeSpan.Zero, true)));
|
||||||
Assert.True(coordinator.HasScheduledRestart(11));
|
Assert.True(coordinator.HasScheduledRestart(11));
|
||||||
|
|
||||||
releaseFirst.SetResult();
|
releaseFirst.SetResult();
|
||||||
|
await replacementCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
await secondCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
await secondCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
Assert.Equal(["replacement"], executedAccounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -46,6 +57,25 @@ public sealed class RestartCoordinatorTests
|
||||||
Assert.False(coordinator.TrySchedule(new RestartRequest(19, TimeSpan.Zero, true)));
|
Assert.False(coordinator.TrySchedule(new RestartRequest(19, TimeSpan.Zero, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RejectsCompletedAttempt()
|
||||||
|
{
|
||||||
|
var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
using var coordinator = new RestartCoordinator(
|
||||||
|
(_, _) =>
|
||||||
|
{
|
||||||
|
completed.SetResult();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
||||||
|
|
||||||
|
Assert.True(coordinator.TrySchedule(new RestartRequest(20, TimeSpan.Zero, true)));
|
||||||
|
await completed.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
Assert.True(SpinWait.SpinUntil(() => !coordinator.HasScheduledRestart(20), TimeSpan.FromSeconds(5)));
|
||||||
|
|
||||||
|
Assert.False(coordinator.TrySchedule(new RestartRequest(20, TimeSpan.Zero, true)));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TerminalStopRejectsFurtherRestarts()
|
public void TerminalStopRejectsFurtherRestarts()
|
||||||
{
|
{
|
||||||
|
|
@ -58,4 +88,12 @@ public sealed class RestartCoordinatorTests
|
||||||
Assert.False(coordinator.TrySchedule(new RestartRequest(1, TimeSpan.Zero, true)));
|
Assert.False(coordinator.TrySchedule(new RestartRequest(1, TimeSpan.Zero, true)));
|
||||||
Assert.False(coordinator.HasScheduledRestart(1));
|
Assert.False(coordinator.HasScheduledRestart(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RestartSettingsSnapshot CreateSettingsSnapshot(string account)
|
||||||
|
{
|
||||||
|
return new RestartSettingsSnapshot(
|
||||||
|
new Settings.MainConfigHelper.MainConfig.AccountInfoConfig(account, "-"),
|
||||||
|
"localhost",
|
||||||
|
25565);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,9 @@ namespace MinecraftClient.Commands
|
||||||
|
|
||||||
if (Settings.Config.Main.SetServerIP(new Settings.MainConfigHelper.MainConfig.ServerInfoConfig(server), true))
|
if (Settings.Config.Main.SetServerIP(new Settings.MainConfigHelper.MainConfig.ServerInfoConfig(server), true))
|
||||||
{
|
{
|
||||||
Program.Restart(keepAccountAndServerSettings: true);
|
return r.SetAndReturn(Program.TryRestart(keepAccountAndServerSettings: true)
|
||||||
return r.SetAndReturn(Status.Done);
|
? Status.Done
|
||||||
|
: Status.Fail);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -64,8 +65,9 @@ namespace MinecraftClient.Commands
|
||||||
|
|
||||||
if (Settings.Config.Main.SetServerIP(new Settings.MainConfigHelper.MainConfig.ServerInfoConfig(args[0]), true))
|
if (Settings.Config.Main.SetServerIP(new Settings.MainConfigHelper.MainConfig.ServerInfoConfig(args[0]), true))
|
||||||
{
|
{
|
||||||
Program.Restart(keepAccountAndServerSettings: true);
|
return Program.TryRestart(keepAccountAndServerSettings: true)
|
||||||
return string.Empty;
|
? string.Empty
|
||||||
|
: Translations.general_fail;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,9 @@ namespace MinecraftClient.Commands
|
||||||
if (!Settings.Config.Main.Advanced.SetAccount(account))
|
if (!Settings.Config.Main.Advanced.SetAccount(account))
|
||||||
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_connect_unknown, account));
|
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_connect_unknown, account));
|
||||||
}
|
}
|
||||||
Program.Restart(keepAccountAndServerSettings: true);
|
return r.SetAndReturn(Program.TryRestart(keepAccountAndServerSettings: true)
|
||||||
return r.SetAndReturn(CmdResult.Status.Done);
|
? CmdResult.Status.Done
|
||||||
|
: CmdResult.Status.Fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static string DoReconnect(string command)
|
internal static string DoReconnect(string command)
|
||||||
|
|
@ -62,8 +63,9 @@ namespace MinecraftClient.Commands
|
||||||
return string.Format(Translations.cmd_connect_unknown, account);
|
return string.Format(Translations.cmd_connect_unknown, account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Program.Restart(keepAccountAndServerSettings: true);
|
return Program.TryRestart(keepAccountAndServerSettings: true)
|
||||||
return String.Empty;
|
? String.Empty
|
||||||
|
: Translations.general_fail;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -923,10 +923,15 @@ namespace MinecraftClient
|
||||||
if (delay < TimeSpan.Zero)
|
if (delay < TimeSpan.Zero)
|
||||||
delay = TimeSpan.Zero;
|
delay = TimeSpan.Zero;
|
||||||
|
|
||||||
|
RestartSettingsSnapshot? settingsSnapshot = keepAccountAndServerSettings
|
||||||
|
? new RestartSettingsSnapshot(InternalConfig.Account, InternalConfig.ServerIP, InternalConfig.ServerPort)
|
||||||
|
: null;
|
||||||
|
|
||||||
return restartCoordinator.TrySchedule(new RestartRequest(
|
return restartCoordinator.TrySchedule(new RestartRequest(
|
||||||
Volatile.Read(ref connectionAttempt),
|
Volatile.Read(ref connectionAttempt),
|
||||||
delay,
|
delay,
|
||||||
keepAccountAndServerSettings));
|
keepAccountAndServerSettings,
|
||||||
|
settingsSnapshot));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task ExecuteRestartAsync(RestartRequest request, CancellationToken cancellationToken)
|
private static async Task ExecuteRestartAsync(RestartRequest request, CancellationToken cancellationToken)
|
||||||
|
|
@ -951,6 +956,12 @@ namespace MinecraftClient
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
ConsoleIO.WriteLine(Translations.mcc_restart);
|
ConsoleIO.WriteLine(Translations.mcc_restart);
|
||||||
ReloadSettings(request.KeepAccountAndServerSettings);
|
ReloadSettings(request.KeepAccountAndServerSettings);
|
||||||
|
if (request.SettingsSnapshot is RestartSettingsSnapshot settingsSnapshot)
|
||||||
|
{
|
||||||
|
InternalConfig.Account = settingsSnapshot.Account;
|
||||||
|
InternalConfig.ServerIP = settingsSnapshot.ServerIP;
|
||||||
|
InternalConfig.ServerPort = settingsSnapshot.ServerPort;
|
||||||
|
}
|
||||||
InitializeClient();
|
InitializeClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,14 @@ namespace MinecraftClient
|
||||||
internal readonly record struct RestartRequest(
|
internal readonly record struct RestartRequest(
|
||||||
long ConnectionAttempt,
|
long ConnectionAttempt,
|
||||||
TimeSpan Delay,
|
TimeSpan Delay,
|
||||||
bool KeepAccountAndServerSettings);
|
bool KeepAccountAndServerSettings,
|
||||||
|
RestartSettingsSnapshot? SettingsSnapshot = null,
|
||||||
|
long RequestId = 0);
|
||||||
|
|
||||||
|
internal readonly record struct RestartSettingsSnapshot(
|
||||||
|
Settings.MainConfigHelper.MainConfig.AccountInfoConfig Account,
|
||||||
|
string ServerIP,
|
||||||
|
ushort ServerPort);
|
||||||
|
|
||||||
internal sealed class RestartCoordinator : IDisposable
|
internal sealed class RestartCoordinator : IDisposable
|
||||||
{
|
{
|
||||||
|
|
@ -19,8 +26,9 @@ namespace MinecraftClient
|
||||||
private readonly Func<RestartRequest, CancellationToken, Task> restart;
|
private readonly Func<RestartRequest, CancellationToken, Task> restart;
|
||||||
private readonly Action<Exception> reportFailure;
|
private readonly Action<Exception> reportFailure;
|
||||||
private readonly Task worker;
|
private readonly Task worker;
|
||||||
private readonly HashSet<long> pendingAttempts = [];
|
private readonly Dictionary<long, long> pendingAttempts = [];
|
||||||
private long highestScheduledAttempt = -1;
|
private long highestScheduledAttempt = -1;
|
||||||
|
private long nextRequestId;
|
||||||
private bool stopped;
|
private bool stopped;
|
||||||
|
|
||||||
internal RestartCoordinator(
|
internal RestartCoordinator(
|
||||||
|
|
@ -44,22 +52,28 @@ namespace MinecraftClient
|
||||||
internal bool HasScheduledRestart(long connectionAttempt)
|
internal bool HasScheduledRestart(long connectionAttempt)
|
||||||
{
|
{
|
||||||
lock (stateLock)
|
lock (stateLock)
|
||||||
return !stopped && pendingAttempts.Contains(connectionAttempt);
|
return !stopped && pendingAttempts.ContainsKey(connectionAttempt);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool TrySchedule(RestartRequest request)
|
internal bool TrySchedule(RestartRequest request)
|
||||||
{
|
{
|
||||||
lock (stateLock)
|
lock (stateLock)
|
||||||
{
|
{
|
||||||
if (stopped || request.ConnectionAttempt <= highestScheduledAttempt)
|
bool hasPendingRequest = pendingAttempts.TryGetValue(request.ConnectionAttempt, out long previousRequestId);
|
||||||
|
if (stopped || request.ConnectionAttempt < highestScheduledAttempt
|
||||||
|
|| (request.ConnectionAttempt == highestScheduledAttempt && !hasPendingRequest))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
highestScheduledAttempt = request.ConnectionAttempt;
|
highestScheduledAttempt = Math.Max(highestScheduledAttempt, request.ConnectionAttempt);
|
||||||
pendingAttempts.Add(request.ConnectionAttempt);
|
request = request with { RequestId = ++nextRequestId };
|
||||||
|
pendingAttempts[request.ConnectionAttempt] = request.RequestId;
|
||||||
if (requests.Writer.TryWrite(request))
|
if (requests.Writer.TryWrite(request))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
pendingAttempts.Remove(request.ConnectionAttempt);
|
if (hasPendingRequest)
|
||||||
|
pendingAttempts[request.ConnectionAttempt] = previousRequestId;
|
||||||
|
else
|
||||||
|
pendingAttempts.Remove(request.ConnectionAttempt);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,6 +98,13 @@ namespace MinecraftClient
|
||||||
{
|
{
|
||||||
await foreach (RestartRequest request in requests.Reader.ReadAllAsync(shutdown.Token).ConfigureAwait(false))
|
await foreach (RestartRequest request in requests.Reader.ReadAllAsync(shutdown.Token).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
if (!pendingAttempts.TryGetValue(request.ConnectionAttempt, out long requestId)
|
||||||
|
|| requestId != request.RequestId)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await restart(request, shutdown.Token).ConfigureAwait(false);
|
await restart(request, shutdown.Token).ConfigureAwait(false);
|
||||||
|
|
@ -99,7 +120,11 @@ namespace MinecraftClient
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
lock (stateLock)
|
lock (stateLock)
|
||||||
pendingAttempts.Remove(request.ConnectionAttempt);
|
{
|
||||||
|
if (pendingAttempts.TryGetValue(request.ConnectionAttempt, out long requestId)
|
||||||
|
&& requestId == request.RequestId)
|
||||||
|
pendingAttempts.Remove(request.ConnectionAttempt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue