Refactor Plugin to IAsyncDalamudPlugin two-phase load
This commit is contained in:
+184
-81
@@ -2,6 +2,7 @@ using System.Diagnostics;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
using HellionChat.Ipc;
|
using HellionChat.Ipc;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Ui;
|
using HellionChat.Ui;
|
||||||
@@ -17,7 +18,7 @@ using Dalamud.Interface.ImGuiFileDialog;
|
|||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
// ReSharper disable once ClassNeverInstantiated.Global
|
// ReSharper disable once ClassNeverInstantiated.Global
|
||||||
public sealed class Plugin : IDalamudPlugin
|
public sealed class Plugin : IAsyncDalamudPlugin
|
||||||
{
|
{
|
||||||
public const string PluginName = "Hellion Chat";
|
public const string PluginName = "Hellion Chat";
|
||||||
|
|
||||||
@@ -47,27 +48,35 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
public static FileDialogManager FileDialogManager { get; private set; } = null!;
|
public static FileDialogManager FileDialogManager { get; private set; } = null!;
|
||||||
|
|
||||||
public readonly WindowSystem WindowSystem = new(PluginName);
|
public readonly WindowSystem WindowSystem = new(PluginName);
|
||||||
public SettingsWindow SettingsWindow { get; }
|
|
||||||
public ChatLogWindow ChatLogWindow { get; }
|
|
||||||
public DbViewer DbViewer { get; }
|
|
||||||
public InputPreview InputPreview { get; }
|
|
||||||
public CommandHelpWindow CommandHelpWindow { get; }
|
|
||||||
public SeStringDebugger SeStringDebugger { get; }
|
|
||||||
public FirstRunWizard FirstRunWizard { get; }
|
|
||||||
public DebuggerWindow DebuggerWindow { get; }
|
|
||||||
|
|
||||||
internal Commands Commands { get; }
|
// v1.4.3: Phase-2 services need private setters now that LoadAsync
|
||||||
internal GameFunctions.GameFunctions Functions { get; }
|
// owns their construction. Phase-1-only services (Commands, Functions,
|
||||||
internal MessageManager MessageManager { get; }
|
// Ipc, ExtraChat, TypingIpc) keep their setters for symmetry.
|
||||||
internal AutoTellTabsService AutoTellTabsService { get; }
|
public SettingsWindow SettingsWindow { get; private set; } = null!;
|
||||||
internal IpcManager Ipc { get; }
|
public ChatLogWindow ChatLogWindow { get; private set; } = null!;
|
||||||
internal ExtraChat ExtraChat { get; }
|
public DbViewer DbViewer { get; private set; } = null!;
|
||||||
internal TypingIpc TypingIpc { get; }
|
public InputPreview InputPreview { get; private set; } = null!;
|
||||||
internal FontManager FontManager { get; }
|
public CommandHelpWindow CommandHelpWindow { get; private set; } = null!;
|
||||||
|
public SeStringDebugger SeStringDebugger { get; private set; } = null!;
|
||||||
|
public FirstRunWizard FirstRunWizard { get; private set; } = null!;
|
||||||
|
public DebuggerWindow DebuggerWindow { get; private set; } = null!;
|
||||||
|
|
||||||
|
internal Commands Commands { get; private set; } = null!;
|
||||||
|
internal GameFunctions.GameFunctions Functions { get; private set; } = null!;
|
||||||
|
internal MessageManager MessageManager { get; private set; } = null!;
|
||||||
|
internal AutoTellTabsService AutoTellTabsService { get; private set; } = null!;
|
||||||
|
internal IpcManager Ipc { get; private set; } = null!;
|
||||||
|
internal ExtraChat ExtraChat { get; private set; } = null!;
|
||||||
|
internal TypingIpc TypingIpc { get; private set; } = null!;
|
||||||
|
internal FontManager FontManager { get; private set; } = null!;
|
||||||
internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!;
|
internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!;
|
||||||
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
||||||
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
|
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
|
||||||
|
|
||||||
|
// (B3) Lightless idempotency guard — Dalamud may fire DisposeAsync twice
|
||||||
|
// in a reload race; second call short-circuits.
|
||||||
|
private int _disposeStarted;
|
||||||
|
|
||||||
internal int DeferredSaveFrames = -1;
|
internal int DeferredSaveFrames = -1;
|
||||||
|
|
||||||
// Serialises retention sweeps. The 24h auto-sweep on plugin load and
|
// Serialises retention sweeps. The 24h auto-sweep on plugin load and
|
||||||
@@ -98,14 +107,17 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
public Plugin()
|
public Plugin()
|
||||||
{
|
{
|
||||||
|
// v1.4.3 Phase 1 — sync ctor: only work that doesn't touch
|
||||||
|
// Theme/Font/MessageManager/UI-windows lives here. Async Phase 2
|
||||||
|
// (LoadAsync) owns those. Hooks subscribe at the END of LoadAsync
|
||||||
|
// so Dalamud's first Draw tick never sees null services (B1).
|
||||||
|
|
||||||
// Refuse to start if upstream Chat 2 is loaded — prevents IPC
|
// Refuse to start if upstream Chat 2 is loaded — prevents IPC
|
||||||
// channel collisions and double-replacement of the in-game chat
|
// channel collisions and double-replacement of the in-game chat
|
||||||
// window. Throwing here makes Dalamud abort the load cleanly with
|
// window. Throwing here makes Dalamud abort the load cleanly with
|
||||||
// our localized message instead of crashing FFXIV mid-frame.
|
// our localized message instead of crashing FFXIV mid-frame.
|
||||||
ChatTwoConflictDetector.ThrowIfChatTwoIsLoaded(Interface);
|
ChatTwoConflictDetector.ThrowIfChatTwoIsLoaded(Interface);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime();
|
GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime();
|
||||||
|
|
||||||
// Hellion Chat: take over config + database from upstream ChatTwo
|
// Hellion Chat: take over config + database from upstream ChatTwo
|
||||||
@@ -440,26 +452,12 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
FileDialogManager = new FileDialogManager();
|
FileDialogManager = new FileDialogManager();
|
||||||
|
|
||||||
|
// Phase-1 services: pure allocation, no Theme/Font/UI touch.
|
||||||
Commands = new Commands();
|
Commands = new Commands();
|
||||||
Functions = new GameFunctions.GameFunctions(this);
|
Functions = new GameFunctions.GameFunctions(this);
|
||||||
Ipc = new IpcManager();
|
Ipc = new IpcManager();
|
||||||
TypingIpc = new TypingIpc(this);
|
TypingIpc = new TypingIpc(this);
|
||||||
ExtraChat = new ExtraChat();
|
ExtraChat = new ExtraChat();
|
||||||
FontManager = new FontManager();
|
|
||||||
|
|
||||||
// v1.1.0 — Theme-Engine init. Custom-Themes liegen in
|
|
||||||
// pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get.
|
|
||||||
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
|
||||||
Directory.CreateDirectory(customThemesDir);
|
|
||||||
SeedExampleThemeIfEmpty(customThemesDir);
|
|
||||||
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
|
|
||||||
ThemeRegistry.Switch(Config.Theme);
|
|
||||||
|
|
||||||
// SelfTest hooks live alongside the live registry — the steps
|
|
||||||
// poll Active per frame and need the registry already wired.
|
|
||||||
SelfTestRegistry.RegisterTestSteps([
|
|
||||||
new SelfTests.ThemeSwitchSelfTestStep(this),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Plugin integrations register their IPC subscribers up-front so
|
// Plugin integrations register their IPC subscribers up-front so
|
||||||
// Ready/Disposing events from the target plugins are caught from
|
// Ready/Disposing events from the target plugins are caught from
|
||||||
@@ -469,7 +467,57 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
StatusBar = new Ui.StatusBar();
|
StatusBar = new Ui.StatusBar();
|
||||||
|
|
||||||
MessageManager = new MessageManager(this); // Does it require UI?
|
Interface.UiBuilder.DisableCutsceneUiHide = true;
|
||||||
|
Interface.UiBuilder.DisableGposeUiHide = true;
|
||||||
|
|
||||||
|
// Hellion Chat — daily retention sweep, off-thread so it never
|
||||||
|
// blocks plugin load. Skips itself when disabled or already ran
|
||||||
|
// within the past 24 hours. Pure fire-and-forget on a background
|
||||||
|
// thread, so it doesn't depend on Phase-2 services being live.
|
||||||
|
RunRetentionSweepIfDue();
|
||||||
|
|
||||||
|
if (Config.ShowEmotes)
|
||||||
|
_ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Group A: Font + Theme parallel (Q1=A). Both are CPU-bound,
|
||||||
|
// independent, and dominate the load-time profile. Everything
|
||||||
|
// else stays sequential to keep ordering simple.
|
||||||
|
// Q3=B transition: BuildFonts() is sync today; Task 5 converts
|
||||||
|
// FontManager itself to BuildFontsAsync.
|
||||||
|
var fontTask = Task.Run(() =>
|
||||||
|
{
|
||||||
|
FontManager = new FontManager();
|
||||||
|
FontManager.BuildFonts();
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
var themeTask = Task.Run(() =>
|
||||||
|
{
|
||||||
|
// v1.1.0 — Theme-Engine init. Custom-Themes liegen in
|
||||||
|
// pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get.
|
||||||
|
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
||||||
|
Directory.CreateDirectory(customThemesDir);
|
||||||
|
SeedExampleThemeIfEmpty(customThemesDir);
|
||||||
|
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
|
||||||
|
ThemeRegistry.Switch(Config.Theme);
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
await Task.WhenAll(fontTask, themeTask).ConfigureAwait(false);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// SelfTest hooks live alongside the live registry — the steps
|
||||||
|
// poll Active per frame and need the registry already wired.
|
||||||
|
SelfTestRegistry.RegisterTestSteps([
|
||||||
|
new SelfTests.ThemeSwitchSelfTestStep(this),
|
||||||
|
]);
|
||||||
|
|
||||||
|
MessageManager = new MessageManager(this);
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
|
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
|
||||||
// MessageManager's MessageProcessed event for live tells and
|
// MessageManager's MessageProcessed event for live tells and
|
||||||
@@ -479,11 +527,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store);
|
AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store);
|
||||||
AutoTellTabsService.Initialize();
|
AutoTellTabsService.Initialize();
|
||||||
|
|
||||||
// Hellion Chat — daily retention sweep, off-thread so it never
|
|
||||||
// blocks plugin load. Skips itself when disabled or already ran
|
|
||||||
// within the past 24 hours.
|
|
||||||
RunRetentionSweepIfDue();
|
|
||||||
|
|
||||||
ChatLogWindow = new ChatLogWindow(this);
|
ChatLogWindow = new ChatLogWindow(this);
|
||||||
SettingsWindow = new SettingsWindow(this);
|
SettingsWindow = new SettingsWindow(this);
|
||||||
DbViewer = new DbViewer(this);
|
DbViewer = new DbViewer(this);
|
||||||
@@ -507,17 +550,24 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
if (!Config.FirstRunCompleted)
|
if (!Config.FirstRunCompleted)
|
||||||
FirstRunWizard.IsOpen = true;
|
FirstRunWizard.IsOpen = true;
|
||||||
|
|
||||||
FontManager.BuildFonts();
|
|
||||||
|
|
||||||
Interface.UiBuilder.DisableCutsceneUiHide = true;
|
|
||||||
Interface.UiBuilder.DisableGposeUiHide = true;
|
|
||||||
|
|
||||||
// let all the other components register, then initialize commands
|
// let all the other components register, then initialize commands
|
||||||
Commands.Initialise();
|
Commands.Initialise();
|
||||||
|
|
||||||
if (Interface.Reason is not PluginLoadReason.Boot)
|
if (Interface.Reason is not PluginLoadReason.Boot)
|
||||||
MessageManager.FilterAllTabsAsync();
|
MessageManager.FilterAllTabsAsync();
|
||||||
|
|
||||||
|
#if !DEBUG
|
||||||
|
// Avoid 300ms hitch when sending first message by preloading the
|
||||||
|
// auto-translate cache. Don't do this in debug because it makes
|
||||||
|
// profiling difficult.
|
||||||
|
AutoTranslate.PreloadCache();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// (B1) Hooks last: every service and window must be live before
|
||||||
|
// Dalamud fires our first Draw / FrameworkUpdate tick. Anything
|
||||||
|
// earlier risks rendering against null FontManager / ThemeRegistry.
|
||||||
Framework.Update += FrameworkUpdate;
|
Framework.Update += FrameworkUpdate;
|
||||||
Interface.UiBuilder.Draw += Draw;
|
Interface.UiBuilder.Draw += Draw;
|
||||||
Interface.LanguageChanged += LanguageChanged;
|
Interface.LanguageChanged += LanguageChanged;
|
||||||
@@ -526,48 +576,86 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
// most useful landing place; OpenConfigUi is already wired to
|
// most useful landing place; OpenConfigUi is already wired to
|
||||||
// the same toggle inside SettingsWindow.
|
// the same toggle inside SettingsWindow.
|
||||||
Interface.UiBuilder.OpenMainUi += OpenMainUi;
|
Interface.UiBuilder.OpenMainUi += OpenMainUi;
|
||||||
|
|
||||||
if (Config.ShowEmotes)
|
|
||||||
_ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside
|
|
||||||
|
|
||||||
#if !DEBUG
|
|
||||||
// Avoid 300ms hitch when sending first message by preloading the
|
|
||||||
// auto-translate cache. Don't do this in debug because it makes
|
|
||||||
// profiling difficult.
|
|
||||||
AutoTranslate.PreloadCache();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch
|
||||||
{
|
{
|
||||||
Log.Error(ex, "Plugin load threw an error, turning off plugin");
|
// Mirror the v1.4.0 load-failure recovery: hand off to DisposeAsync
|
||||||
Dispose();
|
// so partially-built services are torn down. Swallow the cleanup
|
||||||
|
// exception so the original load failure stays the visible cause.
|
||||||
// Re-throw the exception to fail the plugin load.
|
try { await DisposeAsync().ConfigureAwait(false); }
|
||||||
|
catch { /* keep original failure */ }
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suppressing this warning because Dispose() is called in Plugin() if the
|
// Suppressing this warning because DisposeAsync may run after a partial
|
||||||
// load fails, so some values may not be initialized.
|
// LoadAsync, so some properties may not be initialized.
|
||||||
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract")]
|
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract")]
|
||||||
public void Dispose()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
Interface.UiBuilder.OpenMainUi -= OpenMainUi;
|
// (B3) Idempotency guard — Dalamud may reload-race us; second
|
||||||
Interface.LanguageChanged -= LanguageChanged;
|
// call short-circuits so we don't double-dispose services.
|
||||||
Interface.UiBuilder.Draw -= Draw;
|
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
|
||||||
Framework.Update -= FrameworkUpdate;
|
return;
|
||||||
GameFunctions.GameFunctions.SetChatInteractable(true);
|
|
||||||
|
|
||||||
// FrameworkUpdate would have fired the pending save in N frames,
|
Exception? failure = null;
|
||||||
// but we just unsubscribed it. -1 is the idle sentinel.
|
|
||||||
|
// Hooks unsubscribe FIRST so no Draw / FrameworkUpdate / LanguageChanged
|
||||||
|
// tick can fire while we're tearing services down. Mirrors the
|
||||||
|
// hooks-last subscribe order in LoadAsync.
|
||||||
|
failure = CaptureFailure(failure, () => Interface.UiBuilder.OpenMainUi -= OpenMainUi);
|
||||||
|
failure = CaptureFailure(failure, () => Interface.LanguageChanged -= LanguageChanged);
|
||||||
|
failure = CaptureFailure(failure, () => Interface.UiBuilder.Draw -= Draw);
|
||||||
|
failure = CaptureFailure(failure, () => Framework.Update -= FrameworkUpdate);
|
||||||
|
|
||||||
|
// v1.4.0 F5.3 — flush a pending DeferredSave before service teardown,
|
||||||
|
// since FrameworkUpdate just got unsubscribed and won't fire it.
|
||||||
|
failure = CaptureFailure(failure, () =>
|
||||||
|
{
|
||||||
if (DeferredSaveFrames >= 0)
|
if (DeferredSaveFrames >= 0)
|
||||||
{
|
{
|
||||||
SaveConfig();
|
SaveConfig();
|
||||||
DeferredSaveFrames = -1;
|
DeferredSaveFrames = -1;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-Tell-Tabs unsubscribes from MessageProcessed before MessageManager
|
||||||
|
// goes away. Pure-memory cleanup, no framework-thread pflicht.
|
||||||
|
failure = CaptureFailure(failure, () => AutoTellTabsService?.Dispose());
|
||||||
|
|
||||||
|
// v1.4.0 F6.2 — MessageManager has its own async dispose path
|
||||||
|
// (DB flush, pending-message thread shutdown). Run it before the
|
||||||
|
// framework-block so the worker threads are quiesced first.
|
||||||
|
if (MessageManager is not null)
|
||||||
|
{
|
||||||
|
failure = await CaptureFailureAsync(failure, () => MessageManager.DisposeAsync().AsTask())
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (B4) Game-Function / IPC / UI-Window cleanup MUST run on the
|
||||||
|
// framework thread. WindowSystem mutations and IPC subscriber
|
||||||
|
// disposes touch Dalamud state that's only safe from the framework.
|
||||||
|
// Worker-thread DisposeAsync would race the next Draw tick.
|
||||||
|
failure = await CaptureFailureAsync(failure, async () =>
|
||||||
|
{
|
||||||
|
await Framework.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
// Game-Functions first — other services may still query
|
||||||
|
// chat-interactable state during their Dispose.
|
||||||
|
try { GameFunctions.GameFunctions.SetChatInteractable(true); } catch { /* swallowed */ }
|
||||||
|
|
||||||
|
// IPC subscribers — dispose before windows so any final
|
||||||
|
// event firing from the IPC source can't reach a half-torn
|
||||||
|
// ChatLogWindow.
|
||||||
HonorificService?.Dispose();
|
HonorificService?.Dispose();
|
||||||
|
TypingIpc?.Dispose();
|
||||||
|
ExtraChat?.Dispose();
|
||||||
|
Ipc?.Dispose();
|
||||||
|
|
||||||
|
// Windows — RemoveAllWindows first, then per-window Dispose.
|
||||||
|
// Order matches the pre-v1.4.3 Dispose body byte-for-byte.
|
||||||
|
// CommandHelpWindow and FirstRunWizard don't implement
|
||||||
|
// IDisposable; their resources are reclaimed via WindowSystem.
|
||||||
WindowSystem?.RemoveAllWindows();
|
WindowSystem?.RemoveAllWindows();
|
||||||
ChatLogWindow?.Dispose();
|
ChatLogWindow?.Dispose();
|
||||||
DbViewer?.Dispose();
|
DbViewer?.Dispose();
|
||||||
@@ -575,19 +663,34 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
SettingsWindow?.Dispose();
|
SettingsWindow?.Dispose();
|
||||||
DebuggerWindow?.Dispose();
|
DebuggerWindow?.Dispose();
|
||||||
SeStringDebugger?.Dispose();
|
SeStringDebugger?.Dispose();
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
TypingIpc?.Dispose();
|
// Pure-memory cleanups — no Framework / UI / IPC touch, so they
|
||||||
ExtraChat?.Dispose();
|
// run on whatever thread DisposeAsync resumes on.
|
||||||
Ipc?.Dispose();
|
failure = CaptureFailure(failure, () => Functions?.Dispose());
|
||||||
// Dispose the Auto-Tell-Tabs service before MessageManager so it
|
failure = CaptureFailure(failure, () => Commands?.Dispose());
|
||||||
// can cleanly unsubscribe from the MessageProcessed event before
|
failure = CaptureFailure(failure, () => EmoteCache.Dispose());
|
||||||
// its source goes away.
|
|
||||||
AutoTellTabsService?.Dispose();
|
|
||||||
MessageManager?.DisposeAsync().AsTask().Wait();
|
|
||||||
Functions?.Dispose();
|
|
||||||
Commands?.Dispose();
|
|
||||||
|
|
||||||
EmoteCache.Dispose();
|
if (failure is not null)
|
||||||
|
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lightless-pattern capture helpers: run cleanup, remember the FIRST
|
||||||
|
// exception, keep going. Without these one mid-teardown failure would
|
||||||
|
// skip every cleanup behind it and leave services half-torn.
|
||||||
|
private static Exception? CaptureFailure(Exception? failure, Action action)
|
||||||
|
{
|
||||||
|
try { action(); }
|
||||||
|
catch (Exception ex) { failure ??= ex; }
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async ValueTask<Exception?> CaptureFailureAsync(Exception? failure, Func<Task> action)
|
||||||
|
{
|
||||||
|
try { await action().ConfigureAwait(false); }
|
||||||
|
catch (Exception ex) { failure ??= ex; }
|
||||||
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads HellionThemeWindowOpacity from the pre-v13 backup the v12→v13
|
// Reads HellionThemeWindowOpacity from the pre-v13 backup the v12→v13
|
||||||
|
|||||||
Reference in New Issue
Block a user