diff --git a/OF DL.Core/Services/DownloadService.cs b/OF DL.Core/Services/DownloadService.cs index 2a9dfcc..15228bf 100644 --- a/OF DL.Core/Services/DownloadService.cs +++ b/OF DL.Core/Services/DownloadService.cs @@ -939,7 +939,8 @@ public class DownloadService( using HttpClient client = new(); HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = new Uri(url) }; - using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, progressReporter.CancellationToken); + using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, + progressReporter.CancellationToken); response.EnsureSuccessStatusCode(); Stream body = await response.Content.ReadAsStreamAsync(progressReporter.CancellationToken); diff --git a/OF DL.Core/Services/LoggingService.cs b/OF DL.Core/Services/LoggingService.cs index a0b11b4..21389cd 100644 --- a/OF DL.Core/Services/LoggingService.cs +++ b/OF DL.Core/Services/LoggingService.cs @@ -7,8 +7,11 @@ namespace OF_DL.Services; public class LoggingService : ILoggingService { - public LoggingService() + private readonly ILogEventSink? _optionalErrorSink; + + public LoggingService(ILogEventSink? optionalErrorSink = null) { + _optionalErrorSink = optionalErrorSink; LevelSwitch = new LoggingLevelSwitch(); InitializeLogger(); } @@ -38,10 +41,17 @@ public class LoggingService : ILoggingService // Set the initial level to Error (until we've read from config) LevelSwitch.MinimumLevel = LogEventLevel.Error; - Log.Logger = new LoggerConfiguration() + LoggerConfiguration loggerConfiguration = new LoggerConfiguration() .MinimumLevel.ControlledBy(LevelSwitch) - .WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day) - .CreateLogger(); + .WriteTo.File("logs/OFDL.txt", rollingInterval: RollingInterval.Day); + + if (_optionalErrorSink != null) + { + loggerConfiguration = loggerConfiguration.WriteTo.Sink(_optionalErrorSink, + LogEventLevel.Error); + } + + Log.Logger = loggerConfiguration.CreateLogger(); Log.Debug("Logging service initialized"); } diff --git a/OF DL.Gui/Program.cs b/OF DL.Gui/Program.cs index af2df39..3f8e8b1 100644 --- a/OF DL.Gui/Program.cs +++ b/OF DL.Gui/Program.cs @@ -1,6 +1,5 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; -using Microsoft.Extensions.DependencyInjection; using OF_DL.Helpers; using OF_DL.Services; using Serilog; @@ -20,13 +19,10 @@ public static class Program // Parse command line arguments HidePrivateInfo = args.Contains("--hide-private-info", StringComparer.OrdinalIgnoreCase); - ServiceCollection services = new(); - services.AddSingleton(); - ServiceProvider tempProvider = services.BuildServiceProvider(); - ILoggingService loggingService = tempProvider.GetRequiredService(); + // Initialize the logging service to ensure that logs are written before Avalonia starts (with a new logging service) + LoggingService _ = new(); RegisterGlobalExceptionHandlers(); - Log.Information("Starting OF DL GUI"); // Check if running in Docker and print a message if (EnvironmentHelper.IsRunningInDocker()) @@ -40,7 +36,7 @@ public static class Program } catch (Exception ex) { - HandleUnhandledException(ex, "Program.Main", isTerminating: true); + HandleUnhandledException(ex, "Program.Main", true); } finally { @@ -65,7 +61,7 @@ public static class Program TaskScheduler.UnobservedTaskException += (_, eventArgs) => { HandleUnhandledException(eventArgs.Exception, "TaskScheduler.UnobservedTaskException", - isTerminating: false); + false); eventArgs.SetObserved(); }; } diff --git a/OF DL.Gui/Services/DownloadErrorLogTracking.cs b/OF DL.Gui/Services/DownloadErrorLogTracking.cs new file mode 100644 index 0000000..c09d971 --- /dev/null +++ b/OF DL.Gui/Services/DownloadErrorLogTracking.cs @@ -0,0 +1,45 @@ +using Serilog.Core; +using Serilog.Events; + +namespace OF_DL.Gui.Services; + +public sealed class DownloadErrorLogTracker +{ + private int _sessionActive; + private int _errorLoggedInSession; + + public bool IsSessionActive => Volatile.Read(ref _sessionActive) == 1; + + public void StartSession() + { + Interlocked.Exchange(ref _errorLoggedInSession, 0); + Interlocked.Exchange(ref _sessionActive, 1); + } + + public bool StopSession() + { + Interlocked.Exchange(ref _sessionActive, 0); + return Volatile.Read(ref _errorLoggedInSession) == 1; + } + + public void RecordError() + { + if (!IsSessionActive) + { + return; + } + + Interlocked.Exchange(ref _errorLoggedInSession, 1); + } +} + +internal sealed class DownloadErrorTrackingSink(DownloadErrorLogTracker tracker) : ILogEventSink +{ + public void Emit(LogEvent logEvent) + { + if (logEvent.Level >= LogEventLevel.Error) + { + tracker.RecordError(); + } + } +} diff --git a/OF DL.Gui/Services/ServiceCollectionFactory.cs b/OF DL.Gui/Services/ServiceCollectionFactory.cs index 4ce805e..101c6b9 100644 --- a/OF DL.Gui/Services/ServiceCollectionFactory.cs +++ b/OF DL.Gui/Services/ServiceCollectionFactory.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using OF_DL.Gui.ViewModels; using OF_DL.Gui.Views; using OF_DL.Services; +using Serilog.Core; namespace OF_DL.Gui.Services; @@ -11,6 +12,8 @@ internal static class ServiceCollectionFactory { IServiceCollection services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/OF DL.Gui/ViewModels/MainWindowViewModel.cs b/OF DL.Gui/ViewModels/MainWindowViewModel.cs index 64d25ad..c299f9a 100644 --- a/OF DL.Gui/ViewModels/MainWindowViewModel.cs +++ b/OF DL.Gui/ViewModels/MainWindowViewModel.cs @@ -26,7 +26,8 @@ public partial class MainWindowViewModel( IConfigService configService, IAuthService authService, IStartupService startupService, - IDownloadOrchestrationService downloadOrchestrationService) : ViewModelBase + IDownloadOrchestrationService downloadOrchestrationService, + DownloadErrorLogTracker downloadErrorLogTracker) : ViewModelBase { private enum SingleDownloadType { @@ -946,6 +947,7 @@ public partial class MainWindowViewModel( return; } + downloadErrorLogTracker.StartSession(); IsDownloading = true; _workCancellationSource?.Dispose(); _workCancellationSource = new CancellationTokenSource(); @@ -1000,18 +1002,39 @@ public partial class MainWindowViewModel( } ThrowIfStopRequested(); + if (downloadErrorLogTracker.StopSession()) + { + AppendLog( + "Errors were encountered during the download. Check the logs saved to the logs folder for details."); + } + eventHandler.OnScrapeComplete(DateTime.Now - start); } catch (OperationCanceledException) { + if (downloadErrorLogTracker.IsSessionActive) + { + downloadErrorLogTracker.StopSession(); + } + AppendLog("Operation canceled."); } catch (Exception ex) { + if (downloadErrorLogTracker.IsSessionActive) + { + downloadErrorLogTracker.StopSession(); + } + AppendLog($"Download failed: {ex.Message}"); } finally { + if (downloadErrorLogTracker.IsSessionActive) + { + downloadErrorLogTracker.StopSession(); + } + IsDownloading = false; _workCancellationSource?.Dispose(); _workCancellationSource = null; @@ -1228,6 +1251,7 @@ public partial class MainWindowViewModel( return; } + downloadErrorLogTracker.StartSession(); IsDownloading = true; _workCancellationSource?.Dispose(); _workCancellationSource = new CancellationTokenSource(); @@ -1306,18 +1330,39 @@ public partial class MainWindowViewModel( } ThrowIfStopRequested(); + if (downloadErrorLogTracker.StopSession()) + { + AppendLog( + "Errors were encountered during the download. Check the logs saved to the logs folder for details."); + } + eventHandler.OnScrapeComplete(DateTime.Now - start); } catch (OperationCanceledException) { + if (downloadErrorLogTracker.IsSessionActive) + { + downloadErrorLogTracker.StopSession(); + } + AppendLog("Operation canceled."); } catch (Exception ex) { + if (downloadErrorLogTracker.IsSessionActive) + { + downloadErrorLogTracker.StopSession(); + } + AppendLog($"Download failed: {ex.Message}"); } finally { + if (downloadErrorLogTracker.IsSessionActive) + { + downloadErrorLogTracker.StopSession(); + } + IsDownloading = false; _workCancellationSource?.Dispose(); _workCancellationSource = null;