Use a logging sync to ensure GUI users are aware of any errors that occur while downloading

This commit is contained in:
whimsical-c4lic0 2026-02-27 00:01:07 -06:00
parent 49cddd0608
commit f1d3ac7ea3
6 changed files with 114 additions and 14 deletions

View File

@ -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);

View File

@ -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");
}

View File

@ -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<ILoggingService, LoggingService>();
ServiceProvider tempProvider = services.BuildServiceProvider();
ILoggingService loggingService = tempProvider.GetRequiredService<ILoggingService>();
// 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();
};
}

View File

@ -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();
}
}
}

View File

@ -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<DownloadErrorLogTracker>();
services.AddSingleton<ILogEventSink, DownloadErrorTrackingSink>();
services.AddSingleton<ILoggingService, LoggingService>();
services.AddSingleton<IConfigService, ConfigService>();
services.AddSingleton<IAuthService, AuthService>();

View File

@ -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;