我认为很稳定了,后面就要开始弄插件不稳定了
This commit is contained in:
lincube
2026-03-12 12:25:22 +08:00
parent 6952cb2c3e
commit 4679ee006f
20 changed files with 1197 additions and 205 deletions

View File

@@ -12,6 +12,8 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
@@ -29,9 +31,19 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
private string _scopedPlacementId = string.Empty;
public ComponentSettingsService()
: this(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop"))
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var settingsDirectory = Path.Combine(appData, "LanMountainDesktop");
}
internal ComponentSettingsService(string settingsDirectory)
{
if (string.IsNullOrWhiteSpace(settingsDirectory))
{
throw new ArgumentException("Settings directory cannot be null or whitespace.", nameof(settingsDirectory));
}
_settingsPath = Path.Combine(settingsDirectory, "component-settings.json");
_legacyAppSettingsPath = Path.Combine(settingsDirectory, "settings.json");
}
@@ -345,10 +357,11 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
}
ComponentSettingsDocumentSnapshot loadedSnapshot;
var loadedFromLegacy = false;
var loadDetails = ComponentSettingsLoadDetails.Empty;
if (hasFile)
{
loadedSnapshot = LoadSnapshotFromDisk();
loadDetails = LoadSnapshotFromDisk();
loadedSnapshot = loadDetails.Snapshot;
}
else if (TryLoadLegacySnapshot(out var migratedSnapshot))
{
@@ -356,7 +369,10 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
{
DefaultSettings = NormalizeSnapshot(migratedSnapshot)
};
loadedFromLegacy = true;
loadDetails = new ComponentSettingsLoadDetails(
loadedSnapshot,
ComponentSettingsDocumentFormat.LegacySnapshot,
true);
}
else
{
@@ -364,40 +380,44 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
}
var normalizedSnapshot = NormalizeDocument(loadedSnapshot);
if (loadedFromLegacy)
if (loadDetails.ShouldRewriteToCanonical)
{
writeTimeUtc = PersistSnapshotToDisk(normalizedSnapshot);
}
LogLoadDetails(loadDetails.Format, loadDetails.ShouldRewriteToCanonical, normalizedSnapshot);
UpdateCache(normalizedSnapshot, writeTimeUtc, nowUtc);
return normalizedSnapshot.Clone();
}
private ComponentSettingsDocumentSnapshot LoadSnapshotFromDisk()
private ComponentSettingsLoadDetails LoadSnapshotFromDisk()
{
try
{
var json = File.ReadAllText(_settingsPath);
using var document = JsonDocument.Parse(json);
if (document.RootElement.ValueKind == JsonValueKind.Object &&
(document.RootElement.TryGetProperty("defaultSettings", out _) ||
document.RootElement.TryGetProperty("instanceSettings", out _) ||
document.RootElement.TryGetProperty("pluginSettings", out _)))
if (TryGetDocumentFormat(document.RootElement, out var format))
{
var snapshot = JsonSerializer.Deserialize<ComponentSettingsDocumentSnapshot>(json, SerializerOptions);
return NormalizeDocument(snapshot);
return new ComponentSettingsLoadDetails(
snapshot ?? new ComponentSettingsDocumentSnapshot(),
format,
format == ComponentSettingsDocumentFormat.PascalCaseDocument);
}
var legacySnapshot = JsonSerializer.Deserialize<ComponentSettingsSnapshot>(json, SerializerOptions);
return new ComponentSettingsDocumentSnapshot
{
DefaultSettings = NormalizeSnapshot(legacySnapshot)
};
return new ComponentSettingsLoadDetails(
new ComponentSettingsDocumentSnapshot
{
DefaultSettings = NormalizeSnapshot(legacySnapshot)
},
ComponentSettingsDocumentFormat.LegacySnapshot,
true);
}
catch (Exception ex)
{
AppLogger.Warn("ComponentSettings", $"Failed to deserialize component settings from '{_settingsPath}'.", ex);
return new ComponentSettingsDocumentSnapshot();
return ComponentSettingsLoadDetails.Empty;
}
}
@@ -684,6 +704,79 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
_lastProbeUtc = probeTimeUtc;
}
internal static void ResetCacheForTests()
{
lock (CacheGate)
{
_cachedPath = null;
_cachedSnapshot = null;
_cachedWriteTimeUtc = DateTime.MinValue;
_lastProbeUtc = DateTime.MinValue;
}
}
private void LogLoadDetails(
ComponentSettingsDocumentFormat format,
bool rewroteToCanonical,
ComponentSettingsDocumentSnapshot snapshot)
{
AppLogger.Info(
"ComponentSettings",
$"Loaded component settings document. Format={format}; RewroteToCanonical={rewroteToCanonical}; " +
$"InstanceSettings={snapshot.InstanceSettings.Count}; PluginSettings={snapshot.PluginSettings.Count}; Path={_settingsPath}");
}
private static bool TryGetDocumentFormat(
JsonElement rootElement,
out ComponentSettingsDocumentFormat format)
{
format = ComponentSettingsDocumentFormat.EmptyDocument;
if (rootElement.ValueKind != JsonValueKind.Object)
{
return false;
}
var hasDocumentProperties = false;
var requiresCanonicalRewrite = false;
foreach (var property in rootElement.EnumerateObject())
{
if (!IsDocumentPropertyName(property.Name))
{
continue;
}
hasDocumentProperties = true;
if (!IsCanonicalDocumentPropertyName(property.Name))
{
requiresCanonicalRewrite = true;
}
}
if (!hasDocumentProperties)
{
return false;
}
format = requiresCanonicalRewrite
? ComponentSettingsDocumentFormat.PascalCaseDocument
: ComponentSettingsDocumentFormat.CanonicalDocument;
return true;
}
private static bool IsDocumentPropertyName(string propertyName)
{
return string.Equals(propertyName, "defaultSettings", StringComparison.OrdinalIgnoreCase) ||
string.Equals(propertyName, "instanceSettings", StringComparison.OrdinalIgnoreCase) ||
string.Equals(propertyName, "pluginSettings", StringComparison.OrdinalIgnoreCase);
}
private static bool IsCanonicalDocumentPropertyName(string propertyName)
{
return string.Equals(propertyName, "defaultSettings", StringComparison.Ordinal) ||
string.Equals(propertyName, "instanceSettings", StringComparison.Ordinal) ||
string.Equals(propertyName, "pluginSettings", StringComparison.Ordinal);
}
private sealed class ComponentSettingsDocumentSnapshot
{
public ComponentSettingsSnapshot DefaultSettings { get; set; } = new();
@@ -771,4 +864,23 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
public string Stcn24ForumSourceType { get; set; } = Stcn24ForumSourceTypes.LatestCreated;
}
private readonly record struct ComponentSettingsLoadDetails(
ComponentSettingsDocumentSnapshot Snapshot,
ComponentSettingsDocumentFormat Format,
bool ShouldRewriteToCanonical)
{
public static ComponentSettingsLoadDetails Empty { get; } = new(
new ComponentSettingsDocumentSnapshot(),
ComponentSettingsDocumentFormat.EmptyDocument,
false);
}
private enum ComponentSettingsDocumentFormat
{
EmptyDocument,
CanonicalDocument,
PascalCaseDocument,
LegacySnapshot
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Threading.Tasks;
namespace LanMountainDesktop.Services;
internal static class UiExceptionGuard
{
public static bool IsFatalException(Exception? exception)
{
return exception is OutOfMemoryException or AccessViolationException or StackOverflowException;
}
public static void FireAndForgetGuarded(
Func<Task> action,
string actionName,
string? context = null,
Func<Exception, Task>? onHandledException = null)
{
_ = RunGuardedUiActionAsync(action, actionName, context, onHandledException);
}
public static async Task RunGuardedUiActionAsync(
Func<Task> action,
string actionName,
string? context = null,
Func<Exception, Task>? onHandledException = null)
{
ArgumentNullException.ThrowIfNull(action);
try
{
await action();
}
catch (Exception ex) when (!IsFatalException(ex))
{
LogHandledException("GuardedUiAction", actionName, ex, context, isFatal: false);
if (onHandledException is not null)
{
try
{
await onHandledException(ex);
}
catch (Exception handlerEx) when (!IsFatalException(handlerEx))
{
LogHandledException("GuardedUiActionHandler", actionName, handlerEx, context, isFatal: false);
}
}
}
}
public static string BuildContext(params (string Key, object? Value)[] parts)
{
if (parts is null || parts.Length == 0)
{
return string.Empty;
}
return string.Join(
"; ",
Array.ConvertAll(parts, part => $"{part.Key}={part.Value ?? "<null>"}"));
}
private static void LogHandledException(
string category,
string actionName,
Exception exception,
string? context,
bool isFatal)
{
var message =
$"Action={actionName}; ExceptionType={exception.GetType().FullName}; IsFatal={isFatal}; Context={context ?? string.Empty}";
if (isFatal)
{
AppLogger.Critical(category, message, exception);
return;
}
AppLogger.Warn(category, message, exception);
}
}