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
}
///