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.Ipc;
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Ui;
|
||||
using HellionChat.Ui.Components;
|
||||
using HellionChat.Ui.Windows;
|
||||
using Microsoft.Extensions.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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user