- fix: memory leak from canalisation part 2

This commit is contained in:
2025-12-12 22:23:45 +03:00
parent f7cec5d093
commit e0a16f7fb6
10 changed files with 172 additions and 122 deletions

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@ obj/
riderModule.iml riderModule.iml
/_ReSharper.Caches/ /_ReSharper.Caches/
release/ release/
publish/ publish/
/.vs

View File

@@ -32,6 +32,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
<PackageReference Include="libsodium" Version="1.0.20"/> <PackageReference Include="libsodium" Version="1.0.20"/>
<PackageReference Include="Robust.Natives" Version="0.2.3" /> <PackageReference Include="Robust.Natives" Version="0.2.3" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
</ItemGroup> </ItemGroup>
<Target Name="BuildCheck" AfterTargets="AfterBuild"> <Target Name="BuildCheck" AfterTargets="AfterBuild">
@@ -63,8 +64,4 @@
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj"/> <ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj"/>
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/> <ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Controls\ServerListView.axaml" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Nebula.Launcher.Controls;
using Nebula.Launcher.ViewModels; using Nebula.Launcher.ViewModels;
using Nebula.Launcher.ViewModels.Pages; using Nebula.Launcher.ViewModels.Pages;

View File

@@ -118,6 +118,14 @@ public sealed class ServerViewContainer
private readonly List<string> _favorites = []; private readonly List<string> _favorites = [];
private readonly Dictionary<string, string> _customNames = []; private readonly Dictionary<string, string> _customNames = [];
private readonly Dictionary<string, WeakReference<IListEntryModelView>> _entries = new();
public ICollection<IListEntryModelView> Items =>
_entries.Values
.Select(wr => wr.TryGetTarget(out var target) ? target : null)
.Where(t => t != null)
.ToList()!;
public ServerViewContainer() public ServerViewContainer()
{ {
_viewHelperService = new ViewHelperService(); _viewHelperService = new ViewHelperService();
@@ -131,108 +139,124 @@ public sealed class ServerViewContainer
configurationService.SubscribeVarChanged(LauncherConVar.ServerCustomNames, OnCustomNamesChanged, true); configurationService.SubscribeVarChanged(LauncherConVar.ServerCustomNames, OnCustomNamesChanged, true);
} }
private void OnCustomNamesChanged(Dictionary<string,string>? value)
{
var oldNames =
_customNames.ToDictionary(k => k.Key, v => v.Value); //Clone think
_customNames.Clear();
if(value == null)
{
foreach (var (ip,_) in oldNames)
{
if(!_entries.TryGetValue(ip, out var listEntry) || listEntry is not IEntryNameHolder entryNameHolder)
continue;
entryNameHolder.Name = null;
}
return;
}
foreach (var (oldIp, oldName) in oldNames)
{
if(value.TryGetValue(oldIp, out var newName))
{
if (oldName == newName)
value.Remove(newName);
continue;
}
if(!_entries.TryGetValue(oldIp, out var listEntry) ||
listEntry is not IEntryNameHolder entryNameHolder)
continue;
entryNameHolder.Name = null;
}
foreach (var (ip, name) in value)
{
_customNames.Add(ip, name);
if(!_entries.TryGetValue(ip, out var listEntry) || listEntry is not IEntryNameHolder entryNameHolder)
continue;
entryNameHolder.Name = name;
}
}
private void OnFavoritesChange(string[]? value)
{
_favorites.Clear();
if(value == null) return;
foreach (var favorite in value)
{
_favorites.Add(favorite);
if (_entries.TryGetValue(favorite, out var entry) && entry is IFavoriteEntryModelView favoriteView)
{
favoriteView.IsFavorite = true;
}
}
}
private readonly Dictionary<string, IListEntryModelView> _entries = new();
public ICollection<IListEntryModelView> Items => _entries.Values;
public void Clear() public void Clear()
{ {
foreach (var (_, value) in _entries) foreach (var (_, weakRef) in _entries)
{ {
value.Dispose(); if (weakRef.TryGetTarget(out var value))
value.Dispose();
} }
_entries.Clear(); _entries.Clear();
} }
public IListEntryModelView Get(RobustUrl url, ServerStatus? serverStatus = null) public IListEntryModelView Get(RobustUrl url, ServerStatus? serverStatus = null)
{ {
var key = url.ToString();
IListEntryModelView? entry; IListEntryModelView? entry;
lock (_entries) lock (_entries)
{ {
_customNames.TryGetValue(url.ToString(), out var customName); _customNames.TryGetValue(key, out var customName);
if (_entries.TryGetValue(url.ToString(), out entry)) if (_entries.TryGetValue(key, out var weakEntry)
&& weakEntry.TryGetTarget(out entry))
{ {
return entry; return entry;
} }
if (serverStatus is not null) if (serverStatus is not null)
entry = _viewHelperService.GetViewModel<ServerEntryModelView>().WithData(url, customName, serverStatus); {
entry = _viewHelperService
.GetViewModel<ServerEntryModelView>()
.WithData(url, customName, serverStatus);
}
else else
entry = _viewHelperService.GetViewModel<ServerCompoundEntryViewModel>().LoadServerEntry(url, customName, CancellationToken.None); {
entry = _viewHelperService
if(_favorites.Contains(url.ToString()) && .GetViewModel<ServerCompoundEntryViewModel>()
entry is IFavoriteEntryModelView favoriteEntryModelView) .LoadServerEntry(url, customName, CancellationToken.None);
favoriteEntryModelView.IsFavorite = true; }
_entries.Add(url.ToString(), entry); if (_favorites.Contains(key)
&& entry is IFavoriteEntryModelView fav)
{
fav.IsFavorite = true;
}
_entries[key] = new WeakReference<IListEntryModelView>(entry);
} }
return entry; return entry;
} }
private void OnFavoritesChange(string[]? value)
{
_favorites.Clear();
if (value == null) return;
foreach (var favorite in value)
{
_favorites.Add(favorite);
if (_entries.TryGetValue(favorite, out var weak)
&& weak.TryGetTarget(out var entry)
&& entry is IFavoriteEntryModelView fav)
{
fav.IsFavorite = true;
}
}
}
private void OnCustomNamesChanged(Dictionary<string, string>? value)
{
var oldNames = _customNames.ToDictionary(x => x.Key, x => x.Value);
_customNames.Clear();
if (value == null)
{
foreach (var (ip, _) in oldNames)
{
ResetName(ip);
}
return;
}
foreach (var (oldIp, oldName) in oldNames)
{
if (value.TryGetValue(oldIp, out var newName))
{
if (oldName == newName)
value.Remove(newName);
continue;
}
ResetName(oldIp);
}
foreach (var (ip, name) in value)
{
_customNames.Add(ip, name);
if (_entries.TryGetValue(ip, out var weak)
&& weak.TryGetTarget(out var entry)
&& entry is IEntryNameHolder holder)
{
holder.Name = name;
}
}
}
private void ResetName(string ip)
{
if (_entries.TryGetValue(ip, out var weak)
&& weak.TryGetTarget(out var entry)
&& entry is IEntryNameHolder holder)
{
holder.Name = null;
}
}
} }
public interface IListEntryModelView : IDisposable public interface IListEntryModelView : IDisposable

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Nebula.Launcher.Models; using Nebula.Launcher.Models;
@@ -19,7 +18,7 @@ namespace Nebula.Launcher.ViewModels;
public sealed partial class ServerCompoundEntryViewModel : public sealed partial class ServerCompoundEntryViewModel :
ViewModelBase, IFavoriteEntryModelView, IFilterConsumer, IListEntryModelView, IEntryNameHolder ViewModelBase, IFavoriteEntryModelView, IFilterConsumer, IListEntryModelView, IEntryNameHolder
{ {
[ObservableProperty] private ServerEntryModelView? _currentEntry; private ServerEntryModelView? _currentEntry;
[ObservableProperty] private string _message = "Loading server entry..."; [ObservableProperty] private string _message = "Loading server entry...";
[ObservableProperty] private bool _isFavorite; [ObservableProperty] private bool _isFavorite;
[ObservableProperty] private bool _loading = true; [ObservableProperty] private bool _loading = true;
@@ -28,6 +27,28 @@ public sealed partial class ServerCompoundEntryViewModel :
private RobustUrl? _url; private RobustUrl? _url;
private ServerFilter? _currentFilter; private ServerFilter? _currentFilter;
public ServerEntryModelView? CurrentEntry
{
get => _currentEntry;
set
{
if (value == _currentEntry) return;
_currentEntry = value;
if (_currentEntry != null)
{
_currentEntry.IsFavorite = IsFavorite;
_currentEntry.Name = Name;
_currentEntry.ProcessFilter(_currentFilter);
}
Loading = _currentEntry == null;
OnPropertyChanged();
}
}
public string? Name public string? Name
{ {
get => _name; get => _name;
@@ -54,31 +75,43 @@ public sealed partial class ServerCompoundEntryViewModel :
{ {
} }
public ServerCompoundEntryViewModel LoadServerEntry(RobustUrl url,string? name, CancellationToken cancellationToken) public ServerCompoundEntryViewModel LoadWithEntry(ServerEntryModelView? entry)
{ {
Task.Run(async () => CurrentEntry = entry;
{
_url = url;
try
{
Message = "Loading server entry...";
var status = await RestService.GetAsync<ServerStatus>(_url.StatusUri, cancellationToken);
CurrentEntry = ServiceProvider.GetService<ServerEntryModelView>()!.WithData(_url, name, status);
CurrentEntry.IsFavorite = IsFavorite;
CurrentEntry.Loading = false;
CurrentEntry.ProcessFilter(_currentFilter);
Loading = false;
}
catch (Exception e)
{
Message = e.Message;
}
}, cancellationToken);
return this; return this;
} }
public ServerCompoundEntryViewModel LoadServerEntry(RobustUrl url, string? name, CancellationToken cancellationToken)
{
_url = url;
_name = name;
Task.Run(LoadServer, cancellationToken);
return this;
}
private async Task LoadServer()
{
if (_url is null)
{
Message = "Url is not set";
return;
}
try
{
Message = "Loading server entry...";
var status = await RestService.GetAsync<ServerStatus>(_url.StatusUri, CancellationToken.None);
CurrentEntry = ServiceProvider.GetService<ServerEntryModelView>()!.WithData(_url, null, status);
Loading = false;
}
catch (Exception e)
{
Message = "Error while fetching data from " + _url + " : " + e.Message;
}
}
public void ToggleFavorites() public void ToggleFavorites()
{ {
if (CurrentEntry is null && _url is not null) if (CurrentEntry is null && _url is not null)

View File

@@ -28,8 +28,6 @@ public sealed partial class ServerEntryModelView : ViewModelBase, IFilterConsume
[ObservableProperty] private bool _isFavorite; [ObservableProperty] private bool _isFavorite;
[ObservableProperty] private bool _isVisible; [ObservableProperty] private bool _isVisible;
[ObservableProperty] private bool _runVisible = true; [ObservableProperty] private bool _runVisible = true;
[ObservableProperty] private bool _tagDataVisible;
[ObservableProperty] private bool _loading;
[ObservableProperty] private string _realName; [ObservableProperty] private string _realName;
public string? Name public string? Name
@@ -131,7 +129,7 @@ public sealed partial class ServerEntryModelView : ViewModelBase, IFilterConsume
OnPropertyChanged(nameof(Status)); OnPropertyChanged(nameof(Status));
} }
public ServerEntryModelView WithData(RobustUrl url, string? name,ServerStatus serverStatus) public ServerEntryModelView WithData(RobustUrl url, string? name, ServerStatus serverStatus)
{ {
Address = url; Address = url;
SetStatus(serverStatus); SetStatus(serverStatus);

View File

@@ -70,9 +70,9 @@
</Grid> </Grid>
</Border> </Border>
<Panel IsVisible="{Binding !Loading}"> <ContentControl
<views:ServerEntryView IsVisible="{Binding !Loading}" DataContext="{Binding CurrentEntry}"/> IsVisible="{Binding !Loading}"
</Panel> Content="{Binding CurrentEntry}"/>
</Panel> </Panel>
</UserControl> </UserControl>

View File

@@ -2,7 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Nebula.Launcher.Controls"
xmlns:services="clr-namespace:Nebula.Launcher.Services" xmlns:services="clr-namespace:Nebula.Launcher.Services"
xmlns:viewModels1="clr-namespace:Nebula.Launcher.ViewModels" xmlns:viewModels1="clr-namespace:Nebula.Launcher.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"

View File

@@ -64,18 +64,18 @@ namespace {viewNamespace}
}}"; }}";
// Add the source code to the compilation. // Add the source code to the compilation.
context.AddSource($"{viewName}_viewConstructAuto.g.cs", SourceText.From(code, Encoding.UTF8)); context.AddSource($"{viewModelName}_{viewName}_viewConstructAuto.g.cs", SourceText.From(code, Encoding.UTF8));
} }
catch (Exception e) catch (Exception e)
{ {
var coder1 = $@" var coder1 = $@"
// <auto-generated/> // <auto-generated/>
// Error! // Error! {e.Message}
namespace {viewModelNamespace} namespace {viewModelNamespace}
{{ {{
public partial class {viewModelName} public partial class {viewModelName}
{{ {{
// {e.Message} // ERROR: {e.Message}
}} }}
}}"; }}";
@@ -84,6 +84,4 @@ namespace {viewModelNamespace}
} }
} }
} }
} }

