From 0c44e4ba22a9ec4711982a5dac9ee2ec4c17b12a Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Sun, 16 Apr 2023 17:35:43 +0100 Subject: [PATCH 1/2] Core - Added plugin hot reloading --- .../Services/PluginManagementService.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index c03aec7bb..1e24feb47 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -5,6 +5,7 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Artemis.Core.DeviceProviders; using Artemis.Core.DryIoc; @@ -30,6 +31,7 @@ internal class PluginManagementService : IPluginManagementService private readonly IPluginRepository _pluginRepository; private readonly List _plugins; private readonly IQueuedActionRepository _queuedActionRepository; + private FileSystemWatcher _hotReloadWatcher; private bool _disposed; private bool _isElevated; @@ -43,6 +45,8 @@ internal class PluginManagementService : IPluginManagementService _plugins = new List(); ProcessPluginDeletionQueue(); + + StartHotReload(); } private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory) @@ -218,6 +222,14 @@ internal class PluginManagementService : IPluginManagementService return null; } + private Plugin? GetPluginByDirectory(DirectoryInfo directory) + { + lock (_plugins) + { + return _plugins.FirstOrDefault(p => p.Directory.FullName == directory.FullName); + } + } + public void Dispose() { // Disposal happens manually before container disposal but the container doesn't know that so a 2nd call will be made @@ -912,6 +924,67 @@ internal class PluginManagementService : IPluginManagementService } #endregion + + #region Hot Reload + + private void StartHotReload() + { + // Watch for changes in the plugin directory, "plugin.json". + // If this file is changed, reload the plugin. + _hotReloadWatcher = new FileSystemWatcher(Constants.PluginsFolder, "plugin.json"); + _hotReloadWatcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.FileName; + _hotReloadWatcher.Created += FileSystemWatcherOnCreated; + _hotReloadWatcher.Error += FileSystemWatcherOnError; + _hotReloadWatcher.IncludeSubdirectories = true; + _hotReloadWatcher.EnableRaisingEvents = true; + } + + private void FileSystemWatcherOnError(object sender, ErrorEventArgs e) + { + _logger.Error(e.GetException(), "File system watcher error"); + } + + private void FileSystemWatcherOnCreated(object sender, FileSystemEventArgs e) + { + string? pluginPath = Path.GetDirectoryName(e.FullPath); + if (pluginPath == null) + { + _logger.Warning("Plugin change detected, but could not get plugin directory. {fullPath}", e.FullPath); + return; + } + + DirectoryInfo pluginDirectory = new(pluginPath); + Plugin? plugin = GetPluginByDirectory(pluginDirectory); + + if (plugin == null) + { + _logger.Warning("Plugin change detected, but could not find plugin. {fullPath}", e.FullPath); + return; + } + + _logger.Information("Plugin change detected, reloading. {pluginName}", plugin.Info.Name); + bool wasEnabled = plugin.IsEnabled; + + UnloadPlugin(plugin); + + Thread.Sleep(500); + + Plugin? loadedPlugin = LoadPlugin(pluginDirectory); + + if (loadedPlugin == null) + { + return; + } + + if (wasEnabled) + { + EnablePlugin(loadedPlugin, true, false); + } + + _logger.Information("Plugin reloaded. {fullPath}", e.FullPath); + } + + #endregion } /// From 8a0a162429b3bcec43e0e38706f3658df265686e Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 20 Apr 2023 13:47:24 +0200 Subject: [PATCH 2/2] Allow plugins to opt out of hot reloading --- src/Artemis.Core/Plugins/PluginInfo.cs | 12 ++++++ .../Services/PluginManagementService.cs | 38 +++++++++---------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index e8c3f5d50..1eebd03c6 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -28,6 +28,7 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject private string _version = null!; private Uri? _website; private Uri? _helpPage; + private bool _hotReloadSupported; internal PluginInfo() { @@ -156,6 +157,17 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject internal set => SetAndNotify(ref _requiresAdmin, value); } + /// + /// Gets or sets a boolean indicating whether hot reloading this plugin is supported + /// + [DefaultValue(true)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public bool HotReloadSupported + { + get => _hotReloadSupported; + set => SetAndNotify(ref _hotReloadSupported, value); + } + /// /// Gets /// diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 1e24feb47..fb304baf5 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -938,7 +938,7 @@ internal class PluginManagementService : IPluginManagementService _hotReloadWatcher.IncludeSubdirectories = true; _hotReloadWatcher.EnableRaisingEvents = true; } - + private void FileSystemWatcherOnError(object sender, ErrorEventArgs e) { _logger.Error(e.GetException(), "File system watcher error"); @@ -952,38 +952,38 @@ internal class PluginManagementService : IPluginManagementService _logger.Warning("Plugin change detected, but could not get plugin directory. {fullPath}", e.FullPath); return; } - + DirectoryInfo pluginDirectory = new(pluginPath); Plugin? plugin = GetPluginByDirectory(pluginDirectory); - + if (plugin == null) { _logger.Warning("Plugin change detected, but could not find plugin. {fullPath}", e.FullPath); return; } - - _logger.Information("Plugin change detected, reloading. {pluginName}", plugin.Info.Name); - bool wasEnabled = plugin.IsEnabled; - - UnloadPlugin(plugin); - - Thread.Sleep(500); - - Plugin? loadedPlugin = LoadPlugin(pluginDirectory); - - if (loadedPlugin == null) + + if (!plugin.Info.HotReloadSupported) { + _logger.Information("Plugin change detected, but hot reload not supported. {pluginName}", plugin.Info.Name); return; } - + + _logger.Information("Plugin change detected, reloading. {pluginName}", plugin.Info.Name); + bool wasEnabled = plugin.IsEnabled; + + UnloadPlugin(plugin); + Thread.Sleep(500); + Plugin? loadedPlugin = LoadPlugin(pluginDirectory); + + if (loadedPlugin == null) + return; + if (wasEnabled) - { EnablePlugin(loadedPlugin, true, false); - } - + _logger.Information("Plugin reloaded. {fullPath}", e.FullPath); } - + #endregion }