Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic exception overlay #2456

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions NexusMods.App.sln
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Networking.Steam.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Hashes", "src\Abstractions\NexusMods.Abstractions.Hashes\NexusMods.Abstractions.Hashes.csproj", "{AF703852-D7B0-4BAD-8C75-B6046C6F0490}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Logging", "src\Abstractions\NexusMods.Abstractions.Logging\NexusMods.Abstractions.Logging.csproj", "{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -752,6 +754,10 @@ Global
{AF703852-D7B0-4BAD-8C75-B6046C6F0490}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF703852-D7B0-4BAD-8C75-B6046C6F0490}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF703852-D7B0-4BAD-8C75-B6046C6F0490}.Release|Any CPU.Build.0 = Release|Any CPU
{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -885,6 +891,7 @@ Global
{24457AAA-8954-4BD6-8EB5-168EAC6EFB1B} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
{17023DB9-8E31-4397-B3E1-141149987865} = {897C4198-884F-448A-B0B0-C2A6D971EAE0}
{AF703852-D7B0-4BAD-8C75-B6046C6F0490} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9F9F8352-34DD-42C0-8564-EE9AF34A3501}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace NexusMods.Abstractions.Logging;

/// <summary>
/// A global source of exceptions that have been observed by the application.
/// This allows for UI and other systems to tap into log messages.
/// </summary>
public interface IObservableExceptionSource
{
IObservable<LogMessage> Exceptions { get; }
}
12 changes: 12 additions & 0 deletions src/Abstractions/NexusMods.Abstractions.Logging/LogMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace NexusMods.Abstractions.Logging;

