Compare commits

...

3 Commits

8 changed files with 155 additions and 54 deletions

View File

@ -18,4 +18,5 @@ static class MinecraftServerProperties {
public static readonly MinecraftServerProperty<ushort> ServerPort = new UnsignedShort("server-port"); public static readonly MinecraftServerProperty<ushort> ServerPort = new UnsignedShort("server-port");
public static readonly MinecraftServerProperty<ushort> RconPort = new UnsignedShort("rcon.port"); public static readonly MinecraftServerProperty<ushort> RconPort = new UnsignedShort("rcon.port");
public static readonly MinecraftServerProperty<bool> EnableRcon = new Boolean("enable-rcon"); public static readonly MinecraftServerProperty<bool> EnableRcon = new Boolean("enable-rcon");
public static readonly MinecraftServerProperty<bool> SyncChunkWrites = new Boolean("sync-chunk-writes");
} }

View File

@ -5,11 +5,13 @@ namespace Phantom.Agent.Minecraft.Properties;
public sealed record ServerProperties( public sealed record ServerProperties(
ushort ServerPort, ushort ServerPort,
ushort RconPort, ushort RconPort,
bool EnableRcon = true bool EnableRcon = true,
bool SyncChunkWrites = false
) { ) {
internal void SetTo(JavaPropertiesFileEditor properties) { internal void SetTo(JavaPropertiesFileEditor properties) {
MinecraftServerProperties.ServerPort.Set(properties, ServerPort); MinecraftServerProperties.ServerPort.Set(properties, ServerPort);
MinecraftServerProperties.RconPort.Set(properties, RconPort); MinecraftServerProperties.RconPort.Set(properties, RconPort);
MinecraftServerProperties.EnableRcon.Set(properties, EnableRcon); MinecraftServerProperties.EnableRcon.Set(properties, EnableRcon);
MinecraftServerProperties.SyncChunkWrites.Set(properties, SyncChunkWrites);
} }
} }

View File

@ -19,7 +19,7 @@ public sealed class ServerStatusProtocol {
try { try {
return await GetOnlinePlayerCountOrThrow(serverPort, cancellationToken); return await GetOnlinePlayerCountOrThrow(serverPort, cancellationToken);
} catch (Exception e) { } catch (Exception e) {
logger.Error(e, "Caught exception while checking if players are online."); logger.Error(e, "Caught exception checking online player count.");
return null; return null;
} }
} }

View File

