namespace PrivaPub.Infrastructure.Backup
{
// What Program asks before migrations: whether a restore waits, and what this process does about it. The service carries
// it out; a command waits for it (exit 75, EX_TEMPFAIL), except asking how it went.
public static class MaintenanceGate
{
/// The exit code that stops systemd restarting the service (RestartPreventExitStatus=75): a restore gave up.
public const int TryLater = 75;
/// Null to go on booting; else the code to exit with.
public static async Task Enter(IServiceProvider services, string[] command, CancellationToken token)
{
var backups = services.GetRequiredService();
var logger = services.GetRequiredService().CreateLogger(nameof(MaintenanceGate));
if (command != default)
{
var waiting = RestoreMarker.Read(backups.Root);
if (waiting == default || command is ["restore", ..])
return default;
Console.Error.WriteLine($"the restore of {waiting.Backup} is {waiting.State}{(waiting.Error == default ? string.Empty : $" ({waiting.Error})")}: try again once the service has carried it out (PrivaPub admin restore --status)");
return TryLater;
}
return await ServerRestore.ApplyPending(backups.Context(), logger, token) switch
{
RestoreOutcome.Failed => 1,//systemd starts it again, and the next attempt redoes everything
RestoreOutcome.GaveUp => TryLater,
_ => (int?)null
};
}
}
// A restore asked for while the service runs (from the administrator's page or the CLI): the service stops within
// seconds, systemd starts it again, and the restore runs before anything else.
public class RestoreWatcher(Backups backups, IHostApplicationLifetime lifetime, ILogger logger) : BackgroundService
{
static readonly TimeSpan Interval = TimeSpan.FromSeconds(3);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await Task.Delay(Interval, stoppingToken);
}
catch (OperationCanceledException)
{
return;
}
if (RestoreMarker.Read(backups.Root) is { State: "pending" } marker)
{
logger.LogWarning("A restore of {Backup} was asked for by {RequestedBy}: stopping, to restore it at the next start", marker.Backup, marker.RequestedBy);
lifetime.StopApplication();
return;
}
}
}
}
}