using System.Extensions; using System.Extensions.Core; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Robin.Options; using Robin.Services.Persistence; namespace Robin.Services.Monitoring; public sealed class MonitoringService( PositionPersistenceService persistenceService, IOptions options, ILogger logger ) : IHostedLifecycleService { private readonly PositionPersistenceService persistenceService = persistenceService; private readonly MonitorOptions options = options.Value; private readonly CancellationTokenSource cancellationTokenSource = new(); private readonly ILogger logger = logger; private readonly List watchers = []; public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StartedAsync(CancellationToken cancellationToken) => this.MonitorFiles(cancellationToken); public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => this.CancelMonitors(); public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task CancelMonitors() { this.cancellationTokenSource.Cancel(); return Task.WhenAll(this.watchers); } public async Task MonitorFiles(CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(); scopedLogger.LogDebug( "Starting file monitoring service. Parsing provided paths {paths}", string.Join(", ", this.options.Paths) ); var expandedPaths = ExpandPaths(this.options.Paths); scopedLogger.LogInformation( "Monitoring the following files: {files}", string.Join(", ", expandedPaths) ); var cts = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, this.cancellationTokenSource.Token ); foreach (var filePath in expandedPaths) { this.watchers.Add(Task.Run(() => this.MonitorFile(filePath, cts.Token), cts.Token)); } } private async Task MonitorFile(string filePath, CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(); if (!File.Exists(filePath)) { scopedLogger.LogWarning("File does not exist: {filePath}", filePath); return; } scopedLogger.LogInformation("Starting to monitor file: {filePath}", filePath); var previousPosition = this.persistenceService.GetPosition(filePath); using var fs = new FileStream( filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete ); if (fs.Length < previousPosition) { previousPosition = 0; } if (previousPosition > 0) { fs.Seek(previousPosition, SeekOrigin.Begin); } using var reader = new StreamReader(fs); var line = string.Empty; while ((line = await reader.ReadLineAsync(cancellationToken))?.IsNullOrEmpty() is false) { scopedLogger.LogDebug("Read line: {line}", line); //TODO: Publish to IPublishingService await this.persistenceService.SavePositionAsync( filePath, fs.Position, cancellationToken ); } } private static List ExpandPaths(List? patterns) { if (patterns is null || patterns.Count == 0) { return []; } var results = new List(); foreach (var pattern in patterns) { var directory = Path.GetDirectoryName(pattern); var filePattern = Path.GetFileName(pattern); if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) { continue; } results.AddRange(Directory.GetFiles(directory, filePattern)); } return results; } }