From d0074a6fc722d8e60231264381b6a4f0e56e9c27 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Mar 2025 20:34:30 +0200 Subject: [PATCH] Added plugin hot reloading (#377) --- .../Layout/MainLayout.razor.cs | 3 ++ app/MindWork AI Studio/Pages/Plugins.razor.cs | 28 +++++++++++++++- app/MindWork AI Studio/Program.cs | 2 ++ app/MindWork AI Studio/Tools/Event.cs | 1 + .../PluginSystem/PluginFactory.HotReload.cs | 33 +++++++++++++++++++ .../PluginSystem/PluginFactory.Internal.cs | 2 -- .../Tools/PluginSystem/PluginFactory.cs | 9 +++++ .../wwwroot/changelog/v0.9.39.md | 1 + 8 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index b689569b..4c45a3b9 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -94,6 +94,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis // Load (but not start) all plugins, without waiting for them: var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); _ = PluginFactory.LoadAll(pluginLoadingTimeout.Token); + + // Set up hot reloading for plugins: + PluginFactory.SetUpHotReloading(); } // Register this component with the message bus: diff --git a/app/MindWork AI Studio/Pages/Plugins.razor.cs b/app/MindWork AI Studio/Pages/Plugins.razor.cs index f2dc83d9..9504c0ff 100644 --- a/app/MindWork AI Studio/Pages/Plugins.razor.cs +++ b/app/MindWork AI Studio/Pages/Plugins.razor.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Components; namespace AIStudio.Pages; -public partial class Plugins : ComponentBase +public partial class Plugins : ComponentBase, IMessageBusReceiver { private const string GROUP_ENABLED = "Enabled"; private const string GROUP_DISABLED = "Disabled"; @@ -23,6 +23,9 @@ public partial class Plugins : ComponentBase protected override async Task OnInitializedAsync() { + this.MessageBus.RegisterComponent(this); + this.MessageBus.ApplyFilters(this, [], [ Event.PLUGINS_RELOADED ]); + this.groupConfig = new TableGroupDefinition { Expandable = true, @@ -53,4 +56,27 @@ public partial class Plugins : ComponentBase await this.SettingsManager.StoreSettings(); await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); } + + #region Implementation of IMessageBusReceiver + + public string ComponentName => nameof(Plugins); + + public Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) + { + switch (triggeredEvent) + { + case Event.PLUGINS_RELOADED: + this.InvokeAsync(this.StateHasChanged); + break; + } + + return Task.CompletedTask; + } + + public Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) + { + return Task.FromResult(default); + } + + #endregion } \ No newline at end of file diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index cc37d9cd..8283a481 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -1,5 +1,6 @@ using AIStudio.Agents; using AIStudio.Settings; +using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -209,6 +210,7 @@ internal sealed class Program await serverTask; RUST_SERVICE.Dispose(); + PluginFactory.Dispose(); programLogger.LogInformation("The AI Studio server was stopped."); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index 45005fd1..38235c8e 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -8,6 +8,7 @@ public enum Event STATE_HAS_CHANGED, CONFIGURATION_CHANGED, COLOR_THEME_CHANGED, + PLUGINS_RELOADED, // Update events: USER_SEARCH_FOR_UPDATE, diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs new file mode 100644 index 00000000..c2d75bf3 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs @@ -0,0 +1,33 @@ +namespace AIStudio.Tools.PluginSystem; + +public static partial class PluginFactory +{ + public static void SetUpHotReloading() + { + LOG.LogInformation($"Start hot reloading plugins for path '{HOT_RELOAD_WATCHER.Path}'."); + try + { + var messageBus = Program.SERVICE_PROVIDER.GetRequiredService(); + + HOT_RELOAD_WATCHER.IncludeSubdirectories = true; + HOT_RELOAD_WATCHER.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName; + HOT_RELOAD_WATCHER.Filter = "*.lua"; + HOT_RELOAD_WATCHER.Changed += async (_, args) => + { + LOG.LogInformation($"File changed: {args.FullPath}"); + await LoadAll(); + await messageBus.SendMessage(null, Event.PLUGINS_RELOADED); + }; + + HOT_RELOAD_WATCHER.EnableRaisingEvents = true; + } + catch (Exception e) + { + LOG.LogError(e, "Error while setting up hot reloading."); + } + finally + { + LOG.LogInformation("Hot reloading plugins set up."); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs index 316376f6..272d5af3 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs @@ -1,5 +1,3 @@ -using System.Reflection; - using Microsoft.Extensions.FileProviders; namespace AIStudio.Tools.PluginSystem; diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index 81837f73..0cb87178 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -16,6 +16,8 @@ public static partial class PluginFactory private static readonly string PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins"); private static readonly string INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal"); + + private static readonly FileSystemWatcher HOT_RELOAD_WATCHER = new(PLUGINS_ROOT); private static readonly List AVAILABLE_PLUGINS = []; @@ -45,6 +47,8 @@ public static partial class PluginFactory return; } + AVAILABLE_PLUGINS.Clear(); + // // The easiest way to load all plugins is to find all `plugin.lua` files and load them. // By convention, each plugin is enforced to have a `plugin.lua` file. @@ -136,4 +140,9 @@ public static partial class PluginFactory _ => new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.") }; } + + public static void Dispose() + { + HOT_RELOAD_WATCHER.Dispose(); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md index 94f9958e..9f573df3 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md @@ -2,5 +2,6 @@ - Added a feature flag for the plugin system. This flag is disabled by default and can be enabled inside the app settings. Please note that this feature is still in development; there are no plugins available yet. - Added the Lua library we use for the plugin system to the about page. - Added the plugin overview page. This page shows all installed plugins and allows you to enable or disable them. It is only available when the plugin preview feature is enabled. +- Added hot reloading for plugins. When any plugin is changed, the app will automatically reload the plugin without needing to restart the app. - Fixed the preview tooltip component not showing the correct position when used inside a scrollable container. - Upgraded to Rust 1.85.1 \ No newline at end of file