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:
@@ -13,20 +13,21 @@ namespace HellionChat.Ui;
|
||||
internal sealed class CommandHelpWindow : Window
|
||||
{
|
||||
private readonly ChunkRenderer _chunkRenderer;
|
||||
private readonly Windows.MainWindow _mainWindow;
|
||||
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;
|
||||
|
||||
internal CommandHelpWindow(
|
||||
ChunkRenderer chunkRenderer,
|
||||
Windows.MainWindow mainWindow,
|
||||
ILogger<CommandHelpWindow> logger
|
||||
)
|
||||
internal CommandHelpWindow(ChunkRenderer chunkRenderer, ILogger<CommandHelpWindow> logger)
|
||||
: base("command help##chat2-commandhelp")
|
||||
{
|
||||
_chunkRenderer = chunkRenderer;
|
||||
_mainWindow = mainWindow;
|
||||
_logger = logger;
|
||||
|
||||
Flags =
|
||||
@@ -44,8 +45,18 @@ internal sealed class CommandHelpWindow : Window
|
||||
_ = _logger;
|
||||
}
|
||||
|
||||
internal void AttachMainWindow(Windows.MainWindow mainWindow) => _mainWindow = mainWindow;
|
||||
|
||||
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;
|
||||
|
||||
var width = 350;
|
||||
|
||||
Reference in New Issue
Block a user