1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2024-11-25 07:42:58 +01:00

Compare commits

..

3 Commits

Author SHA1 Message Date
651a660cfc
WIP 2024-04-01 18:37:08 +02:00
5e6cb91b64
WIP 2024-04-01 18:20:33 +02:00
80d6bcbcac
Fix backups not working with Forge log format & add timeouts while waiting for server output 2024-04-01 18:13:15 +02:00
9 changed files with 46 additions and 135 deletions

View File

@ -16,47 +16,36 @@ sealed class MinecraftServerExecutableDownloader {
public event EventHandler? Completed; public event EventHandler? Completed;
private readonly CancellationTokenSource cancellationTokenSource = new (); private readonly CancellationTokenSource cancellationTokenSource = new ();
private int listeners = 0;
private readonly List<CancellationTokenRegistration> listenerCancellationRegistrations = new ();
private int listenerCount = 0;
public MinecraftServerExecutableDownloader(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath, MinecraftServerExecutableDownloadListener listener) { public MinecraftServerExecutableDownloader(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath, MinecraftServerExecutableDownloadListener listener) {
Register(listener); Register(listener);
Task = DownloadAndGetPath(fileDownloadInfo, minecraftVersion, filePath, new DownloadProgressCallback(this), cancellationTokenSource.Token); Task = DownloadAndGetPath(fileDownloadInfo, minecraftVersion, filePath);
Task.ContinueWith(OnCompleted, TaskScheduler.Default); Task.ContinueWith(OnCompleted, TaskScheduler.Default);
} }
public void Register(MinecraftServerExecutableDownloadListener listener) { public void Register(MinecraftServerExecutableDownloadListener listener) {
int newListenerCount; ++listeners;
Logger.Debug("Registered download listener, current listener count: {Listeners}", listeners);
lock (this) { DownloadProgress += listener.DownloadProgressEventHandler;
newListenerCount = ++listenerCount; listener.CancellationToken.Register(Unregister, listener);
DownloadProgress += listener.DownloadProgressEventHandler;
listenerCancellationRegistrations.Add(listener.CancellationToken.Register(Unregister, listener));
}
Logger.Debug("Registered download listener, current listener count: {Listeners}", newListenerCount);
} }
private void Unregister(object? listenerObject) { private void Unregister(object? listenerObject) {
int newListenerCount; MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!;
DownloadProgress -= listener.DownloadProgressEventHandler;
lock (this) { if (--listeners <= 0) {
MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!; Logger.Debug("Unregistered last download listener, cancelling download.");
DownloadProgress -= listener.DownloadProgressEventHandler; try {
newListenerCount = --listenerCount;
if (newListenerCount <= 0) {
cancellationTokenSource.Cancel(); cancellationTokenSource.Cancel();
} catch (ObjectDisposedException) {
// TODO THIS IS BAD
} }
} }
if (newListenerCount <= 0) {
Logger.Debug("Unregistered last download listener, cancelling download.");
}
else { else {
Logger.Debug("Unregistered download listener, current listener count: {Listeners}", newListenerCount); Logger.Debug("Unregistered download listener, current listener count: {Listeners}", listeners);
} }
} }
@ -66,19 +55,9 @@ sealed class MinecraftServerExecutableDownloader {
private void OnCompleted(Task task) { private void OnCompleted(Task task) {
Logger.Debug("Download task completed."); Logger.Debug("Download task completed.");
Completed?.Invoke(this, EventArgs.Empty);
lock (this) { Completed = null;
Completed?.Invoke(this, EventArgs.Empty); DownloadProgress = null;
Completed = null;
DownloadProgress = null;
foreach (var registration in listenerCancellationRegistrations) {
registration.Dispose();
}
listenerCancellationRegistrations.Clear();
cancellationTokenSource.Dispose();
}
} }
private sealed class DownloadProgressCallback { private sealed class DownloadProgressCallback {
@ -93,14 +72,15 @@ sealed class MinecraftServerExecutableDownloader {
} }
} }
private static async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath, DownloadProgressCallback progressCallback, CancellationToken cancellationToken) { private async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath) {
string tmpFilePath = filePath + ".tmp"; string tmpFilePath = filePath + ".tmp";
var cancellationToken = cancellationTokenSource.Token;
try { try {
Logger.Information("Downloading server version {Version} from: {Url} ({Size})", minecraftVersion, fileDownloadInfo.DownloadUrl, fileDownloadInfo.Size.ToHumanReadable(decimalPlaces: 1)); Logger.Information("Downloading server version {Version} from: {Url} ({Size})", minecraftVersion, fileDownloadInfo.DownloadUrl, fileDownloadInfo.Size.ToHumanReadable(decimalPlaces: 1));
try { try {
using var http = new HttpClient(); using var http = new HttpClient();
await FetchServerExecutableFile(http, progressCallback, fileDownloadInfo, tmpFilePath, cancellationToken); await FetchServerExecutableFile(http, new DownloadProgressCallback(this), fileDownloadInfo, tmpFilePath, cancellationToken);
} catch (Exception) { } catch (Exception) {
TryDeleteExecutableAfterFailure(tmpFilePath); TryDeleteExecutableAfterFailure(tmpFilePath);
throw; throw;
@ -118,6 +98,8 @@ sealed class MinecraftServerExecutableDownloader {
} catch (Exception e) { } catch (Exception e) {
Logger.Error(e, "An unexpected error occurred."); Logger.Error(e, "An unexpected error occurred.");
return null; return null;
} finally {
cancellationTokenSource.Dispose();
} }
} }

View File

@ -81,7 +81,6 @@ sealed class BackupManager : IDisposable {
} catch (OperationCanceledException) { } catch (OperationCanceledException) {
// Ignore. // Ignore.
} catch (TimeoutException) { } catch (TimeoutException) {
resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving;
logger.Warning("Timed out waiting for automatic saving to be re-enabled."); logger.Warning("Timed out waiting for automatic saving to be re-enabled.");
} catch (Exception e) { } catch (Exception e) {
resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving; resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving;

View File

@ -1,5 +1,4 @@
using System.Collections.Immutable; using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using Phantom.Agent.Minecraft.Command; using Phantom.Agent.Minecraft.Command;
using Phantom.Agent.Minecraft.Instance; using Phantom.Agent.Minecraft.Instance;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
@ -8,27 +7,9 @@ using Serilog;
namespace Phantom.Agent.Services.Backups; namespace Phantom.Agent.Services.Backups;
sealed partial class BackupServerCommandDispatcher : IDisposable { sealed partial class BackupServerCommandDispatcher : IDisposable {
[GeneratedRegex(@"^(?:(?:\[.*?\] \[Server thread/INFO\].*?:)|(?:[\d-]+? [\d:]+? \[INFO\])) (.*?)$", RegexOptions.NonBacktracking)] [GeneratedRegex(@"^\[(?:.*?)\] \[Server thread/INFO\](?:.*?): (.*?)$", RegexOptions.NonBacktracking)]
private static partial Regex ServerThreadInfoRegex(); private static partial Regex ServerThreadInfoRegex();
private static readonly ImmutableHashSet<string> AutomaticSavingDisabledMessages = ImmutableHashSet.Create(
"Automatic saving is now disabled",
"Turned off world auto-saving",
"CONSOLE: Disabling level saving.."
);
private static readonly ImmutableHashSet<string> SavedTheGameMessages = ImmutableHashSet.Create(
"Saved the game",
"Saved the world",
"CONSOLE: Save complete."
);
private static readonly ImmutableHashSet<string> AutomaticSavingEnabledMessages = ImmutableHashSet.Create(
"Automatic saving is now enabled",
"Turned on world auto-saving",
"CONSOLE: Enabling level saving.."
);
private readonly ILogger logger; private readonly ILogger logger;
private readonly InstanceProcess process; private readonly InstanceProcess process;
private readonly CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
@ -78,19 +59,19 @@ sealed partial class BackupServerCommandDispatcher : IDisposable {
string info = match.Groups[1].Value; string info = match.Groups[1].Value;
if (!automaticSavingDisabled.Task.IsCompleted) { if (!automaticSavingDisabled.Task.IsCompleted) {
if (AutomaticSavingDisabledMessages.Contains(info)) { if (info == "Automatic saving is now disabled") {
logger.Debug("Detected that automatic saving is disabled."); logger.Debug("Detected that automatic saving is disabled.");
automaticSavingDisabled.SetResult(); automaticSavingDisabled.SetResult();
} }
} }
else if (!savedTheGame.Task.IsCompleted) { else if (!savedTheGame.Task.IsCompleted) {
if (SavedTheGameMessages.Contains(info)) { if (info == "Saved the game") {
logger.Debug("Detected that the game is saved."); logger.Debug("Detected that the game is saved.");
savedTheGame.SetResult(); savedTheGame.SetResult();
} }
} }
else if (!automaticSavingEnabled.Task.IsCompleted) { else if (!automaticSavingEnabled.Task.IsCompleted) {
if (AutomaticSavingEnabledMessages.Contains(info)) { if (info == "Automatic saving is now enabled") {
logger.Debug("Detected that automatic saving is enabled."); logger.Debug("Detected that automatic saving is enabled.");
automaticSavingEnabled.SetResult(); automaticSavingEnabled.SetResult();
} }

View File

@ -135,12 +135,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning); return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning);
} }
else { else {
SetAndReportStatus(InstanceStatus.BackingUp); return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken);
try {
return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken);
} finally {
SetAndReportStatus(InstanceStatus.Running);
}
} }
} }

