fix.增强应用稳定性,修复了问题组件加载异常时连带应用崩溃的问题

This commit is contained in:
lincube
2026-06-23 12:45:54 +08:00
parent ef764ff974
commit 151ffb1f5e
8 changed files with 299 additions and 97 deletions

View File

@@ -0,0 +1,122 @@
using System.Text.RegularExpressions;
using LanMountainDesktop.Services;
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class ComponentLibraryCrashRegressionTests
{
[Fact]
public void BuiltInComponentXaml_DoesNotAnimateRenderTransformDirectly()
{
var componentsDirectory = FindRepositoryPath("LanMountainDesktop", "Views", "Components");
foreach (var file in Directory.EnumerateFiles(componentsDirectory, "*.axaml", SearchOption.AllDirectories))
{
var xaml = File.ReadAllText(file);
foreach (Match match in Regex.Matches(
xaml,
@"<Style\.Animations>[\s\S]*?</Style\.Animations>",
RegexOptions.CultureInvariant))
{
Assert.DoesNotContain(
"Property=\"RenderTransform\"",
match.Value,
StringComparison.Ordinal);
}
}
}
[Fact]
public void ComponentLibraryPreviewPages_FallBackWhenPreviewAttachFails()
{
var source = ReadRepositoryFile("LanMountainDesktop", "Views", "MainWindow.ComponentSystem.cs");
var buildSource = ExtractMethodSource(source, "BuildComponentLibraryComponentPages");
var pageSource = ExtractMethodSource(source, "CreateComponentLibraryComponentPage");
var contentSource = ExtractMethodSource(source, "CreateComponentLibraryComponentPageContent");
Assert.Contains("try", buildSource);
Assert.Contains("ComponentLibraryComponentPagesContainer.Children.Add(page)", buildSource);
Assert.Contains("forceFallback: true", buildSource);
Assert.Contains("UiExceptionGuard.IsFatalException", buildSource);
Assert.Contains("CreateStaticComponentPreviewFallback", contentSource);
Assert.Contains("forceFallback", pageSource);
}
[Fact]
public void SoftwareRenderRetry_IsDisabledAfterAvaloniaLifetimeStarts()
{
var shouldRetry = Program.ShouldRetryWithSoftwareRendering(
AppRenderingModeHelper.Default,
new InvalidOperationException("render failed"),
isAvaloniaLifetimeStarted: true);
Assert.False(shouldRetry);
}
private static string ReadRepositoryFile(params string[] segments)
{
var path = FindRepositoryPath(segments);
return File.ReadAllText(path);
}
private static string FindRepositoryPath(params string[] segments)
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
var candidate = Path.Combine(new[] { directory.FullName }.Concat(segments).ToArray());
if (File.Exists(candidate) || Directory.Exists(candidate))
{
return candidate;
}
if (File.Exists(Path.Combine(directory.FullName, "LanMountainDesktop.slnx")))
{
break;
}
directory = directory.Parent;
}
throw new FileNotFoundException($"Could not locate repository path '{Path.Combine(segments)}'.");
}
private static string ExtractMethodSource(string source, string methodName)
{
var methodIndex = source.IndexOf($"private void {methodName}(", StringComparison.Ordinal);
if (methodIndex < 0)
{
methodIndex = source.IndexOf($"private Grid {methodName}(", StringComparison.Ordinal);
}
if (methodIndex < 0)
{
methodIndex = source.IndexOf($"private StackPanel {methodName}(", StringComparison.Ordinal);
}
Assert.True(methodIndex >= 0, $"Could not locate method '{methodName}'.");
var braceIndex = source.IndexOf('{', methodIndex);
Assert.True(braceIndex >= 0, $"Could not locate method body for '{methodName}'.");
var depth = 0;
for (var i = braceIndex; i < source.Length; i++)
{
if (source[i] == '{')
{
depth++;
}
else if (source[i] == '}')
{
depth--;
if (depth == 0)
{
return source.Substring(methodIndex, i - methodIndex + 1);
}
}
}
throw new InvalidOperationException($"Could not extract method '{methodName}'.");
}
}

View File

