mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-10-31 11:17:15 +01:00 
			
		
		
		
	Compare commits
	
		
			6 Commits
		
	
	
		
			1b12fd9c3b
			...
			4c3b81c54a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4c3b81c54a | |||
| 8e2b019aa1 | |||
| 9a2c13c1e0 | |||
| 991b32032c | |||
| 875fd9a766 | |||
| f7f08ec55c | 
| @@ -16,32 +16,47 @@ sealed class MinecraftServerExecutableDownloader { | ||||
| 	public event EventHandler? Completed; | ||||
| 	 | ||||
| 	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) { | ||||
| 		Register(listener); | ||||
| 		Task = DownloadAndGetPath(fileDownloadInfo, minecraftVersion, filePath); | ||||
| 		Task = DownloadAndGetPath(fileDownloadInfo, minecraftVersion, filePath, new DownloadProgressCallback(this), cancellationTokenSource.Token); | ||||
| 		Task.ContinueWith(OnCompleted, TaskScheduler.Default); | ||||
| 	} | ||||
|  | ||||
| 	public void Register(MinecraftServerExecutableDownloadListener listener) { | ||||
| 		++listeners; | ||||
| 		Logger.Debug("Registered download listener, current listener count: {Listeners}", listeners); | ||||
| 		int newListenerCount; | ||||
| 		 | ||||
| 		DownloadProgress += listener.DownloadProgressEventHandler; | ||||
| 		listener.CancellationToken.Register(Unregister, listener); | ||||
| 		lock (this) { | ||||
| 			newListenerCount = ++listenerCount; | ||||
| 			 | ||||
| 			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) { | ||||
| 		MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!; | ||||
| 		DownloadProgress -= listener.DownloadProgressEventHandler; | ||||
| 		int newListenerCount; | ||||
| 		 | ||||
| 		lock (this) { | ||||
| 			MinecraftServerExecutableDownloadListener listener = (MinecraftServerExecutableDownloadListener) listenerObject!; | ||||
| 			DownloadProgress -= listener.DownloadProgressEventHandler; | ||||
|  | ||||
| 		if (--listeners <= 0) { | ||||
| 			newListenerCount = --listenerCount; | ||||
| 			if (newListenerCount <= 0) { | ||||
| 				cancellationTokenSource.Cancel(); | ||||
| 			} | ||||
| 		} | ||||
| 		 | ||||
| 		if (newListenerCount <= 0) { | ||||
| 			Logger.Debug("Unregistered last download listener, cancelling download."); | ||||
| 			cancellationTokenSource.Cancel(); | ||||
| 		} | ||||
| 		else { | ||||
| 			Logger.Debug("Unregistered download listener, current listener count: {Listeners}", listeners); | ||||
| 			Logger.Debug("Unregistered download listener, current listener count: {Listeners}", newListenerCount); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -51,9 +66,19 @@ sealed class MinecraftServerExecutableDownloader { | ||||
|  | ||||
| 	private void OnCompleted(Task task) { | ||||
| 		Logger.Debug("Download task completed."); | ||||
| 		Completed?.Invoke(this, EventArgs.Empty); | ||||
| 		Completed = null; | ||||
| 		DownloadProgress = null; | ||||
|  | ||||
| 		lock (this) { | ||||
| 			Completed?.Invoke(this, EventArgs.Empty); | ||||
| 			Completed = null; | ||||
| 			DownloadProgress = null; | ||||
|  | ||||
| 			foreach (var registration in listenerCancellationRegistrations) { | ||||
| 				registration.Dispose(); | ||||
| 			} | ||||
| 			 | ||||
| 			listenerCancellationRegistrations.Clear(); | ||||
| 			cancellationTokenSource.Dispose(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private sealed class DownloadProgressCallback { | ||||
| @@ -68,15 +93,14 @@ sealed class MinecraftServerExecutableDownloader { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath) { | ||||
| 	private static async Task<string?> DownloadAndGetPath(FileDownloadInfo fileDownloadInfo, string minecraftVersion, string filePath, DownloadProgressCallback progressCallback, CancellationToken cancellationToken) { | ||||
| 		string tmpFilePath = filePath + ".tmp"; | ||||
|  | ||||
| 		var cancellationToken = cancellationTokenSource.Token; | ||||
| 		try { | ||||
| 			Logger.Information("Downloading server version {Version} from: {Url} ({Size})", minecraftVersion, fileDownloadInfo.DownloadUrl, fileDownloadInfo.Size.ToHumanReadable(decimalPlaces: 1)); | ||||
| 			try { | ||||
| 				using var http = new HttpClient(); | ||||
| 				await FetchServerExecutableFile(http, new DownloadProgressCallback(this), fileDownloadInfo, tmpFilePath, cancellationToken); | ||||
| 				await FetchServerExecutableFile(http, progressCallback, fileDownloadInfo, tmpFilePath, cancellationToken); | ||||
| 			} catch (Exception) { | ||||
| 				TryDeleteExecutableAfterFailure(tmpFilePath); | ||||
| 				throw; | ||||
| @@ -94,8 +118,6 @@ sealed class MinecraftServerExecutableDownloader { | ||||
| 		} catch (Exception e) { | ||||
| 			Logger.Error(e, "An unexpected error occurred."); | ||||
| 			return null; | ||||
| 		} finally { | ||||
| 			cancellationTokenSource.Dispose(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -67,6 +67,10 @@ sealed class BackupManager : IDisposable { | ||||
| 				resultBuilder.Kind = BackupCreationResultKind.BackupCancelled; | ||||
| 				logger.Warning("Backup creation was cancelled."); | ||||
| 				return null; | ||||
| 			} catch (TimeoutException) { | ||||
| 				resultBuilder.Kind = BackupCreationResultKind.BackupTimedOut; | ||||
| 				logger.Warning("Backup creation timed out."); | ||||
| 				return null; | ||||
| 			} catch (Exception e) { | ||||
| 				resultBuilder.Kind = BackupCreationResultKind.UnknownError; | ||||
| 				logger.Error(e, "Caught exception while creating an instance backup."); | ||||
| @@ -76,6 +80,9 @@ sealed class BackupManager : IDisposable { | ||||
| 					await dispatcher.EnableAutomaticSaving(); | ||||
| 				} catch (OperationCanceledException) { | ||||
| 					// Ignore. | ||||
| 				} catch (TimeoutException) { | ||||
| 					resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving; | ||||
| 					logger.Warning("Timed out waiting for automatic saving to be re-enabled."); | ||||
| 				} catch (Exception e) { | ||||
| 					resultBuilder.Warnings |= BackupCreationWarnings.CouldNotRestoreAutomaticSaving; | ||||
| 					logger.Error(e, "Caught exception while enabling automatic saving after creating an instance backup."); | ||||
| @@ -120,6 +127,7 @@ sealed class BackupManager : IDisposable { | ||||
| 				BackupCreationResultKind.Success                            => "Backup created successfully.", | ||||
| 				BackupCreationResultKind.InstanceNotRunning                 => "Instance is not running.", | ||||
| 				BackupCreationResultKind.BackupCancelled                    => "Backup cancelled.", | ||||
| 				BackupCreationResultKind.BackupTimedOut                     => "Backup timed out.", | ||||
| 				BackupCreationResultKind.BackupAlreadyRunning               => "A backup is already being created.", | ||||
| 				BackupCreationResultKind.BackupFileAlreadyExists            => "Backup with the same name already exists.", | ||||
| 				BackupCreationResultKind.CouldNotCreateBackupFolder         => "Could not create backup folder.", | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.Text.RegularExpressions; | ||||
| using System.Collections.Immutable; | ||||
| using System.Text.RegularExpressions; | ||||
| using Phantom.Agent.Minecraft.Command; | ||||
| using Phantom.Agent.Minecraft.Instance; | ||||
| using Phantom.Utils.Tasks; | ||||
| @@ -7,8 +8,26 @@ using Serilog; | ||||
| namespace Phantom.Agent.Services.Backups; | ||||
|  | ||||
| sealed partial class BackupServerCommandDispatcher : IDisposable { | ||||
| 	[GeneratedRegex(@"^\[(?:.*?)\] \[Server thread/INFO\]: (.*?)$", RegexOptions.NonBacktracking)] | ||||
| 	[GeneratedRegex(@"^(?:(?:\[.*?\] \[Server thread/INFO\].*?:)|(?:[\d-]+? [\d:]+? \[INFO\])) (.*?)$", RegexOptions.NonBacktracking)] | ||||
| 	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 InstanceProcess process; | ||||
| @@ -32,18 +51,18 @@ sealed partial class BackupServerCommandDispatcher : IDisposable { | ||||
|  | ||||
| 	public async Task DisableAutomaticSaving() { | ||||
| 		await process.SendCommand(MinecraftCommand.SaveOff, cancellationToken); | ||||
| 		await automaticSavingDisabled.Task.WaitAsync(cancellationToken); | ||||
| 		await automaticSavingDisabled.Task.WaitAsync(TimeSpan.FromSeconds(30), cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	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 savedTheGame.Task.WaitAsync(cancellationToken); | ||||
| 		await savedTheGame.Task.WaitAsync(TimeSpan.FromMinutes(1), cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	public async Task EnableAutomaticSaving() { | ||||
| 		await process.SendCommand(MinecraftCommand.SaveOn, cancellationToken); | ||||
| 		await automaticSavingEnabled.Task.WaitAsync(cancellationToken); | ||||
| 		await automaticSavingEnabled.Task.WaitAsync(TimeSpan.FromMinutes(1), cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	private void OnOutput(object? sender, string? line) { | ||||
| @@ -59,19 +78,19 @@ sealed partial class BackupServerCommandDispatcher : IDisposable { | ||||
| 		string info = match.Groups[1].Value; | ||||
|  | ||||
| 		if (!automaticSavingDisabled.Task.IsCompleted) { | ||||
| 			if (info == "Automatic saving is now disabled") { | ||||
| 			if (AutomaticSavingDisabledMessages.Contains(info)) { | ||||
| 				logger.Debug("Detected that automatic saving is disabled."); | ||||
| 				automaticSavingDisabled.SetResult(); | ||||
| 			} | ||||
| 		} | ||||
| 		else if (!savedTheGame.Task.IsCompleted) { | ||||
| 			if (info == "Saved the game") { | ||||
| 			if (SavedTheGameMessages.Contains(info)) { | ||||
| 				logger.Debug("Detected that the game is saved."); | ||||
| 				savedTheGame.SetResult(); | ||||
| 			} | ||||
| 		} | ||||
| 		else if (!automaticSavingEnabled.Task.IsCompleted) { | ||||
| 			if (info == "Automatic saving is now enabled") { | ||||
| 			if (AutomaticSavingEnabledMessages.Contains(info)) { | ||||
| 				logger.Debug("Detected that automatic saving is enabled."); | ||||
| 				automaticSavingEnabled.SetResult(); | ||||
| 			} | ||||
|   | ||||
| @@ -135,7 +135,12 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> { | ||||
| 			return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning); | ||||
| 		} | ||||
| 		else { | ||||
| 			return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken); | ||||
| 			SetAndReportStatus(InstanceStatus.BackingUp); | ||||
| 			try { | ||||
| 				return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken); | ||||
| 			} finally { | ||||
| 				SetAndReportStatus(InstanceStatus.Running); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1,15 +1,16 @@ | ||||
| namespace Phantom.Common.Data.Backups; | ||||
|  | ||||
| public enum BackupCreationResultKind : byte { | ||||
| 	UnknownError, | ||||
| 	Success, | ||||
| 	InstanceNotRunning, | ||||
| 	BackupCancelled, | ||||
| 	BackupAlreadyRunning, | ||||
| 	BackupFileAlreadyExists, | ||||
| 	CouldNotCreateBackupFolder, | ||||
| 	CouldNotCopyWorldToTemporaryFolder, | ||||
| 	CouldNotCreateWorldArchive | ||||
| 	UnknownError = 0, | ||||
| 	Success = 1, | ||||
| 	InstanceNotRunning = 2, | ||||
| 	BackupTimedOut = 3, | ||||
| 	BackupCancelled = 4, | ||||
| 	BackupAlreadyRunning = 5, | ||||
| 	BackupFileAlreadyExists = 6, | ||||
| 	CouldNotCreateBackupFolder = 7, | ||||
| 	CouldNotCopyWorldToTemporaryFolder = 8, | ||||
| 	CouldNotCreateWorldArchive = 9 | ||||
| } | ||||
|  | ||||
| public static class BackupCreationResultSummaryExtensions { | ||||
|   | ||||
| @@ -9,9 +9,10 @@ namespace Phantom.Common.Data.Instance; | ||||
| [MemoryPackUnion(3, typeof(InstanceIsDownloading))] | ||||
| [MemoryPackUnion(4, typeof(InstanceIsLaunching))] | ||||
| [MemoryPackUnion(5, typeof(InstanceIsRunning))] | ||||
| [MemoryPackUnion(6, typeof(InstanceIsRestarting))] | ||||
| [MemoryPackUnion(7, typeof(InstanceIsStopping))] | ||||
| [MemoryPackUnion(8, typeof(InstanceIsFailed))] | ||||
| [MemoryPackUnion(6, typeof(InstanceIsBackingUp))] | ||||
| [MemoryPackUnion(7, typeof(InstanceIsRestarting))] | ||||
| [MemoryPackUnion(8, typeof(InstanceIsStopping))] | ||||
| [MemoryPackUnion(9, typeof(InstanceIsFailed))] | ||||
| public partial interface IInstanceStatus {} | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| @@ -32,6 +33,9 @@ public sealed partial record InstanceIsLaunching : IInstanceStatus; | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record InstanceIsRunning : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record InstanceIsBackingUp : IInstanceStatus; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record InstanceIsRestarting : IInstanceStatus; | ||||
|  | ||||
| @@ -46,6 +50,7 @@ public static class InstanceStatus { | ||||
| 	public static readonly IInstanceStatus NotRunning = new InstanceIsNotRunning(); | ||||
| 	public static readonly IInstanceStatus Launching = new InstanceIsLaunching(); | ||||
| 	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 Stopping = new InstanceIsStopping(); | ||||
| 	 | ||||
| @@ -58,7 +63,7 @@ public static class InstanceStatus { | ||||
| 	} | ||||
|  | ||||
| 	public static bool IsRunning(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsRunning; | ||||
| 		return status is InstanceIsRunning or InstanceIsBackingUp; | ||||
| 	} | ||||
| 	 | ||||
| 	public static bool IsStopping(this IInstanceStatus status) { | ||||
| @@ -70,10 +75,10 @@ public static class InstanceStatus { | ||||
| 	} | ||||
|  | ||||
| 	public static bool CanStop(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsDownloading or InstanceIsLaunching or InstanceIsRunning; | ||||
| 		return status.IsRunning() || status.IsLaunching(); | ||||
| 	} | ||||
|  | ||||
| 	public static bool CanSendCommand(this IInstanceStatus status) { | ||||
| 		return status is InstanceIsRunning; | ||||
| 		return status.IsRunning(); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -51,6 +51,7 @@ | ||||
|       form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence)); | ||||
|     } | ||||
|  | ||||
|     StateHasChanged(); | ||||
|     await commandInputElement.FocusAsync(preventScroll: true); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -28,6 +28,11 @@ | ||||
|     <span class="fw-semibold text-success">Running</span> | ||||
|     break; | ||||
|  | ||||
|   case InstanceIsBackingUp: | ||||
|     <div class="spinner-border" role="status"></div> | ||||
|     <span class="fw-semibold"> Backing Up</span> | ||||
|     break; | ||||
|    | ||||
|   case InstanceIsRestarting: | ||||
|     <div class="spinner-border" role="status"></div> | ||||
|     <span class="fw-semibold"> Restarting</span> | ||||
| @@ -41,6 +46,10 @@ | ||||
|   case InstanceIsFailed failed: | ||||
|     <span class="fw-semibold text-danger">Failed <sup title="@failed.Reason.ToSentence()">[?]</sup></span> | ||||
|     break; | ||||
|      | ||||
|   default: | ||||
|     <span class="fw-semibold">Unknown</span> | ||||
|     break; | ||||
| } | ||||
| </nobr> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user