mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-25 02:55:55 +08:00
fix.增强应用稳定性,修复了问题组件加载异常时连带应用崩溃的问题
This commit is contained in:
122
LanMountainDesktop.Tests/ComponentLibraryCrashRegressionTests.cs
Normal file
122
LanMountainDesktop.Tests/ComponentLibraryCrashRegressionTests.cs
Normal 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}'.");
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ public sealed class Program
|
||||
WriteCrashDump(ex, StartupRenderMode);
|
||||
|
||||
// 渲染模式安全降级:若失败且未禁用重试,且当前不是软件渲染,则用软件渲染重试一次
|
||||
if (ShouldRetryWithSoftwareRendering(StartupRenderMode, ex) &&
|
||||
if (ShouldRetryWithSoftwareRendering(StartupRenderMode, ex, isAvaloniaLifetimeStarted: Application.Current is not null) &&
|
||||
!attemptedRenderModes.Contains(AppRenderingModeHelper.Software))
|
||||
{
|
||||
AppLogger.Warn("Startup", $"Retrying startup with Software rendering mode (previous='{StartupRenderMode}').");
|
||||
@@ -195,8 +195,16 @@ public sealed class Program
|
||||
/// 判断是否应该用软件渲染重试。当异常看起来与渲染相关(GPU/驱动/平台初始化),
|
||||
/// 且当前渲染模式不是软件渲染,且未通过环境变量禁用重试时返回 true。
|
||||
/// </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))
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Padding" Value="16,10" />
|
||||
<Setter Property="RenderTransform" Value="scale(1)" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<TransformOperationsTransition Property="RenderTransform" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|FANavigationViewItem.settings-nav-item">
|
||||
<Setter Property="RenderTransform" Value="scale(1)" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<BrushTransition Property="Background" Duration="0:0:0.083" Easing="0.05,0.75,0.10,1.00" />
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<Setter Property="Padding" Value="12,6" />
|
||||
<Setter Property="Margin" Value="0,0,8,8" />
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
<Setter Property="RenderTransform" Value="scale(1)" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.05,0.75,0.10,1.00" />
|
||||
@@ -72,6 +73,7 @@
|
||||
</Style>
|
||||
|
||||
<Style Selector=".settings-scope ComboBox">
|
||||
<Setter Property="RenderTransform" Value="scale(1)" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" Easing="0.05,0.75,0.10,1.00" />
|
||||
@@ -85,6 +87,7 @@
|
||||
</Style>
|
||||
|
||||
<Style Selector=".settings-scope ToggleSwitch">
|
||||
<Setter Property="RenderTransform" Value="scale(1)" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.05,0.75,0.10,1.00" />
|
||||
|
||||
@@ -48,7 +48,15 @@
|
||||
Background="{DynamicResource CardBackgroundSecondaryBrush}"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
RenderTransform="scale(1)"
|
||||
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"
|
||||
Spacing="6"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -70,27 +78,13 @@
|
||||
<Button.Styles>
|
||||
<!-- 悬停状态 -->
|
||||
<Style Selector="Button:pointerover">
|
||||
<Style.Animations>
|
||||
<Animation Duration="0:0:0.15" Easing="CubicEaseOut">
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Background" Value="{DynamicResource CardBackgroundHoverBrush}"/>
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
<Setter Property="Background" Value="{DynamicResource CardBackgroundHoverBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- 按下状态 -->
|
||||
<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="RenderTransform">
|
||||
<ScaleTransform ScaleX="0.98" ScaleY="0.98"/>
|
||||
</Setter>
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
<Setter Property="Background" Value="{DynamicResource CardBackgroundPressedBrush}"/>
|
||||
<Setter Property="RenderTransform" Value="scale(0.98)"/>
|
||||
</Style>
|
||||
</Button.Styles>
|
||||
</Button>
|
||||
|
||||
@@ -3655,90 +3655,40 @@ public partial class MainWindow : Window
|
||||
for (var i = 0; i < componentCount; 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(
|
||||
component.ComponentId,
|
||||
(component.MinWidthCells, component.MinHeightCells));
|
||||
var previewCellSize = Math.Min(
|
||||
previewMaxWidth / Math.Max(1, previewSpan.WidthCells),
|
||||
previewMaxHeight / Math.Max(1, previewSpan.HeightCells));
|
||||
previewCellSize = Math.Clamp(previewCellSize, 24, 96);
|
||||
|
||||
var previewWidth = previewSpan.WidthCells * previewCellSize;
|
||||
var previewHeight = previewSpan.HeightCells * previewCellSize;
|
||||
var previewControl = CreateStaticComponentLibraryPreview(
|
||||
component.ComponentId,
|
||||
previewCellSize,
|
||||
previewWidth,
|
||||
previewHeight);
|
||||
|
||||
var previewSurface = new Border
|
||||
{
|
||||
Width = previewWidth,
|
||||
Height = previewHeight,
|
||||
Background = Brushes.Transparent,
|
||||
ClipToBounds = false,
|
||||
Child = previewControl,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
|
||||
var previewBorder = new Border
|
||||
{
|
||||
Width = previewWidth,
|
||||
Height = previewHeight,
|
||||
ClipToBounds = false,
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Child = previewSurface,
|
||||
Tag = component.ComponentId
|
||||
};
|
||||
previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed;
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = GetLocalizedComponentDisplayName(component),
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"),
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
|
||||
var hint = new TextBlock
|
||||
{
|
||||
Text = L("component_library.drag_hint", "Drag to place"),
|
||||
FontSize = 12,
|
||||
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"),
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
|
||||
var stack = new StackPanel
|
||||
{
|
||||
Spacing = 8,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
previewBorder,
|
||||
label,
|
||||
hint
|
||||
}
|
||||
};
|
||||
|
||||
page.Children.Add(stack);
|
||||
var page = CreateComponentLibraryComponentPage(
|
||||
component,
|
||||
previewSpan,
|
||||
_componentLibraryComponentPageWidth,
|
||||
viewportHeight);
|
||||
|
||||
Grid.SetRow(page, 0);
|
||||
Grid.SetColumn(page, i);
|
||||
ComponentLibraryComponentPagesContainer.Children.Add(page);
|
||||
|
||||
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;
|
||||
@@ -3752,6 +3702,128 @@ public partial class MainWindow : Window
|
||||
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(
|
||||
previewMaxWidth / Math.Max(1, previewSpan.WidthCells),
|
||||
previewMaxHeight / Math.Max(1, previewSpan.HeightCells));
|
||||
previewCellSize = Math.Clamp(previewCellSize, 24, 96);
|
||||
|
||||
var previewWidth = previewSpan.WidthCells * previewCellSize;
|
||||
var previewHeight = previewSpan.HeightCells * previewCellSize;
|
||||
var previewControl = forceFallback
|
||||
? CreateStaticComponentPreviewFallback(previewWidth, previewHeight)
|
||||
: CreateStaticComponentLibraryPreview(
|
||||
component.ComponentId,
|
||||
previewCellSize,
|
||||
previewWidth,
|
||||
previewHeight);
|
||||
|
||||
var previewSurface = new Border
|
||||
{
|
||||
Width = previewWidth,
|
||||
Height = previewHeight,
|
||||
Background = Brushes.Transparent,
|
||||
ClipToBounds = false,
|
||||
Child = previewControl,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
|
||||
var previewBorder = new Border
|
||||
{
|
||||
Width = previewWidth,
|
||||
Height = previewHeight,
|
||||
ClipToBounds = false,
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Child = previewSurface,
|
||||
Tag = component.ComponentId
|
||||
};
|
||||
previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed;
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = GetLocalizedComponentDisplayName(component),
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"),
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
|
||||
var hint = new TextBlock
|
||||
{
|
||||
Text = L("component_library.drag_hint", "Drag to place"),
|
||||
FontSize = 12,
|
||||
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"),
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
|
||||
return new StackPanel
|
||||
{
|
||||
Spacing = 8,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
previewBorder,
|
||||
label,
|
||||
hint
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void ClearComponentLibraryPreviewControls()
|
||||
{
|
||||
if (ComponentLibraryComponentPagesContainer is null)
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="MinHeight" Value="48" />
|
||||
<Setter Property="RenderTransform" Value="scale(1)" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />
|
||||
|
||||
Reference in New Issue
Block a user