using DotBased.Logging; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Manager.App.Services; public abstract class ExtendedBackgroundService(string name, string description, ILogger logger, TimeSpan? executeInterval = null) : BackgroundService { private TaskCompletionSource _resumeSignal = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly List _actions = []; private TaskCompletionSource? _manualContinue; public ServiceState State { get; private set; } = ServiceState.Stopped; public CircularBuffer ProgressEvents { get; } = new(500); public string Name { get; } = name; public string Description { get; } = description; public TimeSpan ExecuteInterval { get; } = executeInterval ?? TimeSpan.FromSeconds(5); public IReadOnlyList Actions => _actions; protected void AddActions(IEnumerable actions) { _actions.AddRange(actions); } protected sealed override async Task ExecuteAsync(CancellationToken stoppingToken) { State = ServiceState.Running; logger.LogInformation("Initializing background service: {ServiceName}", Name); _actions.AddRange( [ new ServiceAction("Start", "Start the service (after the service is stopped of faulted.)", Start, () => State is ServiceState.Stopped or ServiceState.Faulted), new ServiceAction("Pause", "Pause the service", Pause, () => State != ServiceState.Paused), new ServiceAction("Resume", "Resume the service", Resume, () => State != ServiceState.Running) ]); await InitializeAsync(stoppingToken); while (!stoppingToken.IsCancellationRequested) { if (State == ServiceState.Running) { try { logger.LogInformation("Started running background service: {ServiceName}", Name); while (!stoppingToken.IsCancellationRequested) { if (State == ServiceState.Paused) { _resumeSignal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await _resumeSignal.Task.WaitAsync(stoppingToken); } await ExecuteServiceAsync(stoppingToken); await Task.Delay(ExecuteInterval, stoppingToken); } } catch (OperationCanceledException e) { logger.LogInformation(e, "Service {ServiceName} received cancellation", Name); } catch (Exception e) { State = ServiceState.Faulted; logger.LogError(e, "Background service {ServiceName} faulted!", Name); LogEvent("Error executing background service.", LogSeverity.Error); } finally { State = ServiceState.Stopped; } } _manualContinue = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var delayTask = Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); await Task.WhenAny(delayTask, _manualContinue.Task); _manualContinue = null; } } protected void LogEvent(string message, LogSeverity severity = LogSeverity.Info) => ProgressEvents.Add(new ServiceEvent(string.Intern(Name), message, DateTime.UtcNow, severity)); public void Start() { if (State is ServiceState.Stopped or ServiceState.Faulted) { State = ServiceState.Running; _manualContinue?.TrySetResult(); LogEvent("Started service."); } } public void Pause() { if (State == ServiceState.Running) { State = ServiceState.Paused; LogEvent("Service paused."); } } public void Resume() { if (State == ServiceState.Paused) { State = ServiceState.Running; _resumeSignal.TrySetResult(); LogEvent("Service resumed."); } } protected abstract Task InitializeAsync(CancellationToken stoppingToken); protected abstract Task ExecuteServiceAsync(CancellationToken stoppingToken); public override bool Equals(object? obj) { return obj is ExtendedBackgroundService bgService && bgService.Name.Equals(Name, StringComparison.OrdinalIgnoreCase); } public override int GetHashCode() { return Name.GetHashCode(); } } public enum ServiceState { Stopped, Faulted, Running, Paused } public record struct ServiceEvent(string Source, string Message, DateTime DateUtc, LogSeverity Severity); public record ServiceAction(string Id, string Description, Action Action, Func IsEnabled);