/// <summary>
/// A generic log message. Exists as a record to de-couple exceptions and log messages
/// from the backend logging targets
/// </summary>
/// <param name="Exception">The attached Exception (if any)</param>
/// <param name="Message">The log's message</param>
public record LogMessage(Exception? Exception, string Message)
{

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NLog" />
<PackageReference Include="System.Reactive" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class ApplyControlViewModel : AViewModel<IApplyControlViewModel>, IApplyC
private readonly IJobMonitor _jobMonitor;

private readonly LoadoutId _loadoutId;
private readonly IOverlayController _overlayController;
private readonly IServiceProvider _serviceProvider;
private readonly GameInstallMetadataId _gameMetadataId;
[Reactive] private bool CanApply { get; set; } = true;

Expand All @@ -41,7 +41,7 @@ public class ApplyControlViewModel : AViewModel<IApplyControlViewModel>, IApplyC
public ApplyControlViewModel(LoadoutId loadoutId, IServiceProvider serviceProvider, IJobMonitor jobMonitor, IOverlayController overlayController, GameRunningTracker gameRunningTracker)
{
_loadoutId = loadoutId;
_overlayController = overlayController;
_serviceProvider = serviceProvider;
_syncService = serviceProvider.GetRequiredService<ISynchronizerService>();
_conn = serviceProvider.GetRequiredService<IConnection>();
_jobMonitor = serviceProvider.GetRequiredService<IJobMonitor>();
Expand Down Expand Up @@ -118,7 +118,7 @@ await Task.Run(async () =>
catch (ExecutableInUseException)
{
var marker = NexusMods.Abstractions.Loadouts.Loadout.Load(_conn.Db, _loadoutId);
await MessageBoxOkViewModel.ShowGameAlreadyRunningError(_overlayController, marker.Installation.Name);
await MessageBoxOkViewModel.ShowGameAlreadyRunningError(_serviceProvider, marker.Installation.Name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,16 @@ public class LaunchButtonViewModel : AViewModel<ILaunchButtonViewModel>, ILaunch
private readonly IToolManager _toolManager;
private readonly IConnection _conn;
private readonly IJobMonitor _monitor;
private readonly IOverlayController _overlayController;
private readonly IServiceProvider _serviceProvider;
private readonly GameRunningTracker _gameRunningTracker;

public LaunchButtonViewModel(ILogger<ILaunchButtonViewModel> logger, IToolManager toolManager, IConnection conn, IJobMonitor monitor, IOverlayController overlayController, GameRunningTracker gameRunningTracker)
public LaunchButtonViewModel(ILogger<ILaunchButtonViewModel> logger, IToolManager toolManager, IConnection conn, IJobMonitor monitor, IServiceProvider serviceProvider, GameRunningTracker gameRunningTracker)
{
_logger = logger;
_toolManager = toolManager;
_conn = conn;
_monitor = monitor;
_overlayController = overlayController;
_serviceProvider = serviceProvider;
_gameRunningTracker = gameRunningTracker;

this.WhenActivated(cd =>
Expand Down Expand Up @@ -72,7 +72,7 @@ await Task.Run(async () =>
}
catch (ExecutableInUseException)
{
await MessageBoxOkViewModel.ShowGameAlreadyRunningError(_overlayController, marker.Installation.Name);
await MessageBoxOkViewModel.ShowGameAlreadyRunningError(_serviceProvider, marker.Installation.Name);
}
catch (Exception ex)
{
Expand Down
1 change: 1 addition & 0 deletions src/NexusMods.App.UI/NexusMods.App.UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.Logging\NexusMods.Abstractions.Logging.csproj" />
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.NexusModsLibrary\NexusMods.Abstractions.NexusModsLibrary.csproj" />
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.Resources.Caching\NexusMods.Abstractions.Resources.Caching.csproj" />
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.Resources.IO\NexusMods.Abstractions.Resources.IO.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using NexusMods.App.UI.Controls.MarkdownRenderer;
using R3;
namespace NexusMods.App.UI.Overlays.Generic.MessageBox.Ok;

Expand All @@ -6,6 +7,19 @@ namespace NexusMods.App.UI.Overlays.Generic.MessageBox.Ok;
/// </summary>
public interface IMessageBoxOkViewModel : IOverlayViewModel<Unit>
{
/// <summary>
/// A short title for the message box.
/// </summary>
public string Title { get; set; }

/// <summary>
/// A description of what's happening.
/// </summary>
public string Description { get; set; }

/// <summary>
/// If provided, this will be displayed in a markdown control below the description. Use this
/// for more descriptive information.
/// </summary>
public IMarkdownRendererViewModel? MarkdownRenderer { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
xmlns:base="clr-namespace:NexusMods.App.UI.Overlays.Generic.MessageBox.Base"
xmlns:resources="clr-namespace:NexusMods.App.UI.Resources">

<base:MessageBoxBackground MinWidth="300" MaxWidth="450">
<base:MessageBoxBackground MinWidth="300" MaxWidth="640">
<base:MessageBoxBackground.TopContent>
<StackPanel Orientation="Vertical" Margin="24">
<StackPanel Orientation="Vertical" Margin="24" Spacing="16">

<!-- Title -->
<DockPanel HorizontalAlignment="Stretch" Margin="0,0,0,16">
Expand All @@ -30,7 +30,10 @@
</DockPanel>

<!-- Message -->
<TextBlock x:Name="MessageTextBlock" TextWrapping="WrapWithOverflow" />
<TextBlock x:Name="MessageTextBlock" TextWrapping="WrapWithOverflow" MaxLines="50" />

<!-- Supporting Markdown -->
<reactiveUi:ViewModelViewHost x:Name="MarkdownRendererViewModelViewHost" MaxHeight="640" MaxWidth="600"/>

</StackPanel>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.ReactiveUI;
using R3;
using ReactiveUI;
Expand All @@ -21,7 +22,15 @@ public MessageBoxOkView()

this.OneWayBind(ViewModel, vm => vm.Description, v => v.MessageTextBlock.Text)
.DisposeWith(disposables);


this.OneWayBind(ViewModel, vm => vm.MarkdownRenderer, v => v.MarkdownRendererViewModelViewHost.ViewModel)
.DisposeWith(disposables);

this.WhenAnyValue(view => view.ViewModel!.MarkdownRenderer)
.Select(vm => vm != null)
.BindTo(this, v => v.MarkdownRendererViewModelViewHost.IsVisible)
.DisposeWith(disposables);

// Bind commands
OkButton.Command = ReactiveCommand.Create(() =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using NexusMods.App.UI.Controls.MarkdownRenderer;
using NexusMods.App.UI.Resources;
using R3;
using ReactiveUI.Fody.Helpers;
Expand All @@ -11,15 +13,32 @@ public class MessageBoxOkViewModel : AOverlayViewModel<IMessageBoxOkViewModel, U
[Reactive]
public string Description { get; set; } = "This is some very long design only text that spans multiple lines!! This text is super cool!!";

[Reactive] public required IMarkdownRendererViewModel? MarkdownRenderer { get; set; }

/// <summary>
/// Shows the 'Game is already Running' error when you try to synchronize and a game is already running (usually on Windows).
/// </summary>
public static async Task ShowGameAlreadyRunningError(IOverlayController overlayController, string gameName)
public static Task ShowGameAlreadyRunningError(IServiceProvider serviceProvider, string gameName)
{
var viewModel = new MessageBoxOkViewModel()
return Show(serviceProvider, Language.ErrorGameAlreadyRunning_Title, string.Format(Language.ErrorGameAlreadyRunning_Description, gameName));
}

public static async Task Show(IServiceProvider serviceProvider, string title, string description, string? markdown = null)
{
var overlayController = serviceProvider.GetRequiredService<IOverlayController>();

IMarkdownRendererViewModel? markdownRenderer = null;
if (markdown != null)
{
markdownRenderer = serviceProvider.GetRequiredService<IMarkdownRendererViewModel>();
markdownRenderer.Contents = markdown;
}

var viewModel = new MessageBoxOkViewModel
{
Title = Language.ErrorGameAlreadyRunning_Title,
Description = string.Format(Language.ErrorGameAlreadyRunning_Description, gameName) ,
Title = title,
Description = description,
MarkdownRenderer = markdownRenderer,
};
await overlayController.EnqueueAndWait(viewModel);
}
Expand Down
36 changes: 34 additions & 2 deletions src/NexusMods.App.UI/Windows/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Microsoft.Extensions.DependencyInjection;
using NexusMods.Abstractions.Logging;
using NexusMods.Abstractions.NexusWebApi;
using NexusMods.Abstractions.UI;
using NexusMods.App.UI.Controls.DevelopmentBuildBanner;
Expand All @@ -11,6 +12,7 @@
using NexusMods.App.UI.LeftMenu;
using NexusMods.App.UI.Overlays;
using NexusMods.App.UI.Overlays.AlphaWarning;
using NexusMods.App.UI.Overlays.Generic.MessageBox.Ok;
using NexusMods.App.UI.Overlays.Login;
using NexusMods.App.UI.Overlays.MetricsOptIn;
using NexusMods.App.UI.Overlays.Updater;
Expand Down Expand Up @@ -48,9 +50,12 @@ public MainWindowViewModel(

Spine = serviceProvider.GetRequiredService<ISpineViewModel>();
DevelopmentBuildBanner = serviceProvider.GetRequiredService<IDevelopmentBuildBannerViewModel>();

this.WhenActivated(d =>
{
ConnectErrors(serviceProvider)
.DisposeWith(d);

var alphaWarningViewModel = serviceProvider.GetRequiredService<IAlphaWarningViewModel>();
alphaWarningViewModel.WorkspaceController = WorkspaceController;
alphaWarningViewModel.Controller = overlayController;
Expand Down Expand Up @@ -103,7 +108,34 @@ public MainWindowViewModel(
}
});
}


private IDisposable ConnectErrors(IServiceProvider provider)
{
var source = provider.GetService<IObservableExceptionSource>();
if (source is null)
return Disposable.Empty;

return source.Exceptions
.Subscribe(msg =>
{
var title = "Unhandled Exception";
var description = msg.Message;
string? details = null;
if (msg.Exception != null)
{
details = $"""
### Exception Details
```
{msg.Exception}
```
""";
}

Task.Run(() => MessageBoxOkViewModel.Show(provider, title, description, details));
}
);
}

internal void OnClose()
{
// NOTE(erri120): This gets called by the View and can't be inside the disposable
Expand Down
1 change: 1 addition & 0 deletions src/NexusMods.App/NexusMods.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Abstractions\NexusMods.Abstractions.Logging\NexusMods.Abstractions.Logging.csproj" />
<ProjectReference Include="..\ArchiveManagement\NexusMods.FileExtractor\NexusMods.FileExtractor.csproj" />
<ProjectReference Include="..\Games\NexusMods.Games.AdvancedInstaller.UI\NexusMods.Games.AdvancedInstaller.UI.csproj" />
<ProjectReference Include="..\Games\NexusMods.Games.FOMOD.UI\NexusMods.Games.FOMOD.UI.csproj" />
Expand Down
19 changes: 19 additions & 0 deletions src/NexusMods.App/ObservableLoggingTarget.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Reactive.Subjects;
using NexusMods.Abstractions.Logging;
using NLog;
using NLog.Targets;

namespace NexusMods.App;

public class ObservableLoggingTarget : Target, IObservableExceptionSource
{
public IObservable<LogMessage> Exceptions => _exceptions;

private Subject<LogMessage> _exceptions = new();

protected override void Write(LogEventInfo logEvent)
{
if (logEvent.Level == LogLevel.Error || logEvent.Level == LogLevel.Fatal)
_exceptions.OnNext(new LogMessage(logEvent.Exception, logEvent.FormattedMessage));
}
}
9 changes: 7 additions & 2 deletions src/NexusMods.App/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NexusMods.Abstractions.Logging;
using NexusMods.Abstractions.Serialization;
using NexusMods.Abstractions.Settings;
using NexusMods.Abstractions.Telemetry;
Expand Down Expand Up @@ -219,6 +220,7 @@ private static IHost BuildHost(
ExperimentalSettings experimentalSettings,
GameLocatorSettings? gameLocatorSettings = null)
{
var observableTarget = new ObservableLoggingTarget();
var host = new HostBuilder().ConfigureServices(services =>
{
var s = services.AddApp(
Expand All @@ -227,18 +229,20 @@ private static IHost BuildHost(
experimentalSettings: experimentalSettings,
gameLocatorSettings: gameLocatorSettings).Validate();

s.AddSingleton<IObservableExceptionSource, ObservableLoggingTarget>(_ => observableTarget);

if (startupMode.IsAvaloniaDesigner)
{
s.OverrideSettingsForTests<DataModelSettings>(settings => settings with { UseInMemoryDataModel = true, });
}
})
.ConfigureLogging((_, builder) => AddLogging(builder, loggingSettings, startupMode))
.ConfigureLogging((_, builder) => AddLogging(observableTarget, builder, loggingSettings, startupMode))
.Build();

return host;
}

private static void AddLogging(ILoggingBuilder loggingBuilder, LoggingSettings settings, StartupMode startupMode)
private static void AddLogging(ObservableLoggingTarget observableLoggingTarget, ILoggingBuilder loggingBuilder, LoggingSettings settings, StartupMode startupMode)
{
var fs = FileSystem.Shared;
var config = new NLog.Config.LoggingConfiguration();
Expand Down Expand Up @@ -279,6 +283,7 @@ private static void AddLogging(ILoggingBuilder loggingBuilder, LoggingSettings s
}

config.AddRuleForAllLevels(fileTarget);
config.AddRuleForAllLevels(observableLoggingTarget);


// NOTE(erri120): RemoveLoggerFactoryFilter prevents
Expand Down
Loading