mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2024-11-26 01:42:53 +01:00
Compare commits
4 Commits
62a683f8ef
...
b3104f9ac3
Author | SHA1 | Date | |
---|---|---|---|
b3104f9ac3 | |||
c7354dce0e | |||
b5129e2f70 | |||
2f49d72014 |
@ -1,10 +1,12 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Phantom.Utils.Collections;
|
using Phantom.Utils.Collections;
|
||||||
|
using Phantom.Utils.Runtime;
|
||||||
|
|
||||||
namespace Phantom.Agent.Minecraft.Instance;
|
namespace Phantom.Agent.Minecraft.Instance;
|
||||||
|
|
||||||
public sealed class InstanceSession : IDisposable {
|
public sealed class InstanceSession : IDisposable {
|
||||||
public InstanceProperties InstanceProperties { get; }
|
public InstanceProperties InstanceProperties { get; }
|
||||||
|
public CancellableSemaphore BackupSemaphore { get; } = new (1, 1);
|
||||||
|
|
||||||
private readonly RingBuffer<string> outputBuffer = new (10000);
|
private readonly RingBuffer<string> outputBuffer = new (10000);
|
||||||
private event EventHandler<string>? OutputEvent;
|
private event EventHandler<string>? OutputEvent;
|
||||||
@ -64,6 +66,7 @@ public sealed class InstanceSession : IDisposable {
|
|||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
process.Dispose();
|
process.Dispose();
|
||||||
|
BackupSemaphore.Dispose();
|
||||||
OutputEvent = null;
|
OutputEvent = null;
|
||||||
SessionEnded = null;
|
SessionEnded = null;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using Phantom.Agent.Minecraft.Java;
|
using Phantom.Agent.Minecraft.Java;
|
||||||
using Phantom.Agent.Minecraft.Server;
|
using Phantom.Agent.Minecraft.Server;
|
||||||
using Phantom.Utils.Runtime;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Minecraft.Launcher;
|
namespace Phantom.Agent.Minecraft.Launcher;
|
||||||
|
|
||||||
public sealed record LaunchServices(TaskManager TaskManager, MinecraftServerExecutables ServerExecutables, JavaRuntimeRepository JavaRuntimeRepository);
|
public sealed record LaunchServices(MinecraftServerExecutables ServerExecutables, JavaRuntimeRepository JavaRuntimeRepository);
|
||||||
|
93
Agent/Phantom.Agent.Minecraft/Server/ServerStatusProtocol.cs
Normal file
93
Agent/Phantom.Agent.Minecraft/Server/ServerStatusProtocol.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using Phantom.Common.Logging;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Phantom.Agent.Minecraft.Server;
|
||||||
|
|
||||||
|
public sealed class ServerStatusProtocol {
|
||||||
|
private readonly ILogger logger;
|
||||||
|
|
||||||
|
public ServerStatusProtocol(string loggerName) {
|
||||||
|
this.logger = PhantomLogger.Create<ServerStatusProtocol>(loggerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int?> GetOnlinePlayerCount(int serverPort, CancellationToken cancellationToken) {
|
||||||
|
try {
|
||||||
|
return await GetOnlinePlayerCountOrThrow(serverPort, cancellationToken);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.Error(e, "Caught exception while checking if players are online.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int?> GetOnlinePlayerCountOrThrow(int serverPort, CancellationToken cancellationToken) {
|
||||||
|
using var tcpClient = new TcpClient();
|
||||||
|
await tcpClient.ConnectAsync(IPAddress.Loopback, serverPort, cancellationToken);
|
||||||
|
var tcpStream = tcpClient.GetStream();
|
||||||
|
|
||||||
|
// https://wiki.vg/Server_List_Ping
|
||||||
|
tcpStream.WriteByte(0xFE);
|
||||||
|
await tcpStream.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
|
short? messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
|
||||||
|
return messageLength == null ? null : await ReadOnlinePlayerCount(tcpStream, messageLength.Value * 2, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<short?> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
|
||||||
|
var headerBuffer = ArrayPool<byte>.Shared.Rent(3);
|
||||||
|
try {
|
||||||
|
await tcpStream.ReadExactlyAsync(headerBuffer, 0, 3, cancellationToken);
|
||||||
|
|
||||||
|
if (headerBuffer[0] != 0xFF) {
|
||||||
|
logger.Error("Unexpected first byte in response from server: {FirstByte}.", headerBuffer[0]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
short messageLength = BinaryPrimitives.ReadInt16BigEndian(headerBuffer.AsSpan(1));
|
||||||
|
if (messageLength <= 0) {
|
||||||
|
logger.Error("Unexpected message length in response from server: {MessageLength}.", messageLength);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageLength;
|
||||||
|
} finally {
|
||||||
|
ArrayPool<byte>.Shared.Return(headerBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int?> ReadOnlinePlayerCount(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
|
||||||
|
var messageBuffer = ArrayPool<byte>.Shared.Rent(messageLength);
|
||||||
|
try {
|
||||||
|
await tcpStream.ReadExactlyAsync(messageBuffer, 0, messageLength, cancellationToken);
|
||||||
|
|
||||||
|
// Valid response separator encoded in UTF-16BE is 0x00 0xA7 (§).
|
||||||
|
const byte SeparatorSecondByte = 0xA7;
|
||||||
|
|
||||||
|
static bool IsValidSeparator(ReadOnlySpan<byte> buffer, int index) {
|
||||||
|
return index > 0 && buffer[index - 1] == 0x00;
|
||||||
|
}
|
||||||
|
|
||||||
|
int separator2 = Array.LastIndexOf(messageBuffer, SeparatorSecondByte);
|
||||||
|
int separator1 = separator2 == -1 ? -1 : Array.LastIndexOf(messageBuffer, SeparatorSecondByte, separator2 - 1);
|
||||||
|
if (!IsValidSeparator(messageBuffer, separator1) || !IsValidSeparator(messageBuffer, separator2)) {
|
||||||
|
logger.Error("Could not find message separators in response from server.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string onlinePlayerCountStr = Encoding.BigEndianUnicode.GetString(messageBuffer[(separator1 + 1)..(separator2 - 1)]);
|
||||||
|
if (!int.TryParse(onlinePlayerCountStr, out int onlinePlayerCount)) {
|
||||||
|
logger.Error("Could not parse online player count in response from server: {OnlinePlayerCount}.", onlinePlayerCountStr);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Verbose("Detected {OnlinePlayerCount} online player(s).", onlinePlayerCount);
|
||||||
|
return onlinePlayerCount;
|
||||||
|
} finally {
|
||||||
|
ArrayPool<byte>.Shared.Return(messageBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@ public sealed class AgentServices {
|
|||||||
this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>());
|
this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>());
|
||||||
this.BackupManager = new BackupManager(agentFolders);
|
this.BackupManager = new BackupManager(agentFolders);
|
||||||
this.JavaRuntimeRepository = new JavaRuntimeRepository();
|
this.JavaRuntimeRepository = new JavaRuntimeRepository();
|
||||||
this.InstanceSessionManager = new InstanceSessionManager(agentInfo, agentFolders, JavaRuntimeRepository, TaskManager);
|
this.InstanceSessionManager = new InstanceSessionManager(agentInfo, agentFolders, JavaRuntimeRepository, TaskManager, BackupManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize() {
|
public async Task Initialize() {
|
||||||
|
@ -10,11 +10,15 @@ using Serilog;
|
|||||||
namespace Phantom.Agent.Services.Backups;
|
namespace Phantom.Agent.Services.Backups;
|
||||||
|
|
||||||
sealed class BackupArchiver {
|
sealed class BackupArchiver {
|
||||||
|
private readonly string destinationBasePath;
|
||||||
|
private readonly string temporaryBasePath;
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
private readonly InstanceProperties instanceProperties;
|
private readonly InstanceProperties instanceProperties;
|
||||||
private readonly CancellationToken cancellationToken;
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
public BackupArchiver(string loggerName, InstanceProperties instanceProperties, CancellationToken cancellationToken) {
|
public BackupArchiver(string destinationBasePath, string temporaryBasePath, string loggerName, InstanceProperties instanceProperties, CancellationToken cancellationToken) {
|
||||||
|
this.destinationBasePath = destinationBasePath;
|
||||||
|
this.temporaryBasePath = temporaryBasePath;
|
||||||
this.logger = PhantomLogger.Create<BackupArchiver>(loggerName);
|
this.logger = PhantomLogger.Create<BackupArchiver>(loggerName);
|
||||||
this.instanceProperties = instanceProperties;
|
this.instanceProperties = instanceProperties;
|
||||||
this.cancellationToken = cancellationToken;
|
this.cancellationToken = cancellationToken;
|
||||||
@ -40,12 +44,14 @@ sealed class BackupArchiver {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ArchiveWorld(string destinationPath, BackupCreationResult.Builder resultBuilder) {
|
public async Task ArchiveWorld(BackupCreationResult.Builder resultBuilder) {
|
||||||
string backupFolderPath = Path.Combine(destinationPath, instanceProperties.InstanceGuid.ToString());
|
string guid = instanceProperties.InstanceGuid.ToString();
|
||||||
string backupFilePath = Path.Combine(backupFolderPath, DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".tar");
|
string currentDateTime = DateTime.Now.ToString("yyyyMMdd-HHmmss");
|
||||||
|
string backupFolderPath = Path.Combine(destinationBasePath, guid);
|
||||||
|
string backupFilePath = Path.Combine(backupFolderPath, currentDateTime + ".tar");
|
||||||
|
|
||||||
if (File.Exists(backupFilePath)) {
|
if (File.Exists(backupFilePath)) {
|
||||||
resultBuilder.Kind = BackupCreationResultKind.BackupAlreadyExists;
|
resultBuilder.Kind = BackupCreationResultKind.BackupFileAlreadyExists;
|
||||||
logger.Warning("Skipping backup, file already exists: {File}", backupFilePath);
|
logger.Warning("Skipping backup, file already exists: {File}", backupFilePath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -58,7 +64,8 @@ sealed class BackupArchiver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await CopyWorldAndCreateTarArchive(backupFolderPath, backupFilePath, resultBuilder)) {
|
string temporaryFolderPath = Path.Combine(temporaryBasePath, guid + "_" + currentDateTime);
|
||||||
|
if (!await CopyWorldAndCreateTarArchive(temporaryFolderPath, backupFilePath, resultBuilder)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,9 +75,7 @@ sealed class BackupArchiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> CopyWorldAndCreateTarArchive(string backupFolderPath, string backupFilePath, BackupCreationResult.Builder resultBuilder) {
|
private async Task<bool> CopyWorldAndCreateTarArchive(string temporaryFolderPath, string backupFilePath, BackupCreationResult.Builder resultBuilder) {
|
||||||
string temporaryFolderPath = Path.Combine(backupFolderPath, "temp");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!await CopyWorldToTemporaryFolder(temporaryFolderPath)) {
|
if (!await CopyWorldToTemporaryFolder(temporaryFolderPath)) {
|
||||||
resultBuilder.Kind = BackupCreationResultKind.CouldNotCopyWorldToTemporaryFolder;
|
resultBuilder.Kind = BackupCreationResultKind.CouldNotCopyWorldToTemporaryFolder;
|
||||||
@ -109,10 +114,21 @@ sealed class BackupArchiver {
|
|||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.Error(e, "Could not create archive.");
|
logger.Error(e, "Could not create archive.");
|
||||||
|
DeleteBrokenArchiveFile(backupFilePath);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DeleteBrokenArchiveFile(string filePath) {
|
||||||
|
if (File.Exists(filePath)) {
|
||||||
|
try {
|
||||||
|
File.Delete(filePath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.Error(e, "Could not delete broken archive: {File}", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CopyDirectory(DirectoryInfo sourceFolder, string destinationFolderPath, ImmutableList<string> relativePath) {
|
private async Task CopyDirectory(DirectoryInfo sourceFolder, string destinationFolderPath, ImmutableList<string> relativePath) {
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
@ -8,26 +8,44 @@ using Serilog;
|
|||||||
namespace Phantom.Agent.Services.Backups;
|
namespace Phantom.Agent.Services.Backups;
|
||||||
|
|
||||||
sealed partial class BackupManager {
|
sealed partial class BackupManager {
|
||||||
private readonly string basePath;
|
private readonly string destinationBasePath;
|
||||||
|
private readonly string temporaryBasePath;
|
||||||
|
|
||||||
public BackupManager(AgentFolders agentFolders) {
|
public BackupManager(AgentFolders agentFolders) {
|
||||||
this.basePath = agentFolders.BackupsFolderPath;
|
this.destinationBasePath = agentFolders.BackupsFolderPath;
|
||||||
|
this.temporaryBasePath = Path.Combine(agentFolders.TemporaryFolderPath, "backups");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<BackupCreationResult> CreateBackup(string loggerName, InstanceSession session, CancellationToken cancellationToken) {
|
public async Task<BackupCreationResult> CreateBackup(string loggerName, InstanceSession session, CancellationToken cancellationToken) {
|
||||||
return await new BackupCreator(basePath, loggerName, session, cancellationToken).CreateBackup();
|
try {
|
||||||
|
if (!await session.BackupSemaphore.Wait(TimeSpan.FromSeconds(1), cancellationToken)) {
|
||||||
|
return new BackupCreationResult(BackupCreationResultKind.BackupAlreadyRunning);
|
||||||
|
}
|
||||||
|
} catch (ObjectDisposedException) {
|
||||||
|
return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning);
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await new BackupCreator(destinationBasePath, temporaryBasePath, loggerName, session, cancellationToken).CreateBackup();
|
||||||
|
} finally {
|
||||||
|
session.BackupSemaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class BackupCreator {
|
private sealed class BackupCreator {
|
||||||
private readonly string basePath;
|
private readonly string destinationBasePath;
|
||||||
|
private readonly string temporaryBasePath;
|
||||||
private readonly string loggerName;
|
private readonly string loggerName;
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
private readonly InstanceSession session;
|
private readonly InstanceSession session;
|
||||||
private readonly BackupCommandListener listener;
|
private readonly BackupCommandListener listener;
|
||||||
private readonly CancellationToken cancellationToken;
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
public BackupCreator(string basePath, string loggerName, InstanceSession session, CancellationToken cancellationToken) {
|
public BackupCreator(string destinationBasePath, string temporaryBasePath, string loggerName, InstanceSession session, CancellationToken cancellationToken) {
|
||||||
this.basePath = basePath;
|
this.destinationBasePath = destinationBasePath;
|
||||||
|
this.temporaryBasePath = temporaryBasePath;
|
||||||
this.loggerName = loggerName;
|
this.loggerName = loggerName;
|
||||||
this.logger = PhantomLogger.Create<BackupManager>(loggerName);
|
this.logger = PhantomLogger.Create<BackupManager>(loggerName);
|
||||||
this.session = session;
|
this.session = session;
|
||||||
@ -36,21 +54,38 @@ sealed partial class BackupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<BackupCreationResult> CreateBackup() {
|
public async Task<BackupCreationResult> CreateBackup() {
|
||||||
|
logger.Information("Backup started.");
|
||||||
session.AddOutputListener(listener.OnOutput, 0);
|
session.AddOutputListener(listener.OnOutput, 0);
|
||||||
try {
|
try {
|
||||||
var resultBuilder = new BackupCreationResult.Builder();
|
var resultBuilder = new BackupCreationResult.Builder();
|
||||||
await RunBackupProcess(resultBuilder);
|
|
||||||
return resultBuilder.Build();
|
await RunBackupProcedure(resultBuilder);
|
||||||
|
|
||||||
|
var result = resultBuilder.Build();
|
||||||
|
if (result.Kind == BackupCreationResultKind.Success) {
|
||||||
|
var warningCount = result.Warnings.Count();
|
||||||
|
if (warningCount == 0) {
|
||||||
|
logger.Information("Backup finished successfully.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.Warning("Backup finished with {Warnings} warning(s).", warningCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.Warning("Backup failed: {Reason}", result.Kind.ToSentence());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
session.RemoveOutputListener(listener.OnOutput);
|
session.RemoveOutputListener(listener.OnOutput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunBackupProcess(BackupCreationResult.Builder resultBuilder) {
|
private async Task RunBackupProcedure(BackupCreationResult.Builder resultBuilder) {
|
||||||
try {
|
try {
|
||||||
await DisableAutomaticSaving();
|
await DisableAutomaticSaving();
|
||||||
await SaveAllChunks();
|
await SaveAllChunks();
|
||||||
await new BackupArchiver(loggerName, session.InstanceProperties, cancellationToken).ArchiveWorld(basePath, resultBuilder);
|
await new BackupArchiver(destinationBasePath, temporaryBasePath, loggerName, session.InstanceProperties, cancellationToken).ArchiveWorld(resultBuilder);
|
||||||
} catch (OperationCanceledException) {
|
} catch (OperationCanceledException) {
|
||||||
resultBuilder.Kind = BackupCreationResultKind.BackupCancelled;
|
resultBuilder.Kind = BackupCreationResultKind.BackupCancelled;
|
||||||
logger.Warning("Backup creation was cancelled.");
|
logger.Warning("Backup creation was cancelled.");
|
||||||
|
75
Agent/Phantom.Agent.Services/Backups/BackupScheduler.cs
Normal file
75
Agent/Phantom.Agent.Services/Backups/BackupScheduler.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
using Phantom.Agent.Minecraft.Instance;
|
||||||
|
using Phantom.Agent.Minecraft.Server;
|
||||||
|
using Phantom.Common.Data.Backups;
|
||||||
|
using Phantom.Common.Logging;
|
||||||
|
using Phantom.Utils.Runtime;
|
||||||
|
|
||||||
|
namespace Phantom.Agent.Services.Backups;
|
||||||
|
|
||||||
|
sealed class BackupScheduler : CancellableBackgroundTask {
|
||||||
|
// TODO make configurable
|
||||||
|
private static readonly TimeSpan InitialDelay = TimeSpan.FromMinutes(2);
|
||||||
|
private static readonly TimeSpan BackupInterval = TimeSpan.FromMinutes(30);
|
||||||
|
private static readonly TimeSpan BackupFailureRetryDelay = TimeSpan.FromMinutes(5);
|
||||||
|
private static readonly TimeSpan OnlinePlayersCheckInterval = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
private readonly string loggerName;
|
||||||
|
private readonly BackupManager backupManager;
|
||||||
|
private readonly InstanceSession session;
|
||||||
|
private readonly int serverPort;
|
||||||
|
private readonly ServerStatusProtocol serverStatusProtocol;
|
||||||
|
|
||||||
|
public BackupScheduler(TaskManager taskManager, BackupManager backupManager, InstanceSession session, int serverPort, string loggerName) : base(PhantomLogger.Create<BackupScheduler>(loggerName), taskManager, "Backup scheduler for " + loggerName) {
|
||||||
|
this.loggerName = loggerName;
|
||||||
|
this.backupManager = backupManager;
|
||||||
|
this.session = session;
|
||||||
|
this.serverPort = serverPort;
|
||||||
|
this.serverStatusProtocol = new ServerStatusProtocol(loggerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task RunTask() {
|
||||||
|
await Task.Delay(InitialDelay, CancellationToken);
|
||||||
|
Logger.Information("Starting a new backup after server launched.");
|
||||||
|
|
||||||
|
while (!CancellationToken.IsCancellationRequested) {
|
||||||
|
var result = await CreateBackup();
|
||||||
|
if (result.Kind.ShouldRetry()) {
|
||||||
|
Logger.Warning("Scheduled backup failed, retrying in {Minutes} minutes.", BackupFailureRetryDelay.TotalMinutes);
|
||||||
|
await Task.Delay(BackupFailureRetryDelay, CancellationToken);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Logger.Warning("Scheduling next backup in {Minutes} minutes.", BackupInterval.TotalMinutes);
|
||||||
|
await Task.Delay(BackupInterval, CancellationToken);
|
||||||
|
await WaitForOnlinePlayers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<BackupCreationResult> CreateBackup() {
|
||||||
|
return await backupManager.CreateBackup(loggerName, session, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WaitForOnlinePlayers() {
|
||||||
|
bool needsToLogOfflinePlayersMessage = true;
|
||||||
|
|
||||||
|
while (!CancellationToken.IsCancellationRequested) {
|
||||||
|
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(OnlinePlayersCheckInterval, CancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,21 +18,19 @@ sealed class Instance : IDisposable {
|
|||||||
return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref loggerSequenceId);
|
return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref loggerSequenceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Instance> Create(InstanceConfiguration configuration, BaseLauncher launcher, LaunchServices launchServices, PortManager portManager) {
|
public static async Task<Instance> Create(InstanceConfiguration configuration, InstanceServices services, BaseLauncher launcher) {
|
||||||
var instance = new Instance(configuration, launcher, launchServices, portManager);
|
var instance = new Instance(configuration, services, launcher);
|
||||||
await instance.ReportLastStatus();
|
await instance.ReportLastStatus();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InstanceConfiguration Configuration { get; private set; }
|
public InstanceConfiguration Configuration { get; private set; }
|
||||||
|
private InstanceServices Services { get; }
|
||||||
private BaseLauncher Launcher { get; set; }
|
private BaseLauncher Launcher { get; set; }
|
||||||
|
|
||||||
private readonly string shortName;
|
private readonly string shortName;
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
|
|
||||||
private readonly LaunchServices launchServices;
|
|
||||||
private readonly PortManager portManager;
|
|
||||||
|
|
||||||
private IInstanceStatus currentStatus;
|
private IInstanceStatus currentStatus;
|
||||||
private IInstanceState currentState;
|
private IInstanceState currentState;
|
||||||
private readonly SemaphoreSlim stateTransitioningActionSemaphore = new (1, 1);
|
private readonly SemaphoreSlim stateTransitioningActionSemaphore = new (1, 1);
|
||||||
@ -41,15 +39,14 @@ sealed class Instance : IDisposable {
|
|||||||
|
|
||||||
public event EventHandler? IsRunningChanged;
|
public event EventHandler? IsRunningChanged;
|
||||||
|
|
||||||
private Instance(InstanceConfiguration configuration, BaseLauncher launcher, LaunchServices launchServices, PortManager portManager) {
|
private Instance(InstanceConfiguration configuration, InstanceServices services, BaseLauncher launcher) {
|
||||||
this.shortName = GetLoggerName(configuration.InstanceGuid);
|
this.shortName = GetLoggerName(configuration.InstanceGuid);
|
||||||
this.logger = PhantomLogger.Create<Instance>(shortName);
|
this.logger = PhantomLogger.Create<Instance>(shortName);
|
||||||
|
|
||||||
this.Configuration = configuration;
|
this.Configuration = configuration;
|
||||||
|
this.Services = services;
|
||||||
this.Launcher = launcher;
|
this.Launcher = launcher;
|
||||||
|
|
||||||
this.launchServices = launchServices;
|
|
||||||
this.portManager = portManager;
|
|
||||||
this.currentState = new InstanceNotRunningState();
|
this.currentState = new InstanceNotRunningState();
|
||||||
this.currentStatus = InstanceStatus.NotRunning;
|
this.currentStatus = InstanceStatus.NotRunning;
|
||||||
}
|
}
|
||||||
@ -139,20 +136,18 @@ sealed class Instance : IDisposable {
|
|||||||
|
|
||||||
private int statusUpdateCounter;
|
private int statusUpdateCounter;
|
||||||
|
|
||||||
public InstanceContextImpl(Instance instance, CancellationToken shutdownCancellationToken) : base(instance.Configuration, instance.Launcher) {
|
public InstanceContextImpl(Instance instance, CancellationToken shutdownCancellationToken) : base(instance.Configuration, instance.Services, instance.Launcher) {
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
this.shutdownCancellationToken = shutdownCancellationToken;
|
this.shutdownCancellationToken = shutdownCancellationToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override LaunchServices LaunchServices => instance.launchServices;
|
|
||||||
public override PortManager PortManager => instance.portManager;
|
|
||||||
public override ILogger Logger => instance.logger;
|
public override ILogger Logger => instance.logger;
|
||||||
public override string ShortName => instance.shortName;
|
public override string ShortName => instance.shortName;
|
||||||
|
|
||||||
public override void ReportStatus(IInstanceStatus newStatus) {
|
public override void ReportStatus(IInstanceStatus newStatus) {
|
||||||
int myStatusUpdateCounter = Interlocked.Increment(ref statusUpdateCounter);
|
int myStatusUpdateCounter = Interlocked.Increment(ref statusUpdateCounter);
|
||||||
|
|
||||||
instance.launchServices.TaskManager.Run("Report status of instance " + instance.shortName + " as " + newStatus.GetType().Name, async () => {
|
instance.Services.TaskManager.Run("Report status of instance " + instance.shortName + " as " + newStatus.GetType().Name, async () => {
|
||||||
if (myStatusUpdateCounter == statusUpdateCounter) {
|
if (myStatusUpdateCounter == statusUpdateCounter) {
|
||||||
instance.currentStatus = newStatus;
|
instance.currentStatus = newStatus;
|
||||||
await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, newStatus));
|
await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, newStatus));
|
||||||
|
@ -7,16 +7,16 @@ namespace Phantom.Agent.Services.Instances;
|
|||||||
|
|
||||||
abstract class InstanceContext {
|
abstract class InstanceContext {
|
||||||
public InstanceConfiguration Configuration { get; }
|
public InstanceConfiguration Configuration { get; }
|
||||||
|
public InstanceServices Services { get; }
|
||||||
public BaseLauncher Launcher { get; }
|
public BaseLauncher Launcher { get; }
|
||||||
|
|
||||||
public abstract LaunchServices LaunchServices { get; }
|
|
||||||
public abstract PortManager PortManager { get; }
|
|
||||||
public abstract ILogger Logger { get; }
|
public abstract ILogger Logger { get; }
|
||||||
public abstract string ShortName { get; }
|
public abstract string ShortName { get; }
|
||||||
|
|
||||||
protected InstanceContext(InstanceConfiguration configuration, BaseLauncher launcher) {
|
protected InstanceContext(InstanceConfiguration configuration, InstanceServices services, BaseLauncher launcher) {
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
Launcher = launcher;
|
Launcher = launcher;
|
||||||
|
Services = services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void ReportStatus(IInstanceStatus newStatus);
|
public abstract void ReportStatus(IInstanceStatus newStatus);
|
||||||
|
@ -4,50 +4,33 @@ using Phantom.Common.Logging;
|
|||||||
using Phantom.Common.Messages.ToServer;
|
using Phantom.Common.Messages.ToServer;
|
||||||
using Phantom.Utils.Collections;
|
using Phantom.Utils.Collections;
|
||||||
using Phantom.Utils.Runtime;
|
using Phantom.Utils.Runtime;
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
sealed class InstanceLogSender {
|
sealed class InstanceLogSender : CancellableBackgroundTask {
|
||||||
private static readonly TimeSpan SendDelay = TimeSpan.FromMilliseconds(200);
|
private static readonly TimeSpan SendDelay = TimeSpan.FromMilliseconds(200);
|
||||||
|
|
||||||
private readonly Guid instanceGuid;
|
private readonly Guid instanceGuid;
|
||||||
private readonly ILogger logger;
|
|
||||||
private readonly CancellationTokenSource cancellationTokenSource;
|
|
||||||
private readonly CancellationToken cancellationToken;
|
|
||||||
|
|
||||||
private readonly SemaphoreSlim semaphore = new (1, 1);
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
private readonly RingBuffer<string> buffer = new (1000);
|
private readonly RingBuffer<string> buffer = new (1000);
|
||||||
|
|
||||||
public InstanceLogSender(TaskManager taskManager, Guid instanceGuid, string name) {
|
public InstanceLogSender(TaskManager taskManager, Guid instanceGuid, string loggerName) : base(PhantomLogger.Create<InstanceLogSender>(loggerName), taskManager, "Instance log sender for " + loggerName) {
|
||||||
this.instanceGuid = instanceGuid;
|
this.instanceGuid = instanceGuid;
|
||||||
this.logger = PhantomLogger.Create<InstanceLogSender>(name);
|
|
||||||
this.cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
this.cancellationToken = cancellationTokenSource.Token;
|
|
||||||
taskManager.Run("Instance log sender for " + name, Run);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Run() {
|
protected override async Task RunTask() {
|
||||||
logger.Verbose("Task started.");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
while (!CancellationToken.IsCancellationRequested) {
|
||||||
while (!cancellationToken.IsCancellationRequested) {
|
await SendOutputToServer(await DequeueOrThrow());
|
||||||
await SendOutputToServer(await DequeueOrThrow());
|
await Task.Delay(SendDelay, CancellationToken);
|
||||||
await Task.Delay(SendDelay, cancellationToken);
|
|
||||||
}
|
|
||||||
} catch (OperationCanceledException) {
|
|
||||||
// Ignore.
|
|
||||||
}
|
}
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
// Flush remaining lines.
|
// Ignore.
|
||||||
await SendOutputToServer(DequeueWithoutSemaphore());
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.Error(e, "Caught exception in task.");
|
|
||||||
} finally {
|
|
||||||
cancellationTokenSource.Dispose();
|
|
||||||
logger.Verbose("Task stopped.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush remaining lines.
|
||||||
|
await SendOutputToServer(DequeueWithoutSemaphore());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendOutputToServer(ImmutableArray<string> lines) {
|
private async Task SendOutputToServer(ImmutableArray<string> lines) {
|
||||||
@ -63,7 +46,7 @@ sealed class InstanceLogSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ImmutableArray<string>> DequeueOrThrow() {
|
private async Task<ImmutableArray<string>> DequeueOrThrow() {
|
||||||
await semaphore.WaitAsync(cancellationToken);
|
await semaphore.WaitAsync(CancellationToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return DequeueWithoutSemaphore();
|
return DequeueWithoutSemaphore();
|
||||||
@ -74,7 +57,7 @@ sealed class InstanceLogSender {
|
|||||||
|
|
||||||
public void Enqueue(string line) {
|
public void Enqueue(string line) {
|
||||||
try {
|
try {
|
||||||
semaphore.Wait(cancellationToken);
|
semaphore.Wait(CancellationToken);
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -85,12 +68,4 @@ sealed class InstanceLogSender {
|
|||||||
semaphore.Release();
|
semaphore.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Cancel() {
|
|
||||||
try {
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
} catch (ObjectDisposedException) {
|
|
||||||
// Ignore.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
using Phantom.Agent.Minecraft.Launcher;
|
||||||
|
using Phantom.Agent.Services.Backups;
|
||||||
|
using Phantom.Utils.Runtime;
|
||||||
|
|
||||||
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
|
sealed record InstanceServices(TaskManager TaskManager, PortManager PortManager, BackupManager BackupManager, LaunchServices LaunchServices);
|
@ -7,6 +7,7 @@ using Phantom.Agent.Minecraft.Launcher.Types;
|
|||||||
using Phantom.Agent.Minecraft.Properties;
|
using Phantom.Agent.Minecraft.Properties;
|
||||||
using Phantom.Agent.Minecraft.Server;
|
using Phantom.Agent.Minecraft.Server;
|
||||||
using Phantom.Agent.Rpc;
|
using Phantom.Agent.Rpc;
|
||||||
|
using Phantom.Agent.Services.Backups;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Agent;
|
using Phantom.Common.Data.Agent;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
@ -27,21 +28,22 @@ sealed class InstanceSessionManager : IDisposable {
|
|||||||
private readonly string basePath;
|
private readonly string basePath;
|
||||||
|
|
||||||
private readonly MinecraftServerExecutables minecraftServerExecutables;
|
private readonly MinecraftServerExecutables minecraftServerExecutables;
|
||||||
private readonly LaunchServices launchServices;
|
private readonly InstanceServices instanceServices;
|
||||||
private readonly PortManager portManager;
|
|
||||||
private readonly Dictionary<Guid, Instance> instances = new ();
|
private readonly Dictionary<Guid, Instance> instances = new ();
|
||||||
|
|
||||||
private readonly CancellationTokenSource shutdownCancellationTokenSource = new ();
|
private readonly CancellationTokenSource shutdownCancellationTokenSource = new ();
|
||||||
private readonly CancellationToken shutdownCancellationToken;
|
private readonly CancellationToken shutdownCancellationToken;
|
||||||
private readonly SemaphoreSlim semaphore = new (1, 1);
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
|
|
||||||
public InstanceSessionManager(AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager) {
|
public InstanceSessionManager(AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager, BackupManager backupManager) {
|
||||||
this.agentInfo = agentInfo;
|
this.agentInfo = agentInfo;
|
||||||
this.basePath = agentFolders.InstancesFolderPath;
|
this.basePath = agentFolders.InstancesFolderPath;
|
||||||
this.minecraftServerExecutables = new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath);
|
this.minecraftServerExecutables = new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath);
|
||||||
this.launchServices = new LaunchServices(taskManager, minecraftServerExecutables, javaRuntimeRepository);
|
|
||||||
this.portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts);
|
|
||||||
this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
||||||
|
|
||||||
|
var launchServices = new LaunchServices(minecraftServerExecutables, javaRuntimeRepository);
|
||||||
|
var portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts);
|
||||||
|
this.instanceServices = new InstanceServices(taskManager, portManager, backupManager, launchServices);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<InstanceActionResult<T>> AcquireSemaphoreAndRun<T>(Func<Task<InstanceActionResult<T>>> func) {
|
private async Task<InstanceActionResult<T>> AcquireSemaphoreAndRun<T>(Func<Task<InstanceActionResult<T>>> func) {
|
||||||
@ -98,7 +100,7 @@ sealed class InstanceSessionManager : IDisposable {
|
|||||||
Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);
|
Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
instances[instanceGuid] = instance = await Instance.Create(configuration, launcher, launchServices, portManager);
|
instances[instanceGuid] = instance = await Instance.Create(configuration, instanceServices, launcher);
|
||||||
instance.IsRunningChanged += OnInstanceIsRunningChanged;
|
instance.IsRunningChanged += OnInstanceIsRunningChanged;
|
||||||
Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);
|
Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);
|
||||||
}
|
}
|
||||||
@ -116,7 +118,7 @@ sealed class InstanceSessionManager : IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void OnInstanceIsRunningChanged(object? sender, EventArgs e) {
|
private void OnInstanceIsRunningChanged(object? sender, EventArgs e) {
|
||||||
launchServices.TaskManager.Run("Handle instance running state changed event", RefreshAgentStatus);
|
instanceServices.TaskManager.Run("Handle instance running state changed event", RefreshAgentStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshAgentStatus() {
|
public async Task RefreshAgentStatus() {
|
||||||
|
@ -19,7 +19,7 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
|
|||||||
public void Initialize() {
|
public void Initialize() {
|
||||||
context.Logger.Information("Session starting...");
|
context.Logger.Information("Session starting...");
|
||||||
|
|
||||||
var launchTask = context.LaunchServices.TaskManager.Run("Launch procedure for instance " + context.ShortName, DoLaunch);
|
var launchTask = context.Services.TaskManager.Run("Launch procedure for instance " + context.ShortName, DoLaunch);
|
||||||
launchTask.ContinueWith(OnLaunchSuccess, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
|
launchTask.ContinueWith(OnLaunchSuccess, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
|
||||||
launchTask.ContinueWith(OnLaunchFailure, CancellationToken.None, TaskContinuationOptions.NotOnRanToCompletion, TaskScheduler.Default);
|
launchTask.ContinueWith(OnLaunchFailure, CancellationToken.None, TaskContinuationOptions.NotOnRanToCompletion, TaskScheduler.Default);
|
||||||
}
|
}
|
||||||
@ -37,7 +37,7 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var launchResult = await context.Launcher.Launch(context.Logger, context.LaunchServices, OnDownloadProgress, cancellationToken);
|
var launchResult = await context.Launcher.Launch(context.Logger, context.Services.LaunchServices, OnDownloadProgress, cancellationToken);
|
||||||
if (launchResult is LaunchResult.InvalidJavaRuntime) {
|
if (launchResult is LaunchResult.InvalidJavaRuntime) {
|
||||||
throw new LaunchFailureException(InstanceLaunchFailReason.JavaRuntimeNotFound, "Session failed to launch, invalid Java runtime.");
|
throw new LaunchFailureException(InstanceLaunchFailReason.JavaRuntimeNotFound, "Session failed to launch, invalid Java runtime.");
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
|
|||||||
private void OnLaunchSuccess(Task<InstanceSession> task) {
|
private void OnLaunchSuccess(Task<InstanceSession> task) {
|
||||||
context.TransitionState(() => {
|
context.TransitionState(() => {
|
||||||
if (cancellationTokenSource.IsCancellationRequested) {
|
if (cancellationTokenSource.IsCancellationRequested) {
|
||||||
context.PortManager.Release(context.Configuration);
|
context.Services.PortManager.Release(context.Configuration);
|
||||||
return (new InstanceNotRunningState(), InstanceStatus.NotRunning);
|
return (new InstanceNotRunningState(), InstanceStatus.NotRunning);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -83,7 +83,7 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
|
|||||||
context.ReportStatus(InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError));
|
context.ReportStatus(InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError));
|
||||||
}
|
}
|
||||||
|
|
||||||
context.PortManager.Release(context.Configuration);
|
context.Services.PortManager.Release(context.Configuration);
|
||||||
context.TransitionState(new InstanceNotRunningState());
|
context.TransitionState(new InstanceNotRunningState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ sealed class InstanceNotRunningState : IInstanceState {
|
|||||||
public void Initialize() {}
|
public void Initialize() {}
|
||||||
|
|
||||||
public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) {
|
public (IInstanceState, LaunchInstanceResult) Launch(InstanceContext context) {
|
||||||
InstanceLaunchFailReason? failReason = context.PortManager.Reserve(context.Configuration) switch {
|
InstanceLaunchFailReason? failReason = context.Services.PortManager.Reserve(context.Configuration) switch {
|
||||||
PortManager.Result.ServerPortNotAllowed => InstanceLaunchFailReason.ServerPortNotAllowed,
|
PortManager.Result.ServerPortNotAllowed => InstanceLaunchFailReason.ServerPortNotAllowed,
|
||||||
PortManager.Result.ServerPortAlreadyInUse => InstanceLaunchFailReason.ServerPortAlreadyInUse,
|
PortManager.Result.ServerPortAlreadyInUse => InstanceLaunchFailReason.ServerPortAlreadyInUse,
|
||||||
PortManager.Result.RconPortNotAllowed => InstanceLaunchFailReason.RconPortNotAllowed,
|
PortManager.Result.RconPortNotAllowed => InstanceLaunchFailReason.RconPortNotAllowed,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using Phantom.Agent.Minecraft.Command;
|
using Phantom.Agent.Minecraft.Command;
|
||||||
using Phantom.Agent.Minecraft.Instance;
|
using Phantom.Agent.Minecraft.Instance;
|
||||||
|
using Phantom.Agent.Services.Backups;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
@ -10,6 +11,7 @@ sealed class InstanceRunningState : IInstanceState {
|
|||||||
private readonly InstanceContext context;
|
private readonly InstanceContext context;
|
||||||
private readonly InstanceSession session;
|
private readonly InstanceSession session;
|
||||||
private readonly InstanceLogSender logSender;
|
private readonly InstanceLogSender logSender;
|
||||||
|
private readonly BackupScheduler backupScheduler;
|
||||||
private readonly SessionObjects sessionObjects;
|
private readonly SessionObjects sessionObjects;
|
||||||
|
|
||||||
private readonly CancellationTokenSource delayedStopCancellationTokenSource = new ();
|
private readonly CancellationTokenSource delayedStopCancellationTokenSource = new ();
|
||||||
@ -19,7 +21,8 @@ sealed class InstanceRunningState : IInstanceState {
|
|||||||
public InstanceRunningState(InstanceContext context, InstanceSession session) {
|
public InstanceRunningState(InstanceContext context, InstanceSession session) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.logSender = new InstanceLogSender(context.LaunchServices.TaskManager, context.Configuration.InstanceGuid, context.ShortName);
|
this.logSender = new InstanceLogSender(context.Services.TaskManager, context.Configuration.InstanceGuid, context.ShortName);
|
||||||
|
this.backupScheduler = new BackupScheduler(context.Services.TaskManager, context.Services.BackupManager, session, context.Configuration.ServerPort, context.ShortName);
|
||||||
this.sessionObjects = new SessionObjects(this);
|
this.sessionObjects = new SessionObjects(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +33,7 @@ sealed class InstanceRunningState : IInstanceState {
|
|||||||
if (session.HasEnded) {
|
if (session.HasEnded) {
|
||||||
if (sessionObjects.Dispose()) {
|
if (sessionObjects.Dispose()) {
|
||||||
context.Logger.Warning("Session ended immediately after it was started.");
|
context.Logger.Warning("Session ended immediately after it was started.");
|
||||||
context.LaunchServices.TaskManager.Run("Transition state of instance " + context.ShortName + " to not running", () => context.TransitionState(new InstanceNotRunningState(), InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)));
|
context.Services.TaskManager.Run("Transition state of instance " + context.ShortName + " to not running", () => context.TransitionState(new InstanceNotRunningState(), InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -39,9 +42,9 @@ sealed class InstanceRunningState : IInstanceState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SessionOutput(object? sender, string e) {
|
private void SessionOutput(object? sender, string line) {
|
||||||
context.Logger.Verbose("[Server] {Line}", e);
|
context.Logger.Verbose("[Server] {Line}", line);
|
||||||
logSender.Enqueue(e);
|
logSender.Enqueue(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SessionEnded(object? sender, EventArgs e) {
|
private void SessionEnded(object? sender, EventArgs e) {
|
||||||
@ -75,12 +78,13 @@ sealed class InstanceRunningState : IInstanceState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isStopping = true;
|
isStopping = true;
|
||||||
context.LaunchServices.TaskManager.Run("Delayed stop timer for instance " + context.ShortName, () => StopLater(stopStrategy.Seconds));
|
context.Services.TaskManager.Run("Delayed stop timer for instance " + context.ShortName, () => StopLater(stopStrategy.Seconds));
|
||||||
return (this, StopInstanceResult.StopInitiated);
|
return (this, StopInstanceResult.StopInitiated);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IInstanceState PrepareStoppedState() {
|
private IInstanceState PrepareStoppedState() {
|
||||||
session.SessionEnded -= SessionEnded;
|
session.SessionEnded -= SessionEnded;
|
||||||
|
backupScheduler.Stop();
|
||||||
return new InstanceStoppingState(context, session, sessionObjects);
|
return new InstanceStoppingState(context, session, sessionObjects);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,9 +163,9 @@ sealed class InstanceRunningState : IInstanceState {
|
|||||||
state.CancelDelayedStop();
|
state.CancelDelayedStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
state.logSender.Cancel();
|
state.logSender.Stop();
|
||||||
state.session.Dispose();
|
state.session.Dispose();
|
||||||
state.context.PortManager.Release(state.context.Configuration);
|
state.context.Services.PortManager.Release(state.context.Configuration);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,11 +21,17 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable {
|
|||||||
public void Initialize() {
|
public void Initialize() {
|
||||||
context.Logger.Information("Session stopping.");
|
context.Logger.Information("Session stopping.");
|
||||||
context.ReportStatus(InstanceStatus.Stopping);
|
context.ReportStatus(InstanceStatus.Stopping);
|
||||||
context.LaunchServices.TaskManager.Run("Stop procedure for instance " + context.ShortName, DoStop);
|
context.Services.TaskManager.Run("Stop procedure for instance " + context.ShortName, DoStop);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DoStop() {
|
private async Task DoStop() {
|
||||||
try {
|
try {
|
||||||
|
// Do not release the semaphore after this point.
|
||||||
|
if (!await session.BackupSemaphore.CancelAndWait(TimeSpan.FromSeconds(1))) {
|
||||||
|
context.Logger.Information("Waiting for backup to finish...");
|
||||||
|
await session.BackupSemaphore.CancelAndWait(Timeout.InfiniteTimeSpan);
|
||||||
|
}
|
||||||
|
|
||||||
context.Logger.Information("Sending stop command...");
|
context.Logger.Information("Sending stop command...");
|
||||||
await DoSendStopCommand();
|
await DoSendStopCommand();
|
||||||
|
|
||||||
|
@ -3,23 +3,35 @@
|
|||||||
public enum BackupCreationResultKind : byte {
|
public enum BackupCreationResultKind : byte {
|
||||||
UnknownError,
|
UnknownError,
|
||||||
Success,
|
Success,
|
||||||
|
InstanceNotRunning,
|
||||||
BackupCancelled,
|
BackupCancelled,
|
||||||
BackupAlreadyExists,
|
BackupAlreadyRunning,
|
||||||
|
BackupFileAlreadyExists,
|
||||||
CouldNotCreateBackupFolder,
|
CouldNotCreateBackupFolder,
|
||||||
CouldNotCopyWorldToTemporaryFolder,
|
CouldNotCopyWorldToTemporaryFolder,
|
||||||
CouldNotCreateWorldArchive
|
CouldNotCreateWorldArchive
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class BackupCreationResultSummaryExtensions {
|
public static class BackupCreationResultSummaryExtensions {
|
||||||
|
public static bool ShouldRetry(this BackupCreationResultKind kind) {
|
||||||
|
return kind != BackupCreationResultKind.Success &&
|
||||||
|
kind != BackupCreationResultKind.InstanceNotRunning &&
|
||||||
|
kind != BackupCreationResultKind.BackupCancelled &&
|
||||||
|
kind != BackupCreationResultKind.BackupAlreadyRunning &&
|
||||||
|
kind != BackupCreationResultKind.BackupFileAlreadyExists;
|
||||||
|
}
|
||||||
|
|
||||||
public static string ToSentence(this BackupCreationResultKind kind) {
|
public static string ToSentence(this BackupCreationResultKind kind) {
|
||||||
return kind switch {
|
return kind switch {
|
||||||
BackupCreationResultKind.Success => "Backup created successfully.",
|
BackupCreationResultKind.Success => "Backup created successfully.",
|
||||||
|
BackupCreationResultKind.InstanceNotRunning => "Instance is not running.",
|
||||||
BackupCreationResultKind.BackupCancelled => "Backup cancelled.",
|
BackupCreationResultKind.BackupCancelled => "Backup cancelled.",
|
||||||
BackupCreationResultKind.BackupAlreadyExists => "Backup with the same name already exists.",
|
BackupCreationResultKind.BackupAlreadyRunning => "A backup is already being created.",
|
||||||
|
BackupCreationResultKind.BackupFileAlreadyExists => "Backup with the same name already exists.",
|
||||||
BackupCreationResultKind.CouldNotCreateBackupFolder => "Could not create backup folder.",
|
BackupCreationResultKind.CouldNotCreateBackupFolder => "Could not create backup folder.",
|
||||||
BackupCreationResultKind.CouldNotCopyWorldToTemporaryFolder => "Could not copy world to temporary folder.",
|
BackupCreationResultKind.CouldNotCopyWorldToTemporaryFolder => "Could not copy world to temporary folder.",
|
||||||
BackupCreationResultKind.CouldNotCreateWorldArchive => "Could not create world archive.",
|
BackupCreationResultKind.CouldNotCreateWorldArchive => "Could not create world archive.",
|
||||||
_ => "Unknown error."
|
_ => "Unknown error."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
namespace Phantom.Common.Data.Backups;
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Phantom.Common.Data.Backups;
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum BackupCreationWarnings : byte {
|
public enum BackupCreationWarnings : byte {
|
||||||
@ -7,3 +9,9 @@ public enum BackupCreationWarnings : byte {
|
|||||||
CouldNotCompressWorldArchive = 1 << 1,
|
CouldNotCompressWorldArchive = 1 << 1,
|
||||||
CouldNotRestoreAutomaticSaving = 1 << 2
|
CouldNotRestoreAutomaticSaving = 1 << 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class BackupCreationWarningsExtensions {
|
||||||
|
public static int Count(this BackupCreationWarnings warnings) {
|
||||||
|
return BitOperations.PopCount((byte) warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -59,7 +59,8 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \
|
|||||||
rm -f /etc/apt/apt.conf.d/docker-clean && \
|
rm -f /etc/apt/apt.conf.d/docker-clean && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
openjdk-18-jre-headless
|
openjdk-18-jre-headless \
|
||||||
|
zstd
|
||||||
|
|
||||||
RUN mkdir /data && chmod 777 /data
|
RUN mkdir /data && chmod 777 /data
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
|
41
Utils/Phantom.Utils.Runtime/CancellableBackgroundTask.cs
Normal file
41
Utils/Phantom.Utils.Runtime/CancellableBackgroundTask.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Runtime;
|
||||||
|
|
||||||
|
public abstract class CancellableBackgroundTask {
|
||||||
|
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
||||||
|
|
||||||
|
protected ILogger Logger { get; }
|
||||||
|
protected CancellationToken CancellationToken { get; }
|
||||||
|
|
||||||
|
protected CancellableBackgroundTask(ILogger logger, TaskManager taskManager, string taskName) {
|
||||||
|
this.Logger = logger;
|
||||||
|
this.CancellationToken = cancellationTokenSource.Token;
|
||||||
|
taskManager.Run(taskName, Run);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Run() {
|
||||||
|
Logger.Verbose("Task started.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await RunTask();
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
// Ignore.
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.Fatal(e, "Caught exception in task.");
|
||||||
|
} finally {
|
||||||
|
cancellationTokenSource.Dispose();
|
||||||
|
Logger.Verbose("Task stopped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task RunTask();
|
||||||
|
|
||||||
|
public void Stop() {
|
||||||
|
try {
|
||||||
|
cancellationTokenSource.Cancel();
|
||||||
|
} catch (ObjectDisposedException) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
Utils/Phantom.Utils.Runtime/CancellableSemaphore.cs
Normal file
28
Utils/Phantom.Utils.Runtime/CancellableSemaphore.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
namespace Phantom.Utils.Runtime;
|
||||||
|
|
||||||
|
public sealed class CancellableSemaphore : IDisposable {
|
||||||
|
private readonly SemaphoreSlim semaphore;
|
||||||
|
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
||||||
|
|
||||||
|
public CancellableSemaphore(int initialCount, int maxCount) {
|
||||||
|
this.semaphore = new SemaphoreSlim(initialCount, maxCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Wait(TimeSpan timeout, CancellationToken cancellationToken) {
|
||||||
|
return await semaphore.WaitAsync(timeout, cancellationTokenSource.Token).WaitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CancelAndWait(TimeSpan timeout) {
|
||||||
|
cancellationTokenSource.Cancel();
|
||||||
|
return await semaphore.WaitAsync(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Release() {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
semaphore.Dispose();
|
||||||
|
cancellationTokenSource.Dispose();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user