From c1e06e6a011e2079ff1a115749716f0825a18eee Mon Sep 17 00:00:00 2001 From: Fred K Date: Tue, 23 Dec 2025 22:36:50 -0500 Subject: [PATCH] fix(core): A couple plugin manager related fixes. In PluginManager: * Strip suffixes of semantic versioned plugins (ie. `-beta`) using a regex. It doesn't contribute to the actual versioning. The built-in version parser does not handle this, so just strip it. In PluginList: * Wrap the building of the plugin list UI in the TableLayoutPanel with SuspendLayout() / ResumeLayout() to improve performance. * Avoid random layout engine failures (ie. index-out-of-bounds and null dereference errors) resulting from lack of thread safety in the layout engine caused by running multiple threads which manipulate the plugin list. I was able to consistently reproduce this with 4 plugins triggering an update to the plugin list before my fix, and cannot trigger it now. See screenshot for visual of broken UI that results. Instead of firing the update-check tasks immediately during main layout of the plugin list, collect the tasks **un-started** and run them one at a time on a separate non-blocking task after completing the layout of the plugin list UI. These update check tasks may take slightly longer due to running serially, it should not be noticeable because they should not block the main thread. --- .../PluginManagement/PluginManager.cs | 14 ++++++---- ObservatoryCore/UI/PluginList.cs | 26 ++++++++++++++++--- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/ObservatoryCore/PluginManagement/PluginManager.cs b/ObservatoryCore/PluginManagement/PluginManager.cs index 1cbfbfd..79fa96c 100644 --- a/ObservatoryCore/PluginManagement/PluginManager.cs +++ b/ObservatoryCore/PluginManagement/PluginManager.cs @@ -3,13 +3,14 @@ using Observatory.Utils; using System.Data; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO.Compression; using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; using System.Text.Json; using System.Text.Json.Serialization; -using System.Runtime.Loader; -using System.IO.Compression; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; +using System.Text.RegularExpressions; namespace Observatory.PluginManagement { @@ -791,6 +792,8 @@ private void ReadDirectory(DirectoryInfo directory) private class PluginPackage { + private static readonly Regex VersionSuffixRegEx = new("-.*$"); + internal PluginPackage(string filePath, bool includeLegacyDeps = false) { var bundle = new PluginBundle(filePath); @@ -858,7 +861,8 @@ private void ProcessPluginManifest(PluginBundle bundle) var nameAndVersion = pluginLibraryEntry.Key.Split('/') ?? ["No Library", "0"]; PluginName = nameAndVersion.First() ?? string.Empty; - Version = new Version(nameAndVersion.Last() ?? "0"); + var versionWithoutSuffx = VersionSuffixRegEx.Replace(nameAndVersion.Last() ?? "0", ""); + Version = new Version(versionWithoutSuffx); foreach (var dependency in targets.Where(t => t.Key != pluginLibraryEntry.Key)) { diff --git a/ObservatoryCore/UI/PluginList.cs b/ObservatoryCore/UI/PluginList.cs index 35f81f7..2a2d303 100644 --- a/ObservatoryCore/UI/PluginList.cs +++ b/ObservatoryCore/UI/PluginList.cs @@ -13,6 +13,8 @@ internal class PluginList : TableLayoutPanel internal PluginList(IEnumerable plugins) { + SuspendLayout(); + ColumnCount = 7; _title = new() { @@ -62,6 +64,7 @@ Label createHeaderLabel(string text) int row = 2; var enabledPlugins = GetEnabledPlugins(); + List updateTasks = new(); foreach (var plugin in plugins) { @@ -162,7 +165,7 @@ Label createLineLabel(string text) AddWithLocation(pluginEnabled, row, 5); AddWithLocation(pluginMenu, row, 6); - Task.Run(() => + updateTasks.Add(new Task(() => { var thisRow = row; // Check for a plugin update. @@ -198,15 +201,32 @@ Label createLineLabel(string text) var startInfo = new ProcessStartInfo(updateInfo.Url ?? "https://observatory.xjph.net") { UseShellExecute = true }; Process.Start(startInfo); }; - AddWithLocation(updateLink, GetRow(pluginStatus), 4); + var row = GetRow(pluginStatus); Controls.Remove(pluginStatus); + AddWithLocation(updateLink, row, 4); } - }); + })); row++; } + ResumeLayout(); + Resize += PluginList_Resize; + + // Avoid blocking the main thread by running one or more update tasks. + Task.Run(() => + { + // Run the update tasks now that we've completely laid out the plugin list -- one at a time. + // This avoids the these tasks and the main thread trigging layout passes on the underlying TableLayoutPanel + // that end up conflicting with each other causing mysterious index-out-of-bounds and null errors deep + // within the Layout engine. + foreach (var t in updateTasks) + { + t.Start(); + t.Wait(); + } + }); } private void PluginList_Resize(object? sender, EventArgs e)