Skip to content

Commit

Permalink
Add command to restore a full backup
Browse files Browse the repository at this point in the history
  • Loading branch information
Equinox- committed Sep 10, 2023
1 parent 55e28f8 commit 8d4155c
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 52 deletions.
2 changes: 1 addition & 1 deletion Meds.Watchdog/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class Configuration : InstallConfiguration, IEquatable<Configuration>
/// Timeout for server readiness after startup, in seconds.
/// </summary>
[XmlElement]
public double ReadinessTimeout = 60 * 5;
public double ReadinessTimeout = 60 * 15;

/// <summary>
/// In-game channel to send status change messages to.
Expand Down
5 changes: 4 additions & 1 deletion Meds.Watchdog/DataStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public sealed class DataStoreData

[XmlElement]
public GridDatabaseConfig GridDatabase = new GridDatabaseConfig();

[XmlElement]
public LifecycleStateSerialized LifecycleState;
}

public sealed class DataStore : BackgroundService
Expand Down Expand Up @@ -147,7 +150,7 @@ public void Update<T>(ref T value, T newValue) where T : IEquatable<T>
{
// ReSharper disable once HeapView.PossibleBoxingAllocation
if (!_updated)
_updated = typeof(T).IsValueType ? newValue.Equals(value) : Equals(newValue, value);
_updated = typeof(T).IsValueType ? !newValue.Equals(value) : !Equals(newValue, value);
value = newValue;
}

Expand Down
117 changes: 112 additions & 5 deletions Meds.Watchdog/Discord/DiscordCmdSave.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
Expand All @@ -12,6 +13,7 @@
using Meds.Shared;
using Meds.Shared.Data;
using Meds.Watchdog.Save;
using Microsoft.Extensions.Logging;

