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:
2026-05-28 08:16:27 +02:00
parent f6749d206b
commit 24d3f69041
3 changed files with 56 additions and 8 deletions
+18 -7
View File
@@ -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;