View File

@ -9,10 +9,9 @@ namespace Phantom.Common.Data.Instance;
[MemoryPackUnion(3, typeof(InstanceIsDownloading))] [MemoryPackUnion(3, typeof(InstanceIsDownloading))]
[MemoryPackUnion(4, typeof(InstanceIsLaunching))] [MemoryPackUnion(4, typeof(InstanceIsLaunching))]
[MemoryPackUnion(5, typeof(InstanceIsRunning))] [MemoryPackUnion(5, typeof(InstanceIsRunning))]
[MemoryPackUnion(6, typeof(InstanceIsBackingUp))] [MemoryPackUnion(6, typeof(InstanceIsRestarting))]
[MemoryPackUnion(7, typeof(InstanceIsRestarting))] [MemoryPackUnion(7, typeof(InstanceIsStopping))]
[MemoryPackUnion(8, typeof(InstanceIsStopping))] [MemoryPackUnion(8, typeof(InstanceIsFailed))]
[MemoryPackUnion(9, typeof(InstanceIsFailed))]
public partial interface IInstanceStatus {} public partial interface IInstanceStatus {}
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
@ -33,9 +32,6 @@ public sealed partial record InstanceIsLaunching : IInstanceStatus;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record InstanceIsRunning : IInstanceStatus; public sealed partial record InstanceIsRunning : IInstanceStatus;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record InstanceIsBackingUp : IInstanceStatus;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record InstanceIsRestarting : IInstanceStatus; public sealed partial record InstanceIsRestarting : IInstanceStatus;
@ -50,7 +46,6 @@ public static class InstanceStatus {
public static readonly IInstanceStatus NotRunning = new InstanceIsNotRunning(); public static readonly IInstanceStatus NotRunning = new InstanceIsNotRunning();
public static readonly IInstanceStatus Launching = new InstanceIsLaunching(); public static readonly IInstanceStatus Launching = new InstanceIsLaunching();
public static readonly IInstanceStatus Running = new InstanceIsRunning(); public static readonly IInstanceStatus Running = new InstanceIsRunning();
public static readonly IInstanceStatus BackingUp = new InstanceIsBackingUp();
public static readonly IInstanceStatus Restarting = new InstanceIsRestarting(); public static readonly IInstanceStatus Restarting = new InstanceIsRestarting();
public static readonly IInstanceStatus Stopping = new InstanceIsStopping(); public static readonly IInstanceStatus Stopping = new InstanceIsStopping();
@ -63,7 +58,7 @@ public static class InstanceStatus {
} }
public static bool IsRunning(this IInstanceStatus status) { public static bool IsRunning(this IInstanceStatus status) {
return status is InstanceIsRunning or InstanceIsBackingUp; return status is InstanceIsRunning;
} }
public static bool IsStopping(this IInstanceStatus status) { public static bool IsStopping(this IInstanceStatus status) {
@ -75,10 +70,10 @@ public static class InstanceStatus {
} }
public static bool CanStop(this IInstanceStatus status) { public static bool CanStop(this IInstanceStatus status) {
return status.IsRunning() || status.IsLaunching(); return status is InstanceIsDownloading or InstanceIsLaunching or InstanceIsRunning;
} }
public static bool CanSendCommand(this IInstanceStatus status) { public static bool CanSendCommand(this IInstanceStatus status) {
return status.IsRunning(); return status is InstanceIsRunning;
} }
} }

