fix(plugins): close LA5 host lifecycle review
This commit is contained in:
parent
95f4be94db
commit
fbe9c8a288
25 changed files with 1043 additions and 120 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.App.Plugins;
|
||||
|
|
@ -8,20 +9,119 @@ namespace AcDream.App.Plugins;
|
|||
/// Program.cs before the GL window opens) until GameWindow drains them into the
|
||||
/// UiHost tree after construction.
|
||||
/// </summary>
|
||||
public sealed class BufferedUiRegistry : IUiRegistry
|
||||
public sealed class BufferedUiRegistry : IScopedUiRegistry
|
||||
{
|
||||
public readonly record struct Pending(string MarkupPath, object Binding);
|
||||
public readonly record struct Pending(string MarkupPath, object Binding)
|
||||
{
|
||||
internal long RegistrationId { get; init; }
|
||||
}
|
||||
|
||||
private readonly List<Pending> _pending = new();
|
||||
private sealed class Registration(string markupPath, object binding)
|
||||
{
|
||||
internal string MarkupPath { get; } = markupPath;
|
||||
internal object Binding { get; } = binding;
|
||||
internal bool Drained { get; set; }
|
||||
internal UiRoot? Root { get; set; }
|
||||
internal UiElement? Element { get; set; }
|
||||
}
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly Dictionary<long, Registration> _registrations = [];
|
||||
private long _nextRegistrationId;
|
||||
|
||||
public void AddMarkupPanel(string markupPath, object binding)
|
||||
=> _pending.Add(new Pending(markupPath, binding));
|
||||
=> _ = RegisterMarkupPanel(markupPath, binding);
|
||||
|
||||
/// <summary>Return + clear all buffered registrations.</summary>
|
||||
public IDisposable RegisterMarkupPanel(string markupPath, object binding)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(markupPath);
|
||||
ArgumentNullException.ThrowIfNull(binding);
|
||||
long id;
|
||||
lock (_gate)
|
||||
{
|
||||
id = checked(++_nextRegistrationId);
|
||||
_registrations.Add(id, new Registration(markupPath, binding));
|
||||
}
|
||||
return new RegistrationToken(this, id);
|
||||
}
|
||||
|
||||
/// <summary>Returns each not-yet-drained active registration once.</summary>
|
||||
public IReadOnlyList<Pending> Drain()
|
||||
{
|
||||
var copy = _pending.ToArray();
|
||||
_pending.Clear();
|
||||
return copy;
|
||||
lock (_gate)
|
||||
{
|
||||
var pending = new List<Pending>(_registrations.Count);
|
||||
foreach ((long id, Registration registration) in _registrations)
|
||||
{
|
||||
if (registration.Drained)
|
||||
continue;
|
||||
registration.Drained = true;
|
||||
pending.Add(new Pending(
|
||||
registration.MarkupPath,
|
||||
registration.Binding)
|
||||
{
|
||||
RegistrationId = id,
|
||||
});
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
}
|
||||
|
||||
internal void CompleteMount(Pending pending, UiRoot root, UiElement element)
|
||||
{
|
||||
bool stillRegistered;
|
||||
lock (_gate)
|
||||
{
|
||||
stillRegistered = _registrations.TryGetValue(
|
||||
pending.RegistrationId,
|
||||
out Registration? registration);
|
||||
if (stillRegistered)
|
||||
{
|
||||
registration!.Root = root;
|
||||
registration.Element = element;
|
||||
}
|
||||
}
|
||||
|
||||
// A plugin can fail/disable while markup is being built. Never leave
|
||||
// the just-built child mounted if its host-owned token was rolled back.
|
||||
if (!stillRegistered)
|
||||
root.RemoveChild(element);
|
||||
}
|
||||
|
||||
internal void FailMount(Pending pending) => Remove(pending.RegistrationId);
|
||||
|
||||
internal int RegistrationCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
return _registrations.Count;
|
||||
}
|
||||
}
|
||||
|
||||
private void Remove(long id)
|
||||
{
|
||||
UiRoot? root;
|
||||
UiElement? element;
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_registrations.Remove(id, out Registration? registration))
|
||||
return;
|
||||
root = registration.Root;
|
||||
element = registration.Element;
|
||||
}
|
||||
|
||||
if (root is not null && element is not null)
|
||||
root.RemoveChild(element);
|
||||
}
|
||||
|
||||
private sealed class RegistrationToken(
|
||||
BufferedUiRegistry owner,
|
||||
long registrationId) : IDisposable
|
||||
{
|
||||
private BufferedUiRegistry? _owner = owner;
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.Remove(registrationId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,24 @@ namespace AcDream.App.Plugins;
|
|||
internal sealed class GraphicalPluginSession : IDisposable
|
||||
{
|
||||
private readonly PluginSession _plugins;
|
||||
private readonly string[] _roots;
|
||||
private readonly IReadOnlyList<string>? _allowList;
|
||||
private readonly string _sessionId;
|
||||
private readonly SessionStatusWriter _statusWriter;
|
||||
private bool _started;
|
||||
|
||||
private GraphicalPluginSession(PluginSession plugins)
|
||||
private GraphicalPluginSession(
|
||||
PluginSession plugins,
|
||||
string[] roots,
|
||||
IReadOnlyList<string>? allowList,
|
||||
string sessionId,
|
||||
SessionStatusWriter statusWriter)
|
||||
{
|
||||
_plugins = plugins;
|
||||
_roots = roots;
|
||||
_allowList = allowList;
|
||||
_sessionId = sessionId;
|
||||
_statusWriter = statusWriter;
|
||||
}
|
||||
|
||||
internal int LoadedCount => _plugins.LoadedCount;
|
||||
|
|
@ -25,7 +39,7 @@ internal sealed class GraphicalPluginSession : IDisposable
|
|||
internal IReadOnlyList<WeakReference> CaptureLoadContextWeakReferences() =>
|
||||
_plugins.CaptureLoadContextWeakReferences();
|
||||
|
||||
internal static GraphicalPluginSession Start(
|
||||
internal static GraphicalPluginSession Create(
|
||||
ApplicationPathSet paths,
|
||||
IReadOnlyList<string>? allowList,
|
||||
string sessionId,
|
||||
|
|
@ -40,21 +54,28 @@ internal sealed class GraphicalPluginSession : IDisposable
|
|||
var plugins = new PluginSession(
|
||||
host,
|
||||
status => Report(statusWriter, sessionId, status));
|
||||
try
|
||||
{
|
||||
plugins.Start(
|
||||
return new GraphicalPluginSession(
|
||||
plugins,
|
||||
[
|
||||
Path.Combine(AppContext.BaseDirectory, "plugins"),
|
||||
paths.PluginsDirectory,
|
||||
],
|
||||
allowList);
|
||||
return new GraphicalPluginSession(plugins);
|
||||
}
|
||||
catch
|
||||
{
|
||||
plugins.Dispose();
|
||||
throw;
|
||||
}
|
||||
allowList,
|
||||
sessionId,
|
||||
statusWriter);
|
||||
}
|
||||
|
||||
internal void Start()
|
||||
{
|
||||
if (_started)
|
||||
throw new InvalidOperationException(
|
||||
"The graphical plugin session has already started.");
|
||||
_started = true;
|
||||
|
||||
// Both real hosts publish the same startup prefix: started first,
|
||||
// then one outcome for each configured plugin, then connection work.
|
||||
_statusWriter.Started(_sessionId);
|
||||
_plugins.Start(_roots, _allowList);
|
||||
}
|
||||
|
||||
public void Dispose() => _plugins.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue