fix(host): break InputBar/CommandHelpWindow/MainWindow DI cycle
J + J2 closed a singleton cycle:
InputBar.ctor -> CommandHelpWindow (J2)
CommandHelpWindow.ctor -> MainWindow (J)
MainWindow.ctor -> InputBar (pre-existing)
MS.DI does not detect cycles through FactoryCallSite registrations,
so resolution recursed silently on the async plugin-init thread until
the worker died with an uncatchable StackOverflowException. Dalamud's
LoadAsync task never resolved; the plugin UI hung on "Enabling..."
with no exception in the log. First triggered at Plugin.cs:289
(TypingIpc.ctor needs InputBar).
Fix: break the cycle on the laziest edge.
- CommandHelpWindow.ctor no longer takes MainWindow.
- New AttachMainWindow setter wired in
CommandHelpWindowInitHostedService.StartAsync, mirroring the
existing §6.2 MessageList.AttachPayloadHandler pattern.
- UpdateContent throws InvalidOperationException if the setter
never ran, so a future regression fails loudly instead of a
silent NullRef during input draw.
Also enable UseDefaultServiceProvider(ValidateOnBuild + ValidateScopes)
so future ConstructorCallSite cycles throw at Build time instead of
silently hanging. Catches reflection-based registrations; will not
catch FactoryCallSite cycles like this one (those still need code review).
Verified via 6 enable/disable cycles in-game; plugin loads cleanly,
Hosting starts, FilterAllTabs completes, command help popup renders
for /em and /say (exercises AttachMainWindow), hover counter ticks
(exercises PayloadHandlerInitHostedService AddonLifecycle wiring).
This commit is contained in:
@@ -3,7 +3,9 @@ using Dalamud.Plugin;
|
|||||||
using HellionChat.Integrations;
|
using HellionChat.Integrations;
|
||||||
using HellionChat.Ipc;
|
using HellionChat.Ipc;
|
||||||
using HellionChat.Themes;
|
using HellionChat.Themes;
|
||||||
|
using HellionChat.Ui;
|
||||||
using HellionChat.Ui.Components;
|
using HellionChat.Ui.Components;
|
||||||
|
using HellionChat.Ui.Windows;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace HellionChat.Infrastructure.Hosting;
|
namespace HellionChat.Infrastructure.Hosting;
|
||||||
@@ -142,3 +144,23 @@ internal sealed class PayloadHandlerInitHostedService(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wires MainWindow into CommandHelpWindow post-container-build. CommandHelpWindow
|
||||||
|
// cannot take MainWindow as a ctor-param because that would close the cycle
|
||||||
|
// InputBar -> CommandHelpWindow -> MainWindow -> InputBar (MS.DI does not catch
|
||||||
|
// it through FactoryCallSite registrations and the resolve recurses silently).
|
||||||
|
// Both singletons exist by host.StartAsync time, so this is the first safe point
|
||||||
|
// to wire the setter — same §6.2 pattern as MessageList.AttachPayloadHandler.
|
||||||
|
internal sealed class CommandHelpWindowInitHostedService(
|
||||||
|
CommandHelpWindow commandHelpWindow,
|
||||||
|
MainWindow mainWindow
|
||||||
|
) : IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
commandHelpWindow.AttachMainWindow(mainWindow);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ internal static class PluginHostFactory
|
|||||||
logging.AddDalamudLogging(dependencies.PluginLog);
|
logging.AddDalamudLogging(dependencies.PluginLog);
|
||||||
logging.SetMinimumLevel(LogLevel.Trace);
|
logging.SetMinimumLevel(LogLevel.Trace);
|
||||||
})
|
})
|
||||||
|
// ValidateOnBuild eagerly instantiates every singleton at Build time
|
||||||
|
// so missing registrations / ConstructorCallSite cycles throw on
|
||||||
|
// load instead of producing a silent hang. ValidateScopes is cheap
|
||||||
|
// (we only use singletons) but guards against future Scoped misuse.
|
||||||
|
.UseDefaultServiceProvider(o =>
|
||||||
|
{
|
||||||
|
o.ValidateOnBuild = true;
|
||||||
|
o.ValidateScopes = true;
|
||||||
|
})
|
||||||
.ConfigureServices(services => ConfigureServices(services, plugin, dependencies))
|
.ConfigureServices(services => ConfigureServices(services, plugin, dependencies))
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
@@ -268,9 +277,11 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<Ui.Components.InputBar>(),
|
sp.GetRequiredService<Ui.Components.InputBar>(),
|
||||||
sp.GetRequiredService<ILogger<InputPreview>>()
|
sp.GetRequiredService<ILogger<InputPreview>>()
|
||||||
));
|
));
|
||||||
|
// No MainWindow ctor-param: breaks the InputBar -> CommandHelpWindow ->
|
||||||
|
// MainWindow -> InputBar singleton cycle. MainWindow is wired post-build
|
||||||
|
// via CommandHelpWindowInitHostedService.
|
||||||
services.AddSingleton(sp => new CommandHelpWindow(
|
services.AddSingleton(sp => new CommandHelpWindow(
|
||||||
sp.GetRequiredService<Ui.Components.ChunkRenderer>(),
|
sp.GetRequiredService<Ui.Components.ChunkRenderer>(),
|
||||||
sp.GetRequiredService<Ui.Windows.MainWindow>(),
|
|
||||||
sp.GetRequiredService<ILogger<CommandHelpWindow>>()
|
sp.GetRequiredService<ILogger<CommandHelpWindow>>()
|
||||||
));
|
));
|
||||||
services.AddSingleton(sp => new SeStringDebugger(sp.GetRequiredService<Plugin>()));
|
services.AddSingleton(sp => new SeStringDebugger(sp.GetRequiredService<Plugin>()));
|
||||||
@@ -312,6 +323,10 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<PayloadHandler>(),
|
sp.GetRequiredService<PayloadHandler>(),
|
||||||
sp.GetRequiredService<Ui.Components.MessageList>()
|
sp.GetRequiredService<Ui.Components.MessageList>()
|
||||||
));
|
));
|
||||||
|
services.AddHostedService(sp => new CommandHelpWindowInitHostedService(
|
||||||
|
sp.GetRequiredService<Ui.CommandHelpWindow>(),
|
||||||
|
sp.GetRequiredService<Ui.Windows.MainWindow>()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PayloadHandler MakePayloadHandler(IServiceProvider sp) =>
|
private static PayloadHandler MakePayloadHandler(IServiceProvider sp) =>
|
||||||
|
|||||||
@@ -13,20 +13,21 @@ namespace HellionChat.Ui;
|
|||||||
internal sealed class CommandHelpWindow : Window
|
internal sealed class CommandHelpWindow : Window
|
||||||
{
|
{
|
||||||
private readonly ChunkRenderer _chunkRenderer;
|
private readonly ChunkRenderer _chunkRenderer;
|
||||||
private readonly Windows.MainWindow _mainWindow;
|
|
||||||
private readonly ILogger<CommandHelpWindow> _logger;
|
private readonly ILogger<CommandHelpWindow> _logger;
|
||||||
|
|
||||||
|
// Setter-injected post-ctor to break the InputBar -> CommandHelpWindow ->
|
||||||
|
// MainWindow -> InputBar singleton cycle (MS.DI does not detect cycles
|
||||||
|
// through FactoryCallSite registrations). Wired in
|
||||||
|
// CommandHelpWindowInitHostedService.StartAsync, same §6.2 pattern as
|
||||||
|
// MessageList.AttachPayloadHandler.
|
||||||
|
private Windows.MainWindow? _mainWindow;
|
||||||
|
|
||||||
private ReadOnlySeString? _commandDescription;
|
private ReadOnlySeString? _commandDescription;
|
||||||
|
|
||||||
internal CommandHelpWindow(
|
internal CommandHelpWindow(ChunkRenderer chunkRenderer, ILogger<CommandHelpWindow> logger)
|
||||||
ChunkRenderer chunkRenderer,
|
|
||||||
Windows.MainWindow mainWindow,
|
|
||||||
ILogger<CommandHelpWindow> logger
|
|
||||||
)
|
|
||||||
: base("command help##chat2-commandhelp")
|
: base("command help##chat2-commandhelp")
|
||||||
{
|
{
|
||||||
_chunkRenderer = chunkRenderer;
|
_chunkRenderer = chunkRenderer;
|
||||||
_mainWindow = mainWindow;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
Flags =
|
Flags =
|
||||||
@@ -44,8 +45,18 @@ internal sealed class CommandHelpWindow : Window
|
|||||||
_ = _logger;
|
_ = _logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void AttachMainWindow(Windows.MainWindow mainWindow) => _mainWindow = mainWindow;
|
||||||
|
|
||||||
public void UpdateContent(ReadOnlySeString commandDesc)
|
public void UpdateContent(ReadOnlySeString commandDesc)
|
||||||
{
|
{
|
||||||
|
// Loud-fail if the HostedService didn't run AttachMainWindow before
|
||||||
|
// the first slash-command call — better than a silent NullRef during
|
||||||
|
// input draw.
|
||||||
|
if (_mainWindow is null)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"CommandHelpWindow.UpdateContent called before AttachMainWindow."
|
||||||
|
);
|
||||||
|
|
||||||
_commandDescription = commandDesc;
|
_commandDescription = commandDesc;
|
||||||
|
|
||||||
var width = 350;
|
var width = 350;
|
||||||
|
|||||||
Reference in New Issue
Block a user