using JournalBot.Shared; namespace JournalBot.Service; public sealed class Worker : BackgroundService { private readonly ILogger _log; private readonly BotRunner _runner; private readonly TimeSpan _interval; private readonly string _serviceLogPath; public Worker(ILogger log, IConfiguration config) { _log = log; var py = config["JournalBot:PythonExe"]!; var workingDir = config["JournalBot:WorkingDir"]!; var minutes = config.GetValue("JournalBot:IntervalMinutes", 15); _runner = new BotRunner(py, workingDir); _interval = TimeSpan.FromMinutes(minutes); _serviceLogPath = Path.Combine(workingDir, "runtime", "logs", "service.log"); Directory.CreateDirectory(Path.GetDirectoryName(_serviceLogPath)!); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Run once at startup, then every interval. await RunIngestAsync(stoppingToken); using var timer = new PeriodicTimer(_interval); while (await timer.WaitForNextTickAsync(stoppingToken)) await RunIngestAsync(stoppingToken); } private async Task RunIngestAsync(CancellationToken ct) { try { var result = await _runner.RunAsync("ingest", ct); var line = $"[{DateTimeOffset.Now:O}] ingest exit={result.ExitCode}"; _log.LogInformation("{Line}", line); await AppendLogAsync(line + "\n" + result.Output); } catch (OperationCanceledException) { throw; } catch (Exception ex) { _log.LogError(ex, "ingest run failed"); await AppendLogAsync($"[{DateTimeOffset.Now:O}] ingest ERROR: {ex.Message}"); } } private async Task AppendLogAsync(string text) { try { await File.AppendAllTextAsync(_serviceLogPath, text + "\n", System.Text.Encoding.UTF8); } catch { /* logging must never crash the service */ } } }