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; } } } } }