View File

@@ -2,6 +2,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArchiving_002EUtils_002EWindows_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F27e9f12ad1e4318b9b02849ec3e6a502fa3ee761c4f0522ba756ab30cde1c_003FArchiving_002EUtils_002EWindows_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArchiving_002EUtils_002EWindows_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F27e9f12ad1e4318b9b02849ec3e6a502fa3ee761c4f0522ba756ab30cde1c_003FArchiving_002EUtils_002EWindows_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssembly_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F501151723a8d43558c75acbd334f26322066fa4b1c82b1297291314bf92ff_003FAssembly_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssembly_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F501151723a8d43558c75acbd334f26322066fa4b1c82b1297291314bf92ff_003FAssembly_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHeaderValue_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F88b338246f59cffdb6f3dc3d8dbcfc169599dc71d6f44a8f2732983db7f73a_003FAuthenticationHeaderValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHeaderValue_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F88b338246f59cffdb6f3dc3d8dbcfc169599dc71d6f44a8f2732983db7f73a_003FAuthenticationHeaderValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAvaloniaList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3cc366334cc52275393f9def48cfcbccc8382175579fbd4f75b8c0e4bf33_003FAvaloniaList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAvaloniaXamlLoader_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F80462644bd1cc7e0b229dc4f5752b48c01cb67b46ae563b1b5078cc2556b98_003FAvaloniaXamlLoader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAvaloniaXamlLoader_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F80462644bd1cc7e0b229dc4f5752b48c01cb67b46ae563b1b5078cc2556b98_003FAvaloniaXamlLoader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABorder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5fda7f1253ea19edc15f91b94a33322b857f1a9319fbffea8d26e9d304178_003FBorder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABorder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5fda7f1253ea19edc15f91b94a33322b857f1a9319fbffea8d26e9d304178_003FBorder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABrushes_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F86e7f7d5cebacb8f8e37f52cb9a1f6a4b8933239631e3d969a4bc881ae92f9_003FBrushes_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ABrushes_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F86e7f7d5cebacb8f8e37f52cb9a1f6a4b8933239631e3d969a4bc881ae92f9_003FBrushes_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>