using System.Extensions.Core; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Robin.Options; namespace Robin.Services; public sealed class MonitoringService( IOptions options, ILogger logger ) : IHostedLifecycleService { private readonly MonitorOptions options = options.Value; 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) => Task.CompletedTask; public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; 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) ); foreach (var filePath in expandedPaths) { var watcher = CreateWatcher(filePath); if (watcher is null) { scopedLogger.LogWarning("Failed to watch file: {file}", filePath); continue; } this.watchers.Add(watcher); scopedLogger.LogDebug("Watching file: {file}", filePath); } } private FileSystemWatcher? CreateWatcher(string filePath) { var scpoedLogger = this.logger.CreateScopedLogger(); var directory = Path.GetDirectoryName(filePath); var fileName = Path.GetFileName(filePath); if (directory is null || fileName is null) { scpoedLogger.LogWarning("Invalid file path: {filePath}", filePath); return default; } var watcher = new FileSystemWatcher(directory, fileName) { NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size, EnableRaisingEvents = true, }; watcher.Changed += this.OnFileChanged; watcher.Error += this.OnWatcherError; return watcher; } private void OnFileChanged(object sender, FileSystemEventArgs e) { var scopedLogger = this.logger.CreateScopedLogger(); scopedLogger.LogDebug("File changed: {file}", e.FullPath); // TODO:: Read changes and push to Elastic } private void OnWatcherError(object sender, ErrorEventArgs e) { var scopedLogger = this.logger.CreateScopedLogger(); scopedLogger.LogError(e.GetException(), "Watcher error"); } public void Dispose() { foreach (var watcher in this.watchers) { watcher.Changed -= this.OnFileChanged; watcher.Error -= this.OnWatcherError; watcher.Dispose(); } this.watchers.Clear(); } 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; } }