@ -1,10 +1,8 @@
using Phantom.Agent.Minecraft.Instance; using Phantom.Agent.Services.Instances;
using Phantom.Agent.Minecraft.Server; using Phantom.Agent.Services.Instances.State;
using Phantom.Agent.Services.Instances;
using Phantom.Common.Data.Backups; using Phantom.Common.Data.Backups;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
using Phantom.Utils.Threading;
namespace Phantom.Agent.Services.Backups; namespace Phantom.Agent.Services.Backups;
@ -16,27 +14,23 @@ sealed class BackupScheduler : CancellableBackgroundTask {
private readonly BackupManager backupManager; private readonly BackupManager backupManager;
private readonly InstanceContext context; private readonly InstanceContext context;
private readonly InstanceProcess process;
private readonly SemaphoreSlim backupSemaphore = new (1, 1); private readonly SemaphoreSlim backupSemaphore = new (1, 1);
private readonly int serverPort;
private readonly ServerStatusProtocol serverStatusProtocol;
private readonly ManualResetEventSlim serverOutputWhileWaitingForOnlinePlayers = new (); private readonly ManualResetEventSlim serverOutputWhileWaitingForOnlinePlayers = new ();
private readonly InstancePlayerCountTracker playerCountTracker;
public event EventHandler<BackupCreationResult>? BackupCompleted; public event EventHandler<BackupCreationResult>? BackupCompleted;
public BackupScheduler(InstanceContext context, InstanceProcess process, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName)) { public BackupScheduler(InstanceContext context, InstancePlayerCountTracker playerCountTracker) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName)) {
this.backupManager = context.Services.BackupManager; this.backupManager = context.Services.BackupManager;
this.context = context; this.context = context;
this.process = process; this.playerCountTracker = playerCountTracker;
this.serverPort = serverPort;
this.serverStatusProtocol = new ServerStatusProtocol(context.ShortName);
Start(); Start();
} }
protected override async Task RunTask() { protected override async Task RunTask() {
await Task.Delay(InitialDelay, CancellationToken); await Task.Delay(InitialDelay, CancellationToken);
Logger.Information("Starting a new backup after server launched."); Logger.Information("Starting a new backup after server launched.");
while (!CancellationToken.IsCancellationRequested) { while (!CancellationToken.IsCancellationRequested) {
var result = await CreateBackup(); var result = await CreateBackup();
BackupCompleted?.Invoke(this, result); BackupCompleted?.Invoke(this, result);
@ -69,43 +63,18 @@ sealed class BackupScheduler : CancellableBackgroundTask {
} }
private async Task WaitForOnlinePlayers() { private async Task WaitForOnlinePlayers() {
bool needsToLogOfflinePlayersMessage = true; var task = playerCountTracker.WaitForOnlinePlayers(CancellationToken);
if (!task.IsCompleted) {
process.AddOutputListener(ServerOutputListener, maxLinesToReadFromHistory: 0); Logger.Information("Waiting for someone to join before starting a new backup.");
try {
while (!CancellationToken.IsCancellationRequested) {
serverOutputWhileWaitingForOnlinePlayers.Reset();
var onlinePlayerCount = await serverStatusProtocol.GetOnlinePlayerCount(serverPort, CancellationToken);
if (onlinePlayerCount == null) {
Logger.Warning("Could not detect whether any players are online, starting a new backup.");
break;
}
if (onlinePlayerCount > 0) {
Logger.Information("Players are online, starting a new backup.");
break;
}
if (needsToLogOfflinePlayersMessage) {
needsToLogOfflinePlayersMessage = false;
Logger.Information("No players are online, waiting for someone to join before starting a new backup.");
}
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
Logger.Debug("Waiting for server output before checking for online players again...");
await serverOutputWhileWaitingForOnlinePlayers.WaitHandle.WaitOneAsync(CancellationToken);
}
} finally {
process.RemoveOutputListener(ServerOutputListener);
} }
}
try {
private void ServerOutputListener(object? sender, string line) { await task;
if (!serverOutputWhileWaitingForOnlinePlayers.IsSet) { Logger.Information("Players are online, starting a new backup.");
serverOutputWhileWaitingForOnlinePlayers.Set(); } catch (OperationCanceledException) {
Logger.Debug("Detected server output, signalling to check for online players again."); throw;
} catch (Exception) {
Logger.Warning("Could not detect whether any players are online, starting a new backup.");
} }
} }

View File

@ -55,7 +55,6 @@ sealed partial class BackupServerCommandDispatcher : IDisposable {
} }
public async Task SaveAllChunks() { public async Task SaveAllChunks() {
// TODO Try if not flushing and waiting a few seconds before flushing reduces lag.
await process.SendCommand(MinecraftCommand.SaveAll(flush: true), cancellationToken); await process.SendCommand(MinecraftCommand.SaveAll(flush: true), cancellationToken);
await savedTheGame.Task.WaitAsync(TimeSpan.FromMinutes(1), cancellationToken); await savedTheGame.Task.WaitAsync(TimeSpan.FromMinutes(1), cancellationToken);
} }

View File

@ -0,0 +1,121 @@
using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Server;
using Phantom.Utils.Logging;
using Phantom.Utils.Tasks;
using Phantom.Utils.Threading;
namespace Phantom.Agent.Services.Instances.State;
sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
private readonly InstanceProcess process;
private readonly ushort serverPort;
private readonly ServerStatusProtocol serverStatusProtocol;
private readonly TaskCompletionSource firstDetection = AsyncTasks.CreateCompletionSource();
private readonly ManualResetEventSlim serverOutputEvent = new ();
private int? onlinePlayerCount;
public int? OnlinePlayerCount {
get {
lock (this) {
return onlinePlayerCount;
}
}
private set {
EventHandler<int?>? onlinePlayerCountChanged;
lock (this) {
if (onlinePlayerCount == value) {
return;
}
onlinePlayerCount = value;
onlinePlayerCountChanged = OnlinePlayerCountChanged;
}
onlinePlayerCountChanged?.Invoke(this, value);
}
}
private event EventHandler<int?>? OnlinePlayerCountChanged;
private bool isDisposed = false;
public InstancePlayerCountTracker(InstanceContext context, InstanceProcess process, ushort serverPort) : base(PhantomLogger.Create<InstancePlayerCountTracker>(context.ShortName)) {
this.process = process;
this.serverPort = serverPort;
this.serverStatusProtocol = new ServerStatusProtocol(context.ShortName);
Start();
}
protected override async Task RunTask() {
// Give the server time to start accepting connections.
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
serverOutputEvent.Set();
process.AddOutputListener(OnOutput, maxLinesToReadFromHistory: 0);
while (!CancellationToken.IsCancellationRequested) {
serverOutputEvent.Reset();
OnlinePlayerCount = await serverStatusProtocol.GetOnlinePlayerCount(serverPort, CancellationToken);
if (!firstDetection.Task.IsCompleted) {
firstDetection.SetResult();
}
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
await serverOutputEvent.WaitHandle.WaitOneAsync(CancellationToken);
await Task.Delay(TimeSpan.FromSeconds(2), CancellationToken);
}
}
public async Task WaitForOnlinePlayers(CancellationToken cancellationToken) {
await firstDetection.Task.WaitAsync(cancellationToken);
var onlinePlayersDetected = AsyncTasks.CreateCompletionSource();
lock (this) {
if (onlinePlayerCount == null) {
throw new InvalidOperationException();
}
else if (onlinePlayerCount > 0) {
return;
}
OnlinePlayerCountChanged += OnOnlinePlayerCountChanged;
void OnOnlinePlayerCountChanged(object? sender, int? newPlayerCount) {
if (newPlayerCount == null) {
onlinePlayersDetected.TrySetException(new InvalidOperationException());
OnlinePlayerCountChanged -= OnOnlinePlayerCountChanged;
}
else if (newPlayerCount > 0) {
onlinePlayersDetected.TrySetResult();
OnlinePlayerCountChanged -= OnOnlinePlayerCountChanged;
}
}
}
await onlinePlayersDetected.Task;
}
private void OnOutput(object? sender, string? line) {
lock (this) {
if (!isDisposed) {
serverOutputEvent.Set();
}
}
}
protected override void Dispose() {
lock (this) {
isDisposed = true;
onlinePlayerCount = null;
}
process.RemoveOutputListener(OnOutput);
serverOutputEvent.Dispose();
}
}

View File

@ -19,6 +19,7 @@ sealed class InstanceRunningState : IDisposable {
private readonly CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
private readonly InstanceLogSender logSender; private readonly InstanceLogSender logSender;
private readonly InstancePlayerCountTracker playerCountTracker;
private readonly BackupScheduler backupScheduler; private readonly BackupScheduler backupScheduler;
private bool isDisposed; private bool isDisposed;
@ -32,8 +33,9 @@ sealed class InstanceRunningState : IDisposable {
this.cancellationToken = cancellationToken; this.cancellationToken = cancellationToken;
this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.InstanceGuid, context.ShortName); this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.InstanceGuid, context.ShortName);
this.playerCountTracker = new InstancePlayerCountTracker(context, process, configuration.ServerPort);
this.backupScheduler = new BackupScheduler(context, process, configuration.ServerPort); this.backupScheduler = new BackupScheduler(context, playerCountTracker);
this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted; this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
} }
@ -93,6 +95,11 @@ sealed class InstanceRunningState : IDisposable {
} }
} }
public void OnStopInitiated() {
backupScheduler.Stop();
playerCountTracker.Stop();
}
private bool TryDispose() { private bool TryDispose() {
lock (this) { lock (this) {
if (isDisposed) { if (isDisposed) {
@ -102,8 +109,8 @@ sealed class InstanceRunningState : IDisposable {
isDisposed = true; isDisposed = true;
} }
OnStopInitiated();
logSender.Stop(); logSender.Stop();
backupScheduler.Stop();
Process.Dispose(); Process.Dispose();

View File

@ -25,6 +25,8 @@ static class InstanceStopProcedure {
try { try {
// Too late to cancel the stop procedure now. // Too late to cancel the stop procedure now.
runningState.OnStopInitiated();
if (!process.HasEnded) { if (!process.HasEnded) {
context.Logger.Information("Session stopping now."); context.Logger.Information("Session stopping now.");
await DoStop(context, process); await DoStop(context, process);
@ -85,7 +87,7 @@ static class InstanceStopProcedure {
private static async Task WaitForSessionToEnd(InstanceContext context, InstanceProcess process) { private static async Task WaitForSessionToEnd(InstanceContext context, InstanceProcess process) {
try { try {
await process.WaitForExit(TimeSpan.FromSeconds(55)); await process.WaitForExit(TimeSpan.FromSeconds(55));
} catch (OperationCanceledException) { } catch (TimeoutException) {
try { try {
context.Logger.Warning("Waiting timed out, killing session..."); context.Logger.Warning("Waiting timed out, killing session...");
process.Kill(); process.Kill();