diff --git a/.github/forge-posts/v1.4.0.md b/.github/forge-posts/v1.4.0.md new file mode 100644 index 0000000..8e91459 --- /dev/null +++ b/.github/forge-posts/v1.4.0.md @@ -0,0 +1,32 @@ +--- +subtitle: Critical Lifecycle Fixes +versionsnatur: Stability-Hotfix +--- + +**Hellion Chat 1.4.0 — Critical Lifecycle Fixes** + +Erster Sub-Patch der v1.4.x Polish-Sweep-Serie. Sieben +bekannte Lifecycle- und Race-Bugs aus den Audit-Pässen +abgearbeitet, bevor Performance- und Architektur-Refactors +draufkommen. + +- **SQLite-Dispose** lehnt sich nicht mehr an GC-Druck zur + Datei-Freigabe an, Pooling=false auf der Connection macht + den manuellen GC.Collect überflüssig +- **Worker-Threads** (PendingMessage, RetentionSweep) sind + jetzt explizit IsBackground=true, das Plugin-Domain kann + sauber unloaden bei XIVLauncher-Reload ohne darauf zu warten +- **EmoteCache-Loader** von async-void auf async-Task mit + shared Task-Tracker, drain-on-Dispose. Kein Schreib-Risiko + mehr auf disposed EmoteImages-Einträge nach Plugin-Reload +- **DisposeAsync-Timeout** (10s) warnt jetzt laut statt silent + zu failen +- **Plugin-Dispose** flushed pending DeferredSave bevor Services + abgebaut werden, Settings-Änderungen aus den letzten Frames + vor Disable überleben jetzt zuverlässig +- **v13→v14 Config-Migration** liest pre-v13-Backup und überträgt + HellionThemeWindowOpacity in das neue WindowOpacity-Feld statt + auf 0.85 zurückzufallen + +Keine Schema-Bumps, keine User-sichtbaren Funktions-Änderungen +außer dass Reload und Shutdown spürbar sauberer laufen. diff --git a/HellionChat/EmoteCache.cs b/HellionChat/EmoteCache.cs index 97d5dc8..eff10ad 100644 --- a/HellionChat/EmoteCache.cs +++ b/HellionChat/EmoteCache.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.Collections.Concurrent; +using System.Numerics; using System.Text.Json; using System.Text.Json.Serialization; using Dalamud.Interface.Textures; @@ -73,6 +74,19 @@ public static class EmoteCache private static CancellationTokenSource Cts = new(); internal static CancellationToken Token => Cts.Token; + // Drain target for in-flight loads on Dispose; without this an orphan + // continuation could still write to a torn-down Texture/Frames field. + private static readonly ConcurrentBag PendingLoads = new(); + + internal static void TrackLoad(Task loadTask, string emoteCode) + { + PendingLoads.Add(loadTask.ContinueWith(t => + { + if (t.IsFaulted) + Plugin.Log.Error(t.Exception!, $"EmoteCache load failed for {emoteCode}"); + }, TaskScheduler.Default)); + } + public static async Task LoadData() { if (State is not LoadingState.Unloaded) @@ -135,10 +149,20 @@ public static class EmoteCache public static void Dispose() { - // Cancel in-flight downloads / texture creates so the async-void - // Load methods bail out before they touch a disposed TextureProvider. Cts.Cancel(); + // 5s upper bound; anything still running gets abandoned. + try + { + Task.WaitAll(PendingLoads.ToArray(), TimeSpan.FromSeconds(5)); + } + catch (AggregateException) + { + // Faults already logged in TrackLoad. + } + + while (PendingLoads.TryTake(out _)) { } + foreach (var emote in EmoteImages.Values) emote.InnerDispose(); } @@ -233,11 +257,12 @@ public static class EmoteCache public ImGuiEmote Prepare(Emote emote) { var ct = EmoteCache.Token; - Task.Run(() => Load(emote, ct), ct); + // Task.Run keeps the sync prefix off the ImGui render thread. + EmoteCache.TrackLoad(Task.Run(() => LoadAsyncTracked(emote, ct), ct), emote.Code); return this; } - private async void Load(Emote emote, CancellationToken ct) + private async Task LoadAsyncTracked(Emote emote, CancellationToken ct) { try { @@ -251,8 +276,6 @@ public static class EmoteCache } catch (OperationCanceledException) { - // Plugin disposed mid-load; the EmoteImages entry is also - // being torn down, no extra cleanup needed. } catch (Exception ex) { @@ -310,11 +333,11 @@ public static class EmoteCache public ImGuiGif Prepare(Emote emote) { var ct = EmoteCache.Token; - Task.Run(() => Load(emote, ct), ct); + EmoteCache.TrackLoad(Task.Run(() => LoadAsyncTracked(emote, ct), ct), emote.Code); return this; } - private async void Load(Emote emote, CancellationToken ct) + private async Task LoadAsyncTracked(Emote emote, CancellationToken ct) { try { diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 1190e83..98f58fa 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -4,7 +4,7 @@ 0.1.0 is our bootstrap release; the underlying Chat 2 base is called out in the yaml changelog so users can see what it derives from. --> - 1.3.0 + 1.4.0 enable enable