View File

@ -13,13 +13,11 @@ using Phantom.Common.Data.Web.Minecraft;
using Phantom.Common.Messages.Agent; using Phantom.Common.Messages.Agent;
using Phantom.Common.Messages.Agent.ToAgent; using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Controller.Database; using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Minecraft; using Phantom.Controller.Minecraft;
using Phantom.Controller.Services.Instances; using Phantom.Controller.Services.Instances;
using Phantom.Utils.Actor; using Phantom.Utils.Actor;
using Phantom.Utils.Actor.Mailbox; using Phantom.Utils.Actor.Mailbox;
using Phantom.Utils.Actor.Tasks; using Phantom.Utils.Actor.Tasks;
using Phantom.Utils.Collections;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Rpc.Runtime; using Phantom.Utils.Rpc.Runtime;
using Serilog; using Serilog;
@ -196,29 +194,21 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
public sealed record ReceiveInstanceDataCommand(Instance Instance) : ICommand, IJumpAhead; public sealed record ReceiveInstanceDataCommand(Instance Instance) : ICommand, IJumpAhead;
private async Task Initialize(InitializeCommand command) { private async Task Initialize(InitializeCommand command) {
ImmutableArray<InstanceEntity> instanceEntities; await using var ctx = dbProvider.Eager();
await using (var ctx = dbProvider.Eager()) { await foreach (var entity in ctx.Instances.Where(instance => instance.AgentGuid == agentGuid).AsAsyncEnumerable().WithCancellation(cancellationToken)) {
instanceEntities = await ctx.Instances.Where(instance => instance.AgentGuid == agentGuid).AsAsyncEnumerable().ToImmutableArrayCatchingExceptionsAsync(OnException, cancellationToken);
}
static void OnException(Exception e) {
Logger.Error(e, "Could not load instance from database.");
}
foreach (var instanceEntity in instanceEntities) {
var instanceConfiguration = new InstanceConfiguration( var instanceConfiguration = new InstanceConfiguration(
instanceEntity.AgentGuid, entity.AgentGuid,
instanceEntity.InstanceName, entity.InstanceName,
instanceEntity.ServerPort, entity.ServerPort,
instanceEntity.RconPort, entity.RconPort,
instanceEntity.MinecraftVersion, entity.MinecraftVersion,
instanceEntity.MinecraftServerKind, entity.MinecraftServerKind,
instanceEntity.MemoryAllocation, entity.MemoryAllocation,
instanceEntity.JavaRuntimeGuid, entity.JavaRuntimeGuid,
JvmArgumentsHelper.Split(instanceEntity.JvmArguments) JvmArgumentsHelper.Split(entity.JvmArguments)
); );
CreateNewInstance(Instance.Offline(instanceEntity.InstanceGuid, instanceConfiguration, instanceEntity.LaunchAutomatically)); CreateNewInstance(Instance.Offline(entity.InstanceGuid, instanceConfiguration, entity.LaunchAutomatically));
} }
} }

