fix(plugins): close LA5 ownership races

This commit is contained in:
Erik 2026-08-14 19:28:14 +02:00
parent fbe9c8a288
commit f820eb258d
14 changed files with 467 additions and 107 deletions

View file

@ -1,4 +1,5 @@
using AcDream.Plugin.Abstractions;
using System.Runtime.Loader;
namespace AcDream.Plugin.Tests.Fixtures.HostPlugin;
@ -13,6 +14,7 @@ public sealed class HostPlugin : IAcDreamPlugin
private IPluginHost? _host;
private string? _assemblyDirectory;
private bool _throwAfterRegistration;
private bool _throwDuringInitialize;
private int _entitiesSeen;
public void Initialize(IPluginHost host)
@ -22,17 +24,24 @@ public sealed class HostPlugin : IAcDreamPlugin
typeof(HostPlugin).Assembly.Location);
_throwAfterRegistration = File.Exists(
Path.Combine(_assemblyDirectory!, "throw-after-register"));
_throwDuringInitialize = File.Exists(
Path.Combine(_assemblyDirectory!, "throw-during-initialize"));
host.Log.Info($"fixture-initialized:hasUi={host.HasUi}");
if (_throwDuringInitialize)
{
RegisterHostCallbacks(host);
AssemblyLoadContext.GetLoadContext(typeof(HostPlugin).Assembly)!
.Unloading += OnUnloading;
throw new InvalidOperationException(
"fixture initialize failed after registering UI, entity, and selection callbacks");
}
}
public void Enable()
{
IPluginHost host = _host
?? throw new InvalidOperationException("The fixture was not initialized.");
host.Ui.AddMarkupPanel(
Path.Combine(AppContext.BaseDirectory, "fixture-panel.xml"),
this);
host.Events.EntitySpawned += OnEntitySpawned;
RegisterHostCallbacks(host);
if (_throwAfterRegistration)
{
throw new InvalidOperationException(
@ -47,24 +56,75 @@ public sealed class HostPlugin : IAcDreamPlugin
IPluginHost? host = _host;
if (host is null)
return;
if (_throwAfterRegistration)
if (_throwAfterRegistration || _throwDuringInitialize)
{
throw new InvalidOperationException(
"fixture disable intentionally refuses cleanup");
}
host.Events.EntitySpawned -= OnEntitySpawned;
host.Selection.Changed -= OnSelectionChanged;
host.Log.Info($"fixture-disabled:entitiesSeen={_entitiesSeen}");
_host = null;
}
private void RegisterHostCallbacks(IPluginHost host)
{
host.Ui.AddMarkupPanel(
Path.Combine(AppContext.BaseDirectory, "fixture-panel.xml"),
this);
host.Events.EntitySpawned += OnEntitySpawned;
host.Selection.Changed += OnSelectionChanged;
}
private void OnEntitySpawned(WorldEntitySnapshot snapshot)
{
_entitiesSeen++;
if (_throwAfterRegistration && _assemblyDirectory is not null)
RecordUnexpectedCallback(snapshot.Id);
}
private void OnSelectionChanged(SelectionChangedEvent change) =>
RecordUnexpectedCallback(change.SelectedObjectId ?? 0u);
private void RecordUnexpectedCallback(uint objectId)
{
if ((_throwAfterRegistration || _throwDuringInitialize)
&& _assemblyDirectory is not null)
{
File.AppendAllText(
Path.Combine(_assemblyDirectory, "unexpected-callback"),
$"{snapshot.Id}{Environment.NewLine}");
$"{objectId}{Environment.NewLine}");
}
}
private void OnUnloading(AssemblyLoadContext context)
{
IPluginHost host = _host!;
bool uiClosed = Rejects(() => host.Ui.AddMarkupPanel(
Path.Combine(AppContext.BaseDirectory, "unloading-panel.xml"),
this));
bool eventsClosed = Rejects(() =>
{
host.Events.EntitySpawned += OnEntitySpawned;
});
bool selectionClosed = Rejects(() =>
{
host.Selection.Changed += OnSelectionChanged;
});
File.WriteAllText(
Path.Combine(_assemblyDirectory!, "unload-observation"),
$"ui={uiClosed};events={eventsClosed};selection={selectionClosed}");
}
private static bool Rejects(Action action)
{
try
{
action();
return false;
}
catch (ObjectDisposedException)
{
return true;
}
}
}