149 lines
4.3 KiB
C#
149 lines
4.3 KiB
C#
using System.Runtime.ExceptionServices;
|
|
using Dalamud.Plugin.Services;
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
namespace HellionChat;
|
|
|
|
// Orchestrates Host.StartAsync / StopAsync and the framework-thread dispose.
|
|
// Plugin.ctor builds the host and assigns it via the Host property, so
|
|
// PluginLifecycle never constructs the host itself.
|
|
internal sealed class PluginLifecycle : IAsyncDisposable
|
|
{
|
|
private readonly IFramework _framework;
|
|
private readonly Plugin _plugin;
|
|
|
|
private int _disposeStarted;
|
|
private bool _hostStartRequested;
|
|
|
|
public PluginLifecycle(IFramework framework, Plugin plugin)
|
|
{
|
|
_framework = framework;
|
|
_plugin = plugin;
|
|
}
|
|
|
|
// Plugin.ctor fills this immediately after PluginHostFactory.Build and
|
|
// before invoking LoadAsync; LoadAsync may NRE-suppress on Host! safely.
|
|
public IHost? Host { get; set; }
|
|
|
|
public async Task LoadAsync(CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
try
|
|
{
|
|
_hostStartRequested = true;
|
|
await Host!.StartAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
// WindowSystem.AddWindow mutates an internal List<>; v1.4.9 Stage-2
|
|
// verified the list is non-thread-safe, so we marshal the entire
|
|
// registration block to the framework thread.
|
|
await _framework
|
|
.RunOnFrameworkThread(() => RegisterWindows(_plugin))
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
await DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
// Swallow secondary dispose failure so the original load throw wins.
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private static void RegisterWindows(Plugin plugin)
|
|
{
|
|
plugin.WindowSystem.AddWindow(plugin.MainWindow);
|
|
plugin.WindowSystem.AddWindow(plugin.SettingsWindow);
|
|
plugin.WindowSystem.AddWindow(plugin.DbViewer);
|
|
plugin.WindowSystem.AddWindow(Plugin.InputPreview);
|
|
plugin.WindowSystem.AddWindow(plugin.CommandHelpWindow);
|
|
plugin.WindowSystem.AddWindow(plugin.SeStringDebugger);
|
|
plugin.WindowSystem.AddWindow(plugin.DebuggerWindow);
|
|
plugin.WindowSystem.AddWindow(plugin.FirstRunWizard);
|
|
|
|
// Pop-out pool: register all pre-allocated instances ONCE here on the
|
|
// framework thread. Open/Close at runtime is IsOpen-only, never AddWindow.
|
|
foreach (var popout in plugin.ChannelPopoutPool.Instances)
|
|
plugin.WindowSystem.AddWindow(popout);
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
|
|
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
|
|
return;
|
|
|
|
Exception? failure = null;
|
|
|
|
if (_hostStartRequested && Host is not null)
|
|
failure = await CaptureFailureAsync(failure, () => Host.StopAsync())
|
|
.ConfigureAwait(false);
|
|
|
|
failure = await DisposeHostOnFrameworkThreadAsync(failure).ConfigureAwait(false);
|
|
|
|
ThrowIfFailed(failure);
|
|
}
|
|
|
|
private async Task<Exception?> DisposeHostOnFrameworkThreadAsync(Exception? failure)
|
|
{
|
|
try
|
|
{
|
|
await _framework
|
|
.RunOnFrameworkThread(() =>
|
|
{
|
|
failure = CaptureFailure(failure, () => Host?.Dispose());
|
|
})
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failure ??= ex;
|
|
}
|
|
|
|
return failure;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private static void ThrowIfFailed(Exception? failure)
|
|
{
|
|
if (failure is not null)
|
|
ExceptionDispatchInfo.Capture(failure).Throw();
|
|
}
|
|
}
|