View File

@ -13,27 +13,6 @@ public static class EnumerableExtensions {
return builder.ToImmutable(); return builder.ToImmutable();
} }
public static async Task<ImmutableArray<TSource>> ToImmutableArrayCatchingExceptionsAsync<TSource>(this IAsyncEnumerable<TSource> source, Action<Exception> onException, CancellationToken cancellationToken = default) {
var builder = ImmutableArray.CreateBuilder<TSource>();
await using (var enumerator = source.GetAsyncEnumerator(cancellationToken)) {
while (true) {
try {
if (!await enumerator.MoveNextAsync()) {
break;
}
} catch (Exception e) {
onException(e);
continue;
}
builder.Add(enumerator.Current);
}
}
return builder.ToImmutable();
}
public static async Task<ImmutableHashSet<TSource>> ToImmutableSetAsync<TSource>(this IAsyncEnumerable<TSource> source, CancellationToken cancellationToken = default) { public static async Task<ImmutableHashSet<TSource>> ToImmutableSetAsync<TSource>(this IAsyncEnumerable<TSource> source, CancellationToken cancellationToken = default) {
var builder = ImmutableHashSet.CreateBuilder<TSource>(); var builder = ImmutableHashSet.CreateBuilder<TSource>();

View File

@ -51,7 +51,6 @@
form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence)); form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence));
} }
StateHasChanged();
await commandInputElement.FocusAsync(preventScroll: true); await commandInputElement.FocusAsync(preventScroll: true);
} }

View File

@ -28,11 +28,6 @@
<span class="fw-semibold text-success">Running</span> <span class="fw-semibold text-success">Running</span>
break; break;
case InstanceIsBackingUp:
<div class="spinner-border" role="status"></div>
<span class="fw-semibold">&nbsp;Backing Up</span>
break;
case InstanceIsRestarting: case InstanceIsRestarting:
<div class="spinner-border" role="status"></div> <div class="spinner-border" role="status"></div>
<span class="fw-semibold">&nbsp;Restarting</span> <span class="fw-semibold">&nbsp;Restarting</span>
@ -46,10 +41,6 @@
case InstanceIsFailed failed: case InstanceIsFailed failed:
<span class="fw-semibold text-danger">Failed <sup title="@failed.Reason.ToSentence()">[?]</sup></span> <span class="fw-semibold text-danger">Failed <sup title="@failed.Reason.ToSentence()">[?]</sup></span>
break; break;
default:
<span class="fw-semibold">Unknown</span>
break;
} }
</nobr> </nobr>