1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2025-05-12 14:34:03 +02:00

Restore chunk saving immediately after archiving the backup

This commit is contained in:
chylex 2023-02-27 02:49:14 +01:00
parent 2b4fa2c902
commit 8f003c6351
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
3 changed files with 123 additions and 98 deletions

View File

@ -44,7 +44,7 @@ sealed class BackupArchiver {
return false;
}
public async Task ArchiveWorld(BackupCreationResult.Builder resultBuilder) {
public async Task<string?> ArchiveWorld(BackupCreationResult.Builder resultBuilder) {
string guid = instanceProperties.InstanceGuid.ToString();
string currentDateTime = DateTime.Now.ToString("yyyyMMdd-HHmmss");
string backupFolderPath = Path.Combine(destinationBasePath, guid);
@ -53,7 +53,7 @@ sealed class BackupArchiver {
if (File.Exists(backupFilePath)) {
resultBuilder.Kind = BackupCreationResultKind.BackupFileAlreadyExists;
logger.Warning("Skipping backup, file already exists: {File}", backupFilePath);
return;
return null;
}
try {
@ -61,18 +61,16 @@ sealed class BackupArchiver {
} catch (Exception e) {
resultBuilder.Kind = BackupCreationResultKind.CouldNotCreateBackupFolder;
logger.Error(e, "Could not create backup folder: {Folder}", backupFolderPath);
return;
return null;
}
string temporaryFolderPath = Path.Combine(temporaryBasePath, guid + "_" + currentDateTime);
if (!await CopyWorldAndCreateTarArchive(temporaryFolderPath, backupFilePath, resultBuilder)) {
return;
return null;
}
var compressedFilePath = await BackupCompressor.Compress(backupFilePath, cancellationToken);
if (compressedFilePath == null) {
resultBuilder.Warnings |= BackupCreationWarnings.CouldNotCompressWorldArchive;
}
logger.Verbose("Created world backup: {FilePath}", backupFilePath);
return backupFilePath;
}
private async Task<bool> CopyWorldAndCreateTarArchive(string temporaryFolderPath, string backupFilePath, BackupCreationResult.Builder resultBuilder) {

View File

@ -1,13 +1,11 @@
using System.Text.RegularExpressions;
using Phantom.Agent.Minecraft.Command;
using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Instance;
using Phantom.Common.Data.Backups;
using Phantom.Common.Logging;
using Serilog;
namespace Phantom.Agent.Services.Backups;
sealed partial class BackupManager {
sealed class BackupManager {
private readonly string destinationBasePath;
private readonly string temporaryBasePath;
@ -40,7 +38,6 @@ sealed partial class BackupManager {
private readonly string loggerName;
private readonly ILogger logger;
private readonly InstanceProcess process;
private readonly BackupCommandListener listener;
private readonly CancellationToken cancellationToken;
public BackupCreator(string destinationBasePath, string temporaryBasePath, string loggerName, InstanceProcess process, CancellationToken cancellationToken) {
@ -49,52 +46,44 @@ sealed partial class BackupManager {
this.loggerName = loggerName;
this.logger = PhantomLogger.Create<BackupManager>(loggerName);
this.process = process;
this.listener = new BackupCommandListener(logger);
this.cancellationToken = cancellationToken;
}
public async Task<BackupCreationResult> CreateBackup() {
logger.Information("Backup started.");
process.AddOutputListener(listener.OnOutput, maxLinesToReadFromHistory: 0);
try {
var resultBuilder = new BackupCreationResult.Builder();
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 {
process.RemoveOutputListener(listener.OnOutput);
var resultBuilder = new BackupCreationResult.Builder();
string? backupFilePath;
using (var dispatcher = new BackupServerCommandDispatcher(logger, process, cancellationToken)) {
backupFilePath = await CreateWorldArchive(dispatcher, resultBuilder);
}
if (backupFilePath != null) {
await CompressWorldArchive(backupFilePath, resultBuilder);
}
var result = resultBuilder.Build();
LogBackupResult(result);
return result;
}
private async Task RunBackupProcedure(BackupCreationResult.Builder resultBuilder) {
private async Task<string?> CreateWorldArchive(BackupServerCommandDispatcher dispatcher, BackupCreationResult.Builder resultBuilder) {
try {
await DisableAutomaticSaving();
await SaveAllChunks();
await new BackupArchiver(destinationBasePath, temporaryBasePath, loggerName, process.InstanceProperties, cancellationToken).ArchiveWorld(resultBuilder);
await dispatcher.DisableAutomaticSaving();
await dispatcher.SaveAllChunks();
return await new BackupArchiver(destinationBasePath, temporaryBasePath, loggerName, process.InstanceProperties, cancellationToken).ArchiveWorld(resultBuilder);
} catch (OperationCanceledException) {
resultBuilder.Kind = BackupCreationResultKind.BackupCancelled;
logger.Warning("Backup creation was cancelled.");
return null;
} catch (Exception e) {
resultBuilder.Kind = BackupCreationResultKind.UnknownError;
logger.Error(e, "Caught exception while creating an instance backup.");
return null;
} finally {
try {
await EnableAutomaticSaving();
await dispatcher.EnableAutomaticSaving();
} catch (OperationCanceledException) {
// ignore
} catch (Exception e) {
@ -104,66 +93,25 @@ sealed partial class BackupManager {
}
}
private async Task DisableAutomaticSaving() {
await process.SendCommand(MinecraftCommand.SaveOff, cancellationToken);
await listener.AutomaticSavingDisabled.Task.WaitAsync(cancellationToken);
private async Task CompressWorldArchive(string filePath, BackupCreationResult.Builder resultBuilder) {
var compressedFilePath = await BackupCompressor.Compress(filePath, cancellationToken);
if (compressedFilePath == null) {
resultBuilder.Warnings |= BackupCreationWarnings.CouldNotCompressWorldArchive;
}
}
private 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 listener.SavedTheGame.Task.WaitAsync(cancellationToken);
}
private async Task EnableAutomaticSaving() {
await process.SendCommand(MinecraftCommand.SaveOn, cancellationToken);
await listener.AutomaticSavingEnabled.Task.WaitAsync(cancellationToken);
}
}
private sealed partial class BackupCommandListener {
[GeneratedRegex(@"^\[(?:.*?)\] \[Server thread/INFO\]: (.*?)$", RegexOptions.NonBacktracking)]
private static partial Regex ServerThreadInfoRegex();
private readonly ILogger logger;
public BackupCommandListener(ILogger logger) {
this.logger = logger;
}
public TaskCompletionSource AutomaticSavingDisabled { get; } = new ();
public TaskCompletionSource SavedTheGame { get; } = new ();
public TaskCompletionSource AutomaticSavingEnabled { get; } = new ();
public void OnOutput(object? sender, string? line) {
if (line == null) {
private void LogBackupResult(BackupCreationResult result) {
if (result.Kind != BackupCreationResultKind.Success) {
logger.Warning("Backup failed: {Reason}", result.Kind.ToSentence());
return;
}
var match = ServerThreadInfoRegex().Match(line);
if (!match.Success) {
return;
var warningCount = result.Warnings.Count();
if (warningCount > 0) {
logger.Warning("Backup finished with {Warnings} warning(s).", warningCount);
}
string info = match.Groups[1].Value;
if (!AutomaticSavingDisabled.Task.IsCompleted) {
if (info == "Automatic saving is now disabled") {
logger.Verbose("Detected that automatic saving is disabled.");
AutomaticSavingDisabled.SetResult();
}
}
else if (!SavedTheGame.Task.IsCompleted) {
if (info == "Saved the game") {
logger.Verbose("Detected that the game is saved.");
SavedTheGame.SetResult();
}
}
else if (!AutomaticSavingEnabled.Task.IsCompleted) {
if (info == "Automatic saving is now enabled") {
logger.Verbose("Detected that automatic saving is enabled.");
AutomaticSavingEnabled.SetResult();
}
else {
logger.Information("Backup finished successfully.");
}
}
}

View File

@ -0,0 +1,79 @@
using System.Text.RegularExpressions;
using Phantom.Agent.Minecraft.Command;
using Phantom.Agent.Minecraft.Instance;
using Serilog;
namespace Phantom.Agent.Services.Backups;
sealed partial class BackupServerCommandDispatcher : IDisposable {
[GeneratedRegex(@"^\[(?:.*?)\] \[Server thread/INFO\]: (.*?)$", RegexOptions.NonBacktracking)]
private static partial Regex ServerThreadInfoRegex();
private readonly ILogger logger;
private readonly InstanceProcess process;
private readonly CancellationToken cancellationToken;
private readonly TaskCompletionSource automaticSavingDisabled = new ();
private readonly TaskCompletionSource savedTheGame = new ();
private readonly TaskCompletionSource automaticSavingEnabled = new ();
public BackupServerCommandDispatcher(ILogger logger, InstanceProcess process, CancellationToken cancellationToken) {
this.logger = logger;
this.process = process;
this.cancellationToken = cancellationToken;
this.process.AddOutputListener(OnOutput, maxLinesToReadFromHistory: 0);
}
void IDisposable.Dispose() {
process.RemoveOutputListener(OnOutput);
}
public async Task DisableAutomaticSaving() {
await process.SendCommand(MinecraftCommand.SaveOff, cancellationToken);
await automaticSavingDisabled.Task.WaitAsync(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);
}
public async Task EnableAutomaticSaving() {
await process.SendCommand(MinecraftCommand.SaveOn, cancellationToken);
await automaticSavingEnabled.Task.WaitAsync(cancellationToken);
}
private void OnOutput(object? sender, string? line) {
if (line == null) {
return;
}
var match = ServerThreadInfoRegex().Match(line);
if (!match.Success) {
return;
}
string info = match.Groups[1].Value;
if (!automaticSavingDisabled.Task.IsCompleted) {
if (info == "Automatic saving is now disabled") {
logger.Verbose("Detected that automatic saving is disabled.");
automaticSavingDisabled.SetResult();
}
}
else if (!savedTheGame.Task.IsCompleted) {
if (info == "Saved the game") {
logger.Verbose("Detected that the game is saved.");
savedTheGame.SetResult();
}
}
else if (!automaticSavingEnabled.Task.IsCompleted) {
if (info == "Automatic saving is now enabled") {
logger.Verbose("Detected that automatic saving is enabled.");
automaticSavingEnabled.SetResult();
}
}
}
}