fix(plugins): close LA5 ownership races
This commit is contained in:
parent
fbe9c8a288
commit
f820eb258d
14 changed files with 467 additions and 107 deletions
|
|
@ -4,13 +4,14 @@ namespace AcDream.Core.Plugins;
|
|||
|
||||
/// <summary>
|
||||
/// Per-plugin host view that owns every registration made through the public
|
||||
/// event/UI surfaces. Disposal is the host's rollback boundary: it removes
|
||||
/// event/selection/UI surfaces. Disposal is the host's rollback boundary: it removes
|
||||
/// registrations even when plugin Initialize/Enable/Disable code throws.
|
||||
/// </summary>
|
||||
internal sealed class ScopedPluginHost : IPluginHost, IDisposable
|
||||
{
|
||||
private readonly IPluginHost _inner;
|
||||
private readonly ScopedEvents _events;
|
||||
private readonly ScopedSelectionService _selection;
|
||||
private readonly ScopedUiRegistry _ui;
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ internal sealed class ScopedPluginHost : IPluginHost, IDisposable
|
|||
{
|
||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||
_events = new ScopedEvents(inner.Events);
|
||||
_selection = new ScopedSelectionService(inner.Selection);
|
||||
_ui = new ScopedUiRegistry(inner.Ui);
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +27,7 @@ internal sealed class ScopedPluginHost : IPluginHost, IDisposable
|
|||
public IPluginLogger Log => _inner.Log;
|
||||
public IGameState State => _inner.State;
|
||||
public IEvents Events => _events;
|
||||
public ISelectionService Selection => _inner.Selection;
|
||||
public ISelectionService Selection => _selection;
|
||||
public IUiRegistry Ui => _ui;
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -34,9 +36,108 @@ internal sealed class ScopedPluginHost : IPluginHost, IDisposable
|
|||
return;
|
||||
_disposed = true;
|
||||
_events.Dispose();
|
||||
_selection.Dispose();
|
||||
_ui.Dispose();
|
||||
}
|
||||
|
||||
private sealed class ScopedSelectionService(ISelectionService inner)
|
||||
: ISelectionService,
|
||||
IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly List<Action<SelectionChangedEvent>> _registrations = [];
|
||||
private bool _disposed;
|
||||
|
||||
public uint? SelectedObjectId => inner.SelectedObjectId;
|
||||
public uint? PreviousObjectId => inner.PreviousObjectId;
|
||||
|
||||
public event Action<SelectionChangedEvent> Changed
|
||||
{
|
||||
add
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
try
|
||||
{
|
||||
inner.Changed += value;
|
||||
}
|
||||
catch
|
||||
{
|
||||
try { inner.Changed -= value; }
|
||||
catch { }
|
||||
throw;
|
||||
}
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_registrations.Add(value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try { inner.Changed -= value; }
|
||||
catch { }
|
||||
throw new ObjectDisposedException(nameof(ScopedSelectionService));
|
||||
}
|
||||
remove
|
||||
{
|
||||
if (value is null)
|
||||
return;
|
||||
inner.Changed -= value;
|
||||
lock (_gate)
|
||||
RemoveLast(value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Select(uint objectId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
return inner.Select(objectId);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Clear()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
return inner.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Action<SelectionChangedEvent>[] registrations;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
registrations = _registrations.ToArray();
|
||||
_registrations.Clear();
|
||||
}
|
||||
|
||||
for (int index = registrations.Length - 1; index >= 0; index--)
|
||||
{
|
||||
try { inner.Changed -= registrations[index]; }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveLast(Action<SelectionChangedEvent> handler)
|
||||
{
|
||||
for (int index = _registrations.Count - 1; index >= 0; index--)
|
||||
{
|
||||
if (_registrations[index] != handler)
|
||||
continue;
|
||||
_registrations.RemoveAt(index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ScopedEvents(IEvents inner) : IEvents, IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue