using GuestOps.Web; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; var builder = Host.CreateApplicationBuilder(args); builder.Logging.ClearProviders(); builder.Logging.AddConsole(); builder.Logging.AddFilter("System.Net.Http.HttpClient", LogLevel.Warning); builder.Services.AddSingleton(); var keyPath = builder.Configuration["Keys:Path"] ?? throw new InvalidOperationException("Keys:Path is required."); builder.Services.AddDataProtection().SetApplicationName("GuestOps-Web").PersistKeysToFileSystem(new DirectoryInfo(keyPath)); builder.Services.AddHttpClient(c => c.Timeout = TimeSpan.FromSeconds(25)).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false }); builder.Services.AddTransient(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); await builder.Build().RunAsync(); sealed class MailboxWorker(IStore store, IServiceScopeFactory factory, ILogger log) : BackgroundService { readonly string owner = Guid.NewGuid().ToString("N"); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await store.Initialize(); while (!stoppingToken.IsCancellationRequested) { try { foreach (var mailbox in await store.Mailboxes()) { if (!await store.TryLease(mailbox.Id, owner)) continue; // Per-page deadline is shorter than the lease. Import is idempotent. using var deadline = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); deadline.CancelAfter(TimeSpan.FromMinutes(2)); try { using var scope = factory.CreateScope(); await scope.ServiceProvider.GetRequiredService().Sync(mailbox, deadline.Token); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { log.LogWarning("Mailbox synchronization paused for {Id} ({Type})", mailbox.Id, ex.GetType().Name); mailbox.SyncError = "Synchronization failed. Retry later or reconnect your Google mailbox."; await store.SaveSync(mailbox); } finally { await store.ReleaseLease(mailbox.Id, owner); } } } catch (Exception ex) when (!stoppingToken.IsCancellationRequested) { log.LogWarning("Worker cycle paused ({Type})", ex.GetType().Name); } await Task.Delay(TimeSpan.FromSeconds(60), stoppingToken); } } } sealed class DeliveryWorker(IStore store, IServiceScopeFactory factory, ILogger log) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { foreach (var message in await store.Deliveries()) { using var scope = factory.CreateScope(); using var deadline = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); deadline.CancelAfter(TimeSpan.FromMinutes(2)); await scope.ServiceProvider.GetRequiredService().Process(message, deadline.Token); } } catch (Exception ex) when (!stoppingToken.IsCancellationRequested) { log.LogWarning("Reply delivery cycle paused ({Type})", ex.GetType().Name); } await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); } } }