// ReSharper disable HeapView.ClosureAllocation
// ReSharper disable HeapView.ObjectAllocation
Expand All @@ -22,22 +24,27 @@ namespace Meds.Watchdog.Discord
// Not using command groups for discrete permissions.
public class DiscordCmdSave : ApplicationCommandModule
{
private readonly ILogger<DiscordCmdSave> _log;
private readonly HealthTracker _healthTracker;
private readonly ISubscriber<SaveResponse> _saveResponse;
private readonly IPublisher<SaveRequest> _saveRequest;
private readonly SaveFiles _saves;
private readonly DataStore _dataStore;
private readonly ISubscriber<RestoreSceneResponse> _restoreSceneResponse;
private readonly IPublisher<RestoreSceneRequest> _restoreSceneRequest;
private readonly LifecycleController _lifecycle;

public DiscordCmdSave(HealthTracker healthTracker, ISubscriber<SaveResponse> saveResponse, IPublisher<SaveRequest> saveRequest, SaveFiles saves,
DataStore dataStore, ISubscriber<RestoreSceneResponse> restoreSceneResponse, IPublisher<RestoreSceneRequest> restoreSceneRequest)
DataStore dataStore, ISubscriber<RestoreSceneResponse> restoreSceneResponse, IPublisher<RestoreSceneRequest> restoreSceneRequest,
LifecycleController lifecycle, ILogger<DiscordCmdSave> log)
{
_healthTracker = healthTracker;
_saveResponse = saveResponse;
_saveRequest = saveRequest;
_restoreSceneResponse = restoreSceneResponse;
_restoreSceneRequest = restoreSceneRequest;
_lifecycle = lifecycle;
_log = log;
_saves = saves;
_dataStore = dataStore;
}
Expand Down Expand Up @@ -209,14 +216,14 @@ public async Task Bisect(
foreach (var saveFile in saves)
{
using var save = saveFile.Open();
long size;
SaveEntryInfo info;
var isPresent = target switch
{
BisectObjectType.Entity => save.TryGetEntityFileInfo(objectId, out size),
BisectObjectType.Group => save.TryGetGroupFileInfo(objectId, out size),
BisectObjectType.Entity => save.TryGetEntityFileInfo(objectId, out info),
BisectObjectType.Group => save.TryGetGroupFileInfo(objectId, out info),
_ => throw new ArgumentOutOfRangeException(nameof(target), target, null)
};
var curr = new BisectState(isPresent, size, saveFile);
var curr = new BisectState(isPresent, info.Size, saveFile);
var sizeChange = Math.Abs(prevRecorded.Size - curr.Size);
if (curr.Present != prevRecorded.Present || (sizeChange > sizeChangeThreshold && sizeChange > curr.Size * sizeChangeFactor))
{
Expand Down Expand Up @@ -531,6 +538,106 @@ await context.EditResponseAsync(
}
}

[SlashCommand("save-restore", "Restores an entire backup save")]
[SlashCommandPermissions(Permissions.Administrator)]
public async Task RestoreFull(InteractionContext context,
[Option("save", "Save file name from /save list")] [Autocomplete(typeof(AllSaveFilesAutoCompleter))]
string saveName)
{
await context.CreateResponseAsync($"Loading save `{saveName}`...");
if (!_saves.TryOpenSave(saveName, out var saveFile))
{
await context.EditResponseAsync($"Failed to load save `{saveFile}`");
return;
}

if (_lifecycle.Active.State != LifecycleStateCase.Shutdown)
{
await context.EditResponseAsync("Server must be shutdown to restore a backup");
return;
}

using var token = _lifecycle.PinState();
await context.EditResponseAsync("Preventing server from restarting...");
await token.Task;
if (_lifecycle.Active.State != LifecycleStateCase.Shutdown)
{
await context.EditResponseAsync("Failed to prevent server from restarting");
return;
}

if (!_saves.TryOpenLiveSave(out var liveSave))
{
await context.EditResponseAsync("Failed to open the live save file");
return;
}

// Trash everything except the backup folder
var archivePath = _saves.GetArchivePath("pre-restore");
await ArchiveCurrentSaveFiles(context, liveSave, archivePath);

// Restore files in backup
await RestoreSaveFiles(context, saveFile, liveSave);
}

private static async Task ArchiveCurrentSaveFiles(InteractionContext context, SaveFile saveFile, string archivePath)
{
using var saveAccess = saveFile.Open();
var filesToArchive = saveAccess.AllFiles().ToList();

using (var archive = new ZipArchive(new FileStream(archivePath, FileMode.CreateNew), ZipArchiveMode.Create))
{
await context.EditResponseAsync("Archiving current save files...");
var progress = new ProgressReporter(context, "Archiving current save files");
for (var i = 0; i < filesToArchive.Count; i++)
{
var srcEntry = filesToArchive[i];
var destEntry = archive.CreateEntry(srcEntry.RelativePath);
using var src = srcEntry.Open();
using var dst = destEntry.Open();
await src.CopyToAsync(dst);
progress.Reporter(filesToArchive.Count, i + 1, 0);
}

await progress.Finish("Archived current save files");
}

{
await context.EditResponseAsync("Deleting current save files...");
var progress = new ProgressReporter(context, "Deleting current save files");
for (var i = 0; i < filesToArchive.Count; i++)
{
var srcEntry = filesToArchive[i];
File.Delete(Path.Combine(saveFile.SavePath, srcEntry.RelativePath));
progress.Reporter(filesToArchive.Count, i + 1, 0);
}

await progress.Finish("Deleted current save files");
}
}

private static async Task RestoreSaveFiles(InteractionContext context, SaveFile saveFile, SaveFile destSave)
{
await context.EditResponseAsync("Restoring backup save files...");
var progress = new ProgressReporter(context, "Restoring backup save files");
using var saveAccess = saveFile.Open();
var filesToRestore = saveAccess.AllFiles().ToList();
for (var i = 0; i < filesToRestore.Count; i++)
{
var srcEntry = filesToRestore[i];
var destPath = Path.Combine(destSave.SavePath, srcEntry.RelativePath);
var destDir = Path.GetDirectoryName(destPath) ?? throw new Exception();
if (!Directory.Exists(destDir))
Directory.CreateDirectory(destDir);
using var src = srcEntry.Open();
using var dst = new FileStream(destPath, FileMode.CreateNew);
await src.CopyToAsync(dst);
progress.Reporter(filesToRestore.Count, i + 1, 0);
}

await progress.Finish("Restored backup save files");
}

private static string RenderEntityList(
SaveFileAccessor save,
IEnumerable<EntityId> entities,
Expand Down
6 changes: 3 additions & 3 deletions Meds.Watchdog/Discord/DiscordCmdSaveSearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ bool MatchFormatter(Match match, string[] row)
results.Select(match => (match.ObjectId, (IEnumerable<Match>)match.Matches)),
objectHeaders, objectFormatter,
matchHeaders, MatchFormatter);
await progress.ReportingTask;
await progress.Finish();
if (formatted.RowCount > 1)
await context.RespondLongText(formatted.Lines());
else
Expand Down Expand Up @@ -182,14 +182,14 @@ bool TryParseArea(out string desc, out SaveFileGeoSearch.LodSearch search)
var entities = SaveFileGeoSearch.Entities(save, lodSearch);
objectCount = entities.Count;
objectIds = entities
.OrderByDescending(x => save.TryGetEntityFileInfo(x, out var length) ? length : 0)
.OrderByDescending(x => save.TryGetEntityFileInfo(x, out var info) ? info.Size : 0)
.Select(x => x.Value);
break;
case SearchObjectType.Group:
var groups = SaveFileGeoSearch.Groups(save, lodSearch);
objectCount = groups.Count;
objectIds = groups
.OrderByDescending(x => save.TryGetGroupFileInfo(x, out var length) ? length : 0)
.OrderByDescending(x => save.TryGetGroupFileInfo(x, out var info) ? info.Size : 0)
.Select(x => x.Value);
break;
default:
Expand Down
36 changes: 30 additions & 6 deletions Meds.Watchdog/Discord/DiscordSaveFileUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public abstract class SaveFilesAutoCompleter : DiscordAutoCompleter<SaveFile>
}

protected override string FormatData(string key, SaveFile data) => key;

protected override string FormatArgument(SaveFile data) => data.SaveName;
}

