diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index e799129..35de71c 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -126,6 +126,12 @@ public sealed class Plugin : IAsyncDalamudPlugin // Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race. private int _disposeStarted; + // Set in the first DisposeAsync statement so async callbacks scheduled + // via Framework.RunOnTick (v1.4.8 B3 retention sweep) can early-bail + // before they touch state that has already been torn down. Volatile + // because the tick reads it from a different thread than the writer. + private volatile bool _isDisposing; + internal int DeferredSaveFrames = -1; // Cancels the v1.4.8 FTS5 bulk-insert worker on plugin teardown. The @@ -440,6 +446,12 @@ public sealed class Plugin : IAsyncDalamudPlugin if (Interlocked.Exchange(ref _disposeStarted, 1) != 0) return; + // Set before any cleanup so deferred Framework.RunOnTick callbacks + // (B3 retention sweep) see the flag and bail out before they touch + // MessageManager / Log / static fields that the rest of this method + // is about to tear down. + _isDisposing = true; + Exception? failure = null; // Unsubscribe hooks first — mirrors the hooks-last subscribe order in LoadAsync. @@ -711,15 +723,31 @@ public sealed class Plugin : IAsyncDalamudPlugin if (deleted > 0) { Log.Information($"Retention sweep deleted {deleted} expired messages."); - // Run clear+refilter on the framework thread — FilterAllTabsAsync - // is fire-and-forget and would race the next sweep cycle. - Framework - .Run(() => + // Schedule on the next framework tick to avoid the ~194ms + // hitch from blocking with .Wait() while the framework + // finishes the current frame. Tabs-list mutation must + // stay on the framework thread because Plugin.Config.Tabs + // (Configuration.cs:222) is not lock-protected and + // AutoTellTabsService can mutate it from background paths. + // Pattern reference: SimpleTweaks + // Tweaks/Chat/CaseInsensitiveCommands.cs:45. + Framework.RunOnTick(() => + { + // The retention thread is IsBackground=true so plugin + // unload can fire while a scheduled tick is still + // pending; bail before touching anything torn down. + if (_isDisposing) + return; + try { MessageManager.ClearAllTabs(); MessageManager.FilterAllTabs(); - }) - .Wait(); + } + catch (Exception ex) + { + Log.Error(ex, "Retention sweep clear+refilter failed"); + } + }); } else {