fix(plugins): close LA5 host lifecycle review

This commit is contained in:
Erik 2026-08-14 19:05:13 +02:00
parent 95f4be94db
commit fbe9c8a288
25 changed files with 1043 additions and 120 deletions

View file

@ -27,7 +27,8 @@ public sealed class PluginSession : IDisposable
{
private readonly IPluginHost _host;
private readonly Action<PluginSessionStatus>? _report;
private readonly List<LoadedPlugin> _loaded = [];
private readonly List<ActivePlugin> _loaded = [];
private readonly List<WeakReference> _releasedContexts = [];
private bool _started;
private bool _disposed;
@ -42,7 +43,7 @@ public sealed class PluginSession : IDisposable
public int LoadedCount => _loaded.Count;
public IReadOnlyList<string> LoadedPluginIds =>
_loaded.Select(static plugin => plugin.Manifest.Id).ToArray();
_loaded.Select(static active => active.Loaded.Manifest.Id).ToArray();
/// <summary>
/// Discovers and starts the configured set exactly once. A
@ -143,9 +144,11 @@ public sealed class PluginSession : IDisposable
/// owned by this session. The returned weak references do not delay unload.
/// </summary>
public IReadOnlyList<WeakReference> CaptureLoadContextWeakReferences() =>
_loaded
.Select(static plugin => new WeakReference(plugin.LoadContext!))
.ToArray();
[
.. _releasedContexts,
.. _loaded.Select(static active =>
new WeakReference(active.Loaded.LoadContext!)),
];
public void Dispose()
{
@ -155,7 +158,8 @@ public sealed class PluginSession : IDisposable
for (int index = _loaded.Count - 1; index >= 0; index--)
{
LoadedPlugin loaded = _loaded[index];
ActivePlugin active = _loaded[index];
LoadedPlugin loaded = active.Loaded;
try
{
loaded.Plugin!.Disable();
@ -169,6 +173,11 @@ public sealed class PluginSession : IDisposable
error);
}
// Host-owned registrations are released even when Disable throws.
// This must precede ALC unload so no UI binding or event delegate
// can keep the plugin assembly reachable.
active.Scope.Dispose();
try
{
loaded.LoadContext!.Unload();
@ -198,12 +207,16 @@ public sealed class PluginSession : IDisposable
{
foreach (PluginDiscoveryResult candidate in available)
{
var scope = new ScopedPluginHost(_host);
LoadedPlugin loaded = PluginLoader.Load(
candidate.PluginDirectory,
candidate.Manifest!,
_host);
scope);
if (!loaded.Success)
{
scope.Dispose();
if (loaded.ReleasedLoadContext is { } released)
_releasedContexts.Add(released);
AddError(
errors,
id,
@ -215,7 +228,7 @@ public sealed class PluginSession : IDisposable
try
{
loaded.Plugin!.Enable();
_loaded.Add(loaded);
_loaded.Add(new ActivePlugin(loaded, scope));
SafeLog(
static (log, message, _) => log.Info(message),
$"plugin loaded: {loaded.Manifest.Id} "
@ -229,7 +242,7 @@ public sealed class PluginSession : IDisposable
catch (Exception error)
{
AddError(errors, id, error);
ReleaseFailedEnable(loaded);
ReleaseFailedEnable(loaded, scope);
}
}
}
@ -257,7 +270,9 @@ public sealed class PluginSession : IDisposable
null);
}
private void ReleaseFailedEnable(LoadedPlugin loaded)
private void ReleaseFailedEnable(
LoadedPlugin loaded,
ScopedPluginHost scope)
{
try
{
@ -272,6 +287,9 @@ public sealed class PluginSession : IDisposable
error);
}
scope.Dispose();
_releasedContexts.Add(new WeakReference(loaded.LoadContext!));
try
{
loaded.LoadContext!.Unload();
@ -358,4 +376,8 @@ public sealed class PluginSession : IDisposable
or ArgumentException
or NotSupportedException
or System.Security.SecurityException;
private sealed record ActivePlugin(
LoadedPlugin Loaded,
ScopedPluginHost Scope);
}