public sealed class AllSaveFilesAutoCompleter : SaveFilesAutoCompleter
Expand All @@ -35,17 +37,34 @@ public sealed class AutomaticSaveFilesAutoCompleter : SaveFilesAutoCompleter
public sealed class ProgressReporter
{
private static readonly long ReportInterval = (long)TimeSpan.FromSeconds(5).TotalSeconds * Stopwatch.Frequency;
private long _lastReporting = Stopwatch.GetTimestamp() + ReportInterval;
private volatile Task _reportingTask = Task.CompletedTask;
private readonly InteractionContext _ctx;
private readonly string _prefix;
private int _total, _successful, _failed;

public Task ReportingTask => _reportingTask;
private long _lastReporting = Stopwatch.GetTimestamp();
private volatile Task _reportingTask = Task.CompletedTask;

public readonly DelReportProgress Reporter;

public ProgressReporter(InteractionContext ctx)
private string Message(string prefix = null)
{
lock (this)
{
var total = _total;
var complete = _successful + _failed;
return $"{prefix ?? _prefix} {(total == 0 ? 100 : complete * 100 / total):D}% ({complete} / {total})";
}
}

public ProgressReporter(InteractionContext ctx, string prefix = "Processed")
{
_ctx = ctx;
_prefix = prefix;
Reporter = (total, successful, failed) =>
{
Volatile.Write(ref _total, total);
Volatile.Write(ref _successful, successful);
Volatile.Write(ref _failed, failed);
var last = Volatile.Read(ref _lastReporting);
var now = Stopwatch.GetTimestamp();
if (last + ReportInterval >= now)
Expand All @@ -54,11 +73,16 @@ public ProgressReporter(InteractionContext ctx)
return;
lock (this)
{
var complete = successful + failed;
if (!_reportingTask.IsCompleted) return;
_reportingTask = ctx.EditResponseAsync($"Processed {complete * 100 / total:D}% ({complete} / {total})");
_reportingTask = _ctx.EditResponseAsync(Message());
}
};
}

public async Task Finish(string prefix = null)
{
await _reportingTask;
await _ctx.EditResponseAsync(Message(prefix));
}
}
}
Loading

0 comments on commit 8d4155c

Please sign in to comment.