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 c03aec7bb..fb304baf5 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; + } + + 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 } ///