@@ -54,7 +54,7 @@ public sealed class Program
WriteCrashDump(ex, StartupRenderMode); WriteCrashDump(ex, StartupRenderMode);
// 渲染模式安全降级:若失败且未禁用重试,且当前不是软件渲染,则用软件渲染重试一次 // 渲染模式安全降级:若失败且未禁用重试,且当前不是软件渲染,则用软件渲染重试一次
if (ShouldRetryWithSoftwareRendering(StartupRenderMode, ex) && if (ShouldRetryWithSoftwareRendering(StartupRenderMode, ex, isAvaloniaLifetimeStarted: Application.Current is not null) &&
!attemptedRenderModes.Contains(AppRenderingModeHelper.Software)) !attemptedRenderModes.Contains(AppRenderingModeHelper.Software))
{ {
AppLogger.Warn("Startup", $"Retrying startup with Software rendering mode (previous='{StartupRenderMode}')."); AppLogger.Warn("Startup", $"Retrying startup with Software rendering mode (previous='{StartupRenderMode}').");
@@ -195,8 +195,16 @@ public sealed class Program
/// 判断是否应该用软件渲染重试。当异常看起来与渲染相关GPU/驱动/平台初始化), /// 判断是否应该用软件渲染重试。当异常看起来与渲染相关GPU/驱动/平台初始化),
/// 且当前渲染模式不是软件渲染,且未通过环境变量禁用重试时返回 true。 /// 且当前渲染模式不是软件渲染,且未通过环境变量禁用重试时返回 true。
/// </summary> /// </summary>
private static bool ShouldRetryWithSoftwareRendering(string currentRenderMode, Exception ex) internal static bool ShouldRetryWithSoftwareRendering(
string currentRenderMode,
Exception ex,
bool isAvaloniaLifetimeStarted = false)
{ {
if (isAvaloniaLifetimeStarted)
{
return false;
}
if (string.Equals(currentRenderMode, AppRenderingModeHelper.Software, StringComparison.OrdinalIgnoreCase)) if (string.Equals(currentRenderMode, AppRenderingModeHelper.Software, StringComparison.OrdinalIgnoreCase))
{ {
return false; return false;

View File

@@ -26,6 +26,7 @@
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" /> <Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="FontSize" Value="14" /> <Setter Property="FontSize" Value="14" />
<Setter Property="Padding" Value="16,10" /> <Setter Property="Padding" Value="16,10" />
<Setter Property="RenderTransform" Value="scale(1)" />
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="{StaticResource FluttermotionToken.Duration.Fast}" /> <TransformOperationsTransition Property="RenderTransform" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />

View File

@@ -150,6 +150,7 @@
</Style> </Style>
<Style Selector="ui|FANavigationViewItem.settings-nav-item"> <Style Selector="ui|FANavigationViewItem.settings-nav-item">
<Setter Property="RenderTransform" Value="scale(1)" />
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
<BrushTransition Property="Background" Duration="0:0:0.083" Easing="0.05,0.75,0.10,1.00" /> <BrushTransition Property="Background" Duration="0:0:0.083" Easing="0.05,0.75,0.10,1.00" />

View File

@@ -51,6 +51,7 @@
<Setter Property="Padding" Value="12,6" /> <Setter Property="Padding" Value="12,6" />
<Setter Property="Margin" Value="0,0,8,8" /> <Setter Property="Margin" Value="0,0,8,8" />
<Setter Property="MinHeight" Value="34" /> <Setter Property="MinHeight" Value="34" />
<Setter Property="RenderTransform" Value="scale(1)" />
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.05,0.75,0.10,1.00" /> <BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.05,0.75,0.10,1.00" />
@@ -72,6 +73,7 @@
</Style> </Style>
<Style Selector=".settings-scope ComboBox"> <Style Selector=".settings-scope ComboBox">
<Setter Property="RenderTransform" Value="scale(1)" />
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" Easing="0.05,0.75,0.10,1.00" /> <BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" Easing="0.05,0.75,0.10,1.00" />
@@ -85,6 +87,7 @@
</Style> </Style>
<Style Selector=".settings-scope ToggleSwitch"> <Style Selector=".settings-scope ToggleSwitch">
<Setter Property="RenderTransform" Value="scale(1)" />
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.05,0.75,0.10,1.00" /> <DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.05,0.75,0.10,1.00" />

View File

@@ -48,7 +48,15 @@
Background="{DynamicResource CardBackgroundSecondaryBrush}" Background="{DynamicResource CardBackgroundSecondaryBrush}"
BorderBrush="Transparent" BorderBrush="Transparent"
BorderThickness="0" BorderThickness="0"
RenderTransform="scale(1)"
Cursor="Hand"> Cursor="Hand">
<Button.Transitions>
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.15" Easing="CubicEaseOut" />
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.10" Easing="CubicEaseOut" />
</Transitions>
</Button.Transitions>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Spacing="6" Spacing="6"
HorizontalAlignment="Center" HorizontalAlignment="Center"
@@ -70,27 +78,13 @@
<Button.Styles> <Button.Styles>
<!-- 悬停状态 --> <!-- 悬停状态 -->
<Style Selector="Button:pointerover"> <Style Selector="Button:pointerover">
<Style.Animations>
<Animation Duration="0:0:0.15" Easing="CubicEaseOut">
<KeyFrame Cue="100%">
<Setter Property="Background" Value="{DynamicResource CardBackgroundHoverBrush}"/> <Setter Property="Background" Value="{DynamicResource CardBackgroundHoverBrush}"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style> </Style>
<!-- 按下状态 --> <!-- 按下状态 -->
<Style Selector="Button:pressed"> <Style Selector="Button:pressed">
<Style.Animations>
<Animation Duration="0:0:0.1" Easing="CubicEaseOut">
<KeyFrame Cue="100%">
<Setter Property="Background" Value="{DynamicResource CardBackgroundPressedBrush}"/> <Setter Property="Background" Value="{DynamicResource CardBackgroundPressedBrush}"/>
<Setter Property="RenderTransform"> <Setter Property="RenderTransform" Value="scale(0.98)"/>
<ScaleTransform ScaleX="0.98" ScaleY="0.98"/>
</Setter>
</KeyFrame>
</Animation>
</Style.Animations>
</Style> </Style>
</Button.Styles> </Button.Styles>
</Button> </Button>

View File

@@ -3655,20 +3655,107 @@ public partial class MainWindow : Window
for (var i = 0; i < componentCount; i++) for (var i = 0; i < componentCount; i++)
{ {
var component = _componentLibraryActiveComponents[i]; var component = _componentLibraryActiveComponents[i];
var page = new Grid
{
Width = _componentLibraryComponentPageWidth,
Height = viewportHeight,
Background = Brushes.Transparent
};
// Fit the preview to the page while preserving component cell span proportions.
var previewMaxWidth = _componentLibraryComponentPageWidth * 0.94;
var previewMaxHeight = viewportHeight * 0.86;
var previewSpan = NormalizeComponentCellSpan( var previewSpan = NormalizeComponentCellSpan(
component.ComponentId, component.ComponentId,
(component.MinWidthCells, component.MinHeightCells)); (component.MinWidthCells, component.MinHeightCells));
var page = CreateComponentLibraryComponentPage(
component,
previewSpan,
_componentLibraryComponentPageWidth,
viewportHeight);
Grid.SetRow(page, 0);
Grid.SetColumn(page, i);
try
{
ComponentLibraryComponentPagesContainer.Children.Add(page);
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
AppLogger.Warn(
"ComponentLibrary",
$"Failed to attach component library preview page for component '{component.ComponentId}'. Falling back to placeholder.",
ex);
DisposeStaticComponentLibraryPreviews([page]);
var fallbackPage = CreateComponentLibraryComponentPage(
component,
previewSpan,
_componentLibraryComponentPageWidth,
viewportHeight,
forceFallback: true);
Grid.SetRow(fallbackPage, 0);
Grid.SetColumn(fallbackPage, i);
ComponentLibraryComponentPagesContainer.Children.Add(fallbackPage);
}
}
_componentLibraryComponentHostTransform = ComponentLibraryComponentPagesHost.RenderTransform as TranslateTransform;
if (_componentLibraryComponentHostTransform is null)
{
_componentLibraryComponentHostTransform = new TranslateTransform();
ComponentLibraryComponentPagesHost.RenderTransform = _componentLibraryComponentHostTransform;
}
ApplyComponentLibraryComponentOffset();
UpdateComponentLibraryComponentNavigationButtons();
}
private Grid CreateComponentLibraryComponentPage(
ComponentLibraryComponentEntry component,
(int WidthCells, int HeightCells) previewSpan,
double pageWidth,
double pageHeight,
bool forceFallback = false)
{
var page = new Grid
{
Width = pageWidth,
Height = pageHeight,
Background = Brushes.Transparent
};
try
{
var stack = CreateComponentLibraryComponentPageContent(
component,
previewSpan,
pageWidth,
pageHeight,
forceFallback);
page.Children.Add(stack);
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex) && !forceFallback)
{
AppLogger.Warn(
"ComponentLibrary",
$"Failed to build component library preview page for component '{component.ComponentId}'. Falling back to placeholder.",
ex);
page.Children.Clear();
var stack = CreateComponentLibraryComponentPageContent(
component,
previewSpan,
pageWidth,
pageHeight,
forceFallback: true);
page.Children.Add(stack);
}
return page;
}
private StackPanel CreateComponentLibraryComponentPageContent(
ComponentLibraryComponentEntry component,
(int WidthCells, int HeightCells) previewSpan,
double pageWidth,
double pageHeight,
bool forceFallback)
{
// Fit the preview to the page while preserving component cell span proportions.
var previewMaxWidth = pageWidth * 0.94;
var previewMaxHeight = pageHeight * 0.86;
var previewCellSize = Math.Min( var previewCellSize = Math.Min(
previewMaxWidth / Math.Max(1, previewSpan.WidthCells), previewMaxWidth / Math.Max(1, previewSpan.WidthCells),
previewMaxHeight / Math.Max(1, previewSpan.HeightCells)); previewMaxHeight / Math.Max(1, previewSpan.HeightCells));
@@ -3676,7 +3763,9 @@ public partial class MainWindow : Window
var previewWidth = previewSpan.WidthCells * previewCellSize; var previewWidth = previewSpan.WidthCells * previewCellSize;
var previewHeight = previewSpan.HeightCells * previewCellSize; var previewHeight = previewSpan.HeightCells * previewCellSize;
var previewControl = CreateStaticComponentLibraryPreview( var previewControl = forceFallback
? CreateStaticComponentPreviewFallback(previewWidth, previewHeight)
: CreateStaticComponentLibraryPreview(
component.ComponentId, component.ComponentId,
previewCellSize, previewCellSize,
previewWidth, previewWidth,
@@ -3721,7 +3810,7 @@ public partial class MainWindow : Window
HorizontalAlignment = HorizontalAlignment.Center HorizontalAlignment = HorizontalAlignment.Center
}; };
var stack = new StackPanel return new StackPanel
{ {
Spacing = 8, Spacing = 8,
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
@@ -3733,23 +3822,6 @@ public partial class MainWindow : Window
hint hint
} }
}; };
page.Children.Add(stack);
Grid.SetRow(page, 0);
Grid.SetColumn(page, i);
ComponentLibraryComponentPagesContainer.Children.Add(page);
}
_componentLibraryComponentHostTransform = ComponentLibraryComponentPagesHost.RenderTransform as TranslateTransform;
if (_componentLibraryComponentHostTransform is null)
{
_componentLibraryComponentHostTransform = new TranslateTransform();
ComponentLibraryComponentPagesHost.RenderTransform = _componentLibraryComponentHostTransform;
}
ApplyComponentLibraryComponentOffset();
UpdateComponentLibraryComponentNavigationButtons();
} }
private void ClearComponentLibraryPreviewControls() private void ClearComponentLibraryPreviewControls()

View File

@@ -76,6 +76,7 @@
<Setter Property="HorizontalAlignment" Value="Stretch" /> <Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="MinHeight" Value="48" /> <Setter Property="MinHeight" Value="48" />
<Setter Property="RenderTransform" Value="scale(1)" />
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" /> <BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />