From c2ab5503296b9cdaf8a786ec9c6443e69b9cc3a9 Mon Sep 17 00:00:00 2001 From: Cinka Date: Thu, 22 Jan 2026 21:13:39 +0300 Subject: [PATCH] - tweak: exception representation --- Nebula.Launcher/Models/ContentLogConsumer.cs | 15 ++- .../DotnetProcessStartInfoProviderBase.cs | 5 +- .../GameProcessStartInfoProvider.cs | 5 +- .../ProcessHelper/IProcessRunner.cs | 3 +- .../ProcessHelper/ProcessRunHandler.cs | 124 +++++++++++++----- .../Services/InstanceRunningContainer.cs | 4 +- .../ViewModels/ExceptionCompound.cs | 39 ++++++ Nebula.Launcher/ViewModels/MainViewModel.cs | 7 +- .../Popup/ExceptionListViewModel.cs | 11 +- .../ServerCompoundEntryModelView.cs | 23 ++-- .../ViewModels/ServerEntryModelView.cs | 1 + Nebula.Launcher/Views/ExceptionView.axaml | 8 +- Nebula.Launcher/Views/ExceptionView.axaml.cs | 4 +- Nebula.Shared/Nebula.Shared.csproj | 1 + Nebula.Shared/ServiceManager.cs | 2 + .../Services/DotnetResolverService.cs | 14 +- Nebula.sln.DotSettings.user | 2 + 17 files changed, 195 insertions(+), 73 deletions(-) create mode 100644 Nebula.Launcher/ViewModels/ExceptionCompound.cs diff --git a/Nebula.Launcher/Models/ContentLogConsumer.cs b/Nebula.Launcher/Models/ContentLogConsumer.cs index 1d9d750..77d8988 100644 --- a/Nebula.Launcher/Models/ContentLogConsumer.cs +++ b/Nebula.Launcher/Models/ContentLogConsumer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Nebula.Launcher.ProcessHelper; +using Nebula.Launcher.ViewModels; using Nebula.Launcher.ViewModels.Popup; using Nebula.Shared.Services; @@ -8,18 +9,24 @@ namespace Nebula.Launcher.Models; public sealed class ContentLogConsumer : IProcessLogConsumer { + private readonly PopupMessageService _popupMessageService; private readonly List _outMessages = []; private LogPopupModelView? _currentLogPopup; public int MaxMessages { get; set; } = 100; - public void Popup(PopupMessageService popupMessageService) + public ContentLogConsumer(PopupMessageService popupMessageService) + { + _popupMessageService = popupMessageService; + } + + public void Popup() { if(_currentLogPopup is not null) return; - _currentLogPopup = new LogPopupModelView(popupMessageService); + _currentLogPopup = new LogPopupModelView(_popupMessageService); _currentLogPopup.OnDisposing += OnLogPopupDisposing; foreach (var message in _outMessages.ToArray()) @@ -27,7 +34,7 @@ public sealed class ContentLogConsumer : IProcessLogConsumer _currentLogPopup.Append(message); } - popupMessageService.Popup(_currentLogPopup); + _popupMessageService.Popup(_currentLogPopup); } private void OnLogPopupDisposing(PopupViewModelBase obj) @@ -55,6 +62,6 @@ public sealed class ContentLogConsumer : IProcessLogConsumer public void Fatal(string text) { - throw new Exception("Error while running programm: " + text); + _popupMessageService.Popup(new ExceptionCompound("Error while running program", text)); } } \ No newline at end of file diff --git a/Nebula.Launcher/ProcessHelper/DotnetProcessStartInfoProviderBase.cs b/Nebula.Launcher/ProcessHelper/DotnetProcessStartInfoProviderBase.cs index b342261..6816208 100644 --- a/Nebula.Launcher/ProcessHelper/DotnetProcessStartInfoProviderBase.cs +++ b/Nebula.Launcher/ProcessHelper/DotnetProcessStartInfoProviderBase.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Text; +using System.Threading; using System.Threading.Tasks; using Nebula.Shared.Services; @@ -9,11 +10,11 @@ public abstract class DotnetProcessStartInfoProviderBase(DotnetResolverService r { protected abstract string GetDllPath(); - public virtual async Task GetProcessStartInfo() + public virtual async Task GetProcessStartInfo(CancellationToken cancellationToken = default) { return new ProcessStartInfo { - FileName = await resolverService.EnsureDotnet(), + FileName = await resolverService.EnsureDotnet(cancellationToken), Arguments = GetDllPath(), CreateNoWindow = true, UseShellExecute = false, diff --git a/Nebula.Launcher/ProcessHelper/GameProcessStartInfoProvider.cs b/Nebula.Launcher/ProcessHelper/GameProcessStartInfoProvider.cs index 1308e5e..b401b29 100644 --- a/Nebula.Launcher/ProcessHelper/GameProcessStartInfoProvider.cs +++ b/Nebula.Launcher/ProcessHelper/GameProcessStartInfoProvider.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using System.IO; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Nebula.Launcher.ViewModels.Pages; using Nebula.Shared; @@ -31,9 +32,9 @@ public sealed class GameProcessStartInfoProvider(DotnetResolverService resolverS return this; } - public override async Task GetProcessStartInfo() + public override async Task GetProcessStartInfo(CancellationToken cancellationToken = default) { - var baseStart = await base.GetProcessStartInfo(); + var baseStart = await base.GetProcessStartInfo(cancellationToken); var authProv = accountInfoViewModel.Credentials.Value; if(authProv is null) diff --git a/Nebula.Launcher/ProcessHelper/IProcessRunner.cs b/Nebula.Launcher/ProcessHelper/IProcessRunner.cs index fb30a68..650de10 100644 --- a/Nebula.Launcher/ProcessHelper/IProcessRunner.cs +++ b/Nebula.Launcher/ProcessHelper/IProcessRunner.cs @@ -1,9 +1,10 @@ using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; namespace Nebula.Launcher.ProcessHelper; public interface IProcessStartInfoProvider { - public Task GetProcessStartInfo(); + public Task GetProcessStartInfo(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Nebula.Launcher/ProcessHelper/ProcessRunHandler.cs b/Nebula.Launcher/ProcessHelper/ProcessRunHandler.cs index 9629f24..a73ceb0 100644 --- a/Nebula.Launcher/ProcessHelper/ProcessRunHandler.cs +++ b/Nebula.Launcher/ProcessHelper/ProcessRunHandler.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics; +using System.Text; +using System.Threading; using System.Threading.Tasks; using Nebula.Shared.Services; using Nebula.Shared.Services.Logging; @@ -8,36 +10,23 @@ namespace Nebula.Launcher.ProcessHelper; public class ProcessRunHandler : IDisposable { - private ProcessStartInfo? _processInfo; - private Task? _processInfoTask; - private Process? _process; private readonly IProcessLogConsumer _logConsumer; - private string _lastError = string.Empty; - private readonly IProcessStartInfoProvider _currentProcessStartInfoProvider; + private StringBuilder _lastErrorBuilder = new StringBuilder(); - public IProcessStartInfoProvider GetCurrentProcessStartInfo() => _currentProcessStartInfoProvider; - public bool IsRunning => _processInfo is not null; + public bool IsRunning => _process is not null; public Action? OnProcessExited; + + public AsyncValueCache ProcessStartInfoProvider { get; } public bool Disposed { get; private set; } public ProcessRunHandler(IProcessStartInfoProvider processStartInfoProvider, IProcessLogConsumer logConsumer) { - _currentProcessStartInfoProvider = processStartInfoProvider; _logConsumer = logConsumer; - _processInfoTask = _currentProcessStartInfoProvider.GetProcessStartInfo(); - _processInfoTask.GetAwaiter().OnCompleted(OnInfoProvided); - } - private void OnInfoProvided() - { - if (_processInfoTask == null) - return; - - _processInfo = _processInfoTask.GetAwaiter().GetResult(); - _processInfoTask = null; + ProcessStartInfoProvider = new AsyncValueCache(processStartInfoProvider.GetProcessStartInfo); } private void CheckIfDisposed() @@ -51,13 +40,8 @@ public class ProcessRunHandler : IDisposable CheckIfDisposed(); if(_process is not null) throw new InvalidOperationException("Already running"); - - if (_processInfoTask != null) - { - _processInfoTask.Wait(); - } - _process = Process.Start(_processInfo ?? throw new Exception("Process info is null, please try again.")); + _process = Process.Start(ProcessStartInfoProvider.GetValue()); if (_process is null) return; @@ -86,9 +70,8 @@ public class ProcessRunHandler : IDisposable _process.ErrorDataReceived -= OnErrorDataReceived; _process.Exited -= OnExited; - if (_process.ExitCode != 0) - _logConsumer.Fatal(_lastError); + _logConsumer.Fatal(_lastErrorBuilder.ToString()); _process.Dispose(); _process = null; @@ -99,11 +82,13 @@ public class ProcessRunHandler : IDisposable private void OnErrorDataReceived(object sender, DataReceivedEventArgs e) { - if (e.Data != null) - { - _lastError = e.Data; - _logConsumer.Error(e.Data); - } + if (e.Data == null) return; + + if (!e.Data.StartsWith(" ")) + _lastErrorBuilder.Clear(); + + _lastErrorBuilder.AppendLine(e.Data); + _logConsumer.Error(e.Data); } private void OnOutputDataReceived(object sender, DataReceivedEventArgs e) @@ -122,9 +107,10 @@ public class ProcessRunHandler : IDisposable return; } + ProcessStartInfoProvider.Invalidate(); + CheckIfDisposed(); - - _processInfoTask?.Dispose(); + Disposed = true; } } @@ -152,4 +138,76 @@ public sealed class DebugLoggerBridge : IProcessLogConsumer { _logger.Log(LoggerCategory.Error, text); } +} + +public class AsyncValueCache +{ + private readonly Func> _valueFactory; + private readonly SemaphoreSlim _semaphore = new(1, 1); + private readonly CancellationTokenSource _cacheCts = new(); + + private Lazy> _lazyTask = null!; + private T _cachedValue = default!; + private bool _isCacheValid; + + public AsyncValueCache(Func> valueFactory) + { + _valueFactory = valueFactory ?? throw new ArgumentNullException(nameof(valueFactory)); + ResetLazyTask(); + } + + public T GetValue() + { + if (_isCacheValid) return _cachedValue; + + try + { + _semaphore.Wait(); + if (_isCacheValid) return _cachedValue; + + _cachedValue = _lazyTask.Value + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + + _isCacheValid = true; + return _cachedValue; + } + finally + { + _semaphore.Release(); + } + } + + public void Invalidate() + { + using var cts = new CancellationTokenSource(); + try + { + _semaphore.Wait(); + _isCacheValid = false; + _cacheCts.Cancel(); + _cacheCts.Dispose(); + ResetLazyTask(); + } + finally + { + _semaphore.Release(); + } + } + + private void ResetLazyTask() + { + _lazyTask = new Lazy>(() => + _valueFactory(_cacheCts.Token) + .ContinueWith(t => + { + if (t.IsCanceled || t.IsFaulted) + { + _isCacheValid = false; + throw t.Exception ?? new Exception(); + } + return t.Result; + }, TaskContinuationOptions.ExecuteSynchronously)); + } } \ No newline at end of file diff --git a/Nebula.Launcher/Services/InstanceRunningContainer.cs b/Nebula.Launcher/Services/InstanceRunningContainer.cs index 88eb987..fa7f03f 100644 --- a/Nebula.Launcher/Services/InstanceRunningContainer.cs +++ b/Nebula.Launcher/Services/InstanceRunningContainer.cs @@ -22,7 +22,7 @@ public sealed class InstanceRunningContainer(PopupMessageService popupMessageSer { var id = _keyPool.Take(); - var currentContentLogConsumer = new ContentLogConsumer(); + var currentContentLogConsumer = new ContentLogConsumer(popupMessageService); var logBridge = new DebugLoggerBridge(debugService.GetLogger("PROCESS_"+id.Id)); var logContainer = new ProcessLogConsumerCollection(); logContainer.RegisterLogger(currentContentLogConsumer); @@ -43,7 +43,7 @@ public sealed class InstanceRunningContainer(PopupMessageService popupMessageSer if(!_contentLoggerCache.TryGetValue(instanceKey, out var handler)) return; - handler.Popup(popupMessageService); + handler.Popup(); } public void Run(InstanceKey instanceKey) diff --git a/Nebula.Launcher/ViewModels/ExceptionCompound.cs b/Nebula.Launcher/ViewModels/ExceptionCompound.cs new file mode 100644 index 0000000..36d536b --- /dev/null +++ b/Nebula.Launcher/ViewModels/ExceptionCompound.cs @@ -0,0 +1,39 @@ +using System; +using Nebula.Launcher.Views; +using Nebula.Shared.ViewHelper; + +namespace Nebula.Launcher.ViewModels; + +[ViewModelRegister(typeof(ExceptionView), false)] +public class ExceptionCompound : ViewModelBase +{ + public ExceptionCompound() + { + Message = "Test exception"; + StackTrace = "Stack trace"; + } + + public ExceptionCompound(string message, string stackTrace) + { + Message = message; + StackTrace = stackTrace; + } + + public ExceptionCompound(Exception ex) + { + Message = ex.Message; + StackTrace = ex.StackTrace; + } + + public string Message { get; set; } + public string? StackTrace { get; set; } + + + protected override void InitialiseInDesignMode() + { + } + + protected override void Initialise() + { + } +} \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/MainViewModel.cs b/Nebula.Launcher/ViewModels/MainViewModel.cs index e0844f8..220b90a 100644 --- a/Nebula.Launcher/ViewModels/MainViewModel.cs +++ b/Nebula.Launcher/ViewModels/MainViewModel.cs @@ -3,9 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; -using Avalonia.Logging; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; using Nebula.Launcher.Models; using Nebula.Launcher.Services; using Nebula.Launcher.Utils; @@ -231,6 +229,11 @@ public partial class MainViewModel : ViewModelBase case PopupViewModelBase @base: PopupMessage(@base); break; + case ExceptionCompound error: + var errViewModel = ViewHelperService.GetViewModel(); + errViewModel.AppendError(error); + PopupMessage(errViewModel); + break; case Exception error: var err = ViewHelperService.GetViewModel(); _logger.Error(error); diff --git a/Nebula.Launcher/ViewModels/Popup/ExceptionListViewModel.cs b/Nebula.Launcher/ViewModels/Popup/ExceptionListViewModel.cs index d5299a2..b451364 100644 --- a/Nebula.Launcher/ViewModels/Popup/ExceptionListViewModel.cs +++ b/Nebula.Launcher/ViewModels/Popup/ExceptionListViewModel.cs @@ -15,7 +15,7 @@ public sealed partial class ExceptionListViewModel : PopupViewModelBase public override string Title => LocalizationService.GetString("popup-exception"); public override bool IsClosable => true; - public ObservableCollection Errors { get; } = new(); + public ObservableCollection Errors { get; } = new(); protected override void Initialise() { @@ -23,13 +23,18 @@ public sealed partial class ExceptionListViewModel : PopupViewModelBase protected override void InitialiseInDesignMode() { - var e = new Exception("TEST"); + var e = new ExceptionCompound("TEST", "thrown in design mode"); AppendError(e); } + public void AppendError(ExceptionCompound exception) + { + Errors.Add(exception); + } + public void AppendError(Exception exception) { - Errors.Add(exception); + AppendError(new ExceptionCompound(exception)); if (exception.InnerException != null) AppendError(exception.InnerException); } diff --git a/Nebula.Launcher/ViewModels/ServerCompoundEntryModelView.cs b/Nebula.Launcher/ViewModels/ServerCompoundEntryModelView.cs index 459c226..7d491a7 100644 --- a/Nebula.Launcher/ViewModels/ServerCompoundEntryModelView.cs +++ b/Nebula.Launcher/ViewModels/ServerCompoundEntryModelView.cs @@ -18,7 +18,6 @@ namespace Nebula.Launcher.ViewModels; public sealed partial class ServerCompoundEntryViewModel : ViewModelBase, IFavoriteEntryModelView, IFilterConsumer, IListEntryModelView, IEntryNameHolder { - private ServerEntryModelView? _currentEntry; [ObservableProperty] private string _message = "Loading server entry..."; [ObservableProperty] private bool _isFavorite; [ObservableProperty] private bool _loading = true; @@ -29,22 +28,22 @@ public sealed partial class ServerCompoundEntryViewModel : public ServerEntryModelView? CurrentEntry { - get => _currentEntry; + get; set { - if (value == _currentEntry) return; - - _currentEntry = value; + if (value == field) return; - if (_currentEntry != null) + field = value; + + if (field != null) { - _currentEntry.IsFavorite = IsFavorite; - _currentEntry.Name = Name; - _currentEntry.ProcessFilter(_currentFilter); + field.IsFavorite = IsFavorite; + field.Name = Name; + field.ProcessFilter(_currentFilter); } - - Loading = _currentEntry == null; - + + Loading = field == null; + OnPropertyChanged(); } } diff --git a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs index 6b822a3..b2bf61a 100644 --- a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs +++ b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs @@ -230,6 +230,7 @@ public sealed partial class ServerEntryModelView : ViewModelBase, IFilterConsume public void Dispose() { _logger.Dispose(); + InstanceRunningContainer.IsRunningChanged -= IsRunningChanged; } } diff --git a/Nebula.Launcher/Views/ExceptionView.axaml b/Nebula.Launcher/Views/ExceptionView.axaml index 185d15b..1cbe766 100644 --- a/Nebula.Launcher/Views/ExceptionView.axaml +++ b/Nebula.Launcher/Views/ExceptionView.axaml @@ -3,14 +3,14 @@ d:DesignWidth="800" mc:Ignorable="d" x:Class="Nebula.Launcher.Views.ExceptionView" - x:DataType="system:Exception" + x:DataType="viewModels:ExceptionCompound" xmlns="https://github.com/avaloniaui" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:system="clr-namespace:System;assembly=System.Runtime" - xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels"> - + Utility.runtime.json + diff --git a/Nebula.Shared/ServiceManager.cs b/Nebula.Shared/ServiceManager.cs index cb559d4..62885d8 100644 --- a/Nebula.Shared/ServiceManager.cs +++ b/Nebula.Shared/ServiceManager.cs @@ -1,4 +1,5 @@ using System.Reflection; +using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; namespace Nebula.Shared; @@ -42,6 +43,7 @@ public static class ServiceExt } } +[MeansImplicitUse] public sealed class ServiceRegisterAttribute : Attribute { public ServiceRegisterAttribute(Type? inference = null, bool isSingleton = true) diff --git a/Nebula.Shared/Services/DotnetResolverService.cs b/Nebula.Shared/Services/DotnetResolverService.cs index caee927..bfe81b3 100644 --- a/Nebula.Shared/Services/DotnetResolverService.cs +++ b/Nebula.Shared/Services/DotnetResolverService.cs @@ -14,15 +14,15 @@ public class DotnetResolverService(DebugService debugService, ConfigurationServi private string ExecutePath => Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension()); private readonly HttpClient _httpClient = new(); - public async Task EnsureDotnet() + public async Task EnsureDotnet(CancellationToken cancellationToken = default) { if (!Directory.Exists(FullPath)) - await Download(); + await Download(cancellationToken); return ExecutePath; } - private async Task Download() + private async Task Download(CancellationToken cancellationToken = default) { var debugLogger = debugService.GetLogger(this); debugLogger.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}..."); @@ -31,16 +31,16 @@ public class DotnetResolverService(DebugService debugService, ConfigurationServi configurationService.GetConfigValue(CurrentConVar.DotnetUrl)! ); - using var response = await _httpClient.GetAsync(url); + using var response = await _httpClient.GetAsync(url, cancellationToken); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); Directory.CreateDirectory(FullPath); if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { - using var zipArchive = new ZipArchive(stream); - zipArchive.ExtractToDirectory(FullPath, true); + await using var zipArchive = new ZipArchive(stream); + await zipArchive.ExtractToDirectoryAsync(FullPath, true, cancellationToken); } else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) || url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) diff --git a/Nebula.sln.DotSettings.user b/Nebula.sln.DotSettings.user index 4ededbf..e478e5e 100644 --- a/Nebula.sln.DotSettings.user +++ b/Nebula.sln.DotSettings.user @@ -77,7 +77,9 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded