diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index 0575a80ac..53c581e7c 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -178,12 +178,15 @@ namespace Artemis.Core { foreach (PluginFeature feature in Features) feature.Dispose(); + SetEnabled(false); Kernel?.Dispose(); PluginLoader?.Dispose(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + _features.Clear(); - SetEnabled(false); } } diff --git a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs index 83bab9211..887321a72 100644 --- a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs +++ b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs @@ -70,6 +70,12 @@ namespace Artemis.Core.Services /// The resulting plugin Plugin ImportPlugin(string fileName); + /// + /// Unloads and permanently removes the provided plugin + /// + /// The plugin to remove + void RemovePlugin(Plugin plugin); + /// /// Enables the provided plugin feature /// diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 31dc0070c..66b7933ec 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.Runtime.Loader; using Artemis.Core.DeviceProviders; using Artemis.Core.Ninject; using Artemis.Storage.Entities.Plugins; @@ -275,6 +276,7 @@ namespace Artemis.Core.Services plugin.PluginLoader = PluginLoader.CreateFromAssemblyFile(mainFile!, configure => { configure.IsUnloadable = true; + configure.LoadInMemory = true; configure.PreferSharedTypes = true; }); @@ -430,7 +432,6 @@ namespace Artemis.Core.Services OnPluginDisabled(new PluginEventArgs(plugin)); } - /// public Plugin ImportPlugin(string fileName) { DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins")); @@ -449,7 +450,16 @@ namespace Artemis.Core.Services Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid); if (existing != null) - throw new ArtemisPluginException($"A plugin with the same GUID is already loaded: {existing.Info}"); + { + try + { + RemovePlugin(existing); + } + catch (Exception e) + { + throw new ArtemisPluginException("A plugin with the same GUID is already loaded, failed to remove old version", e); + } + } string targetDirectory = pluginInfo.Main.Split(".dll")[0].Replace("/", "").Replace("\\", ""); string uniqueTargetDirectory = targetDirectory; @@ -464,19 +474,38 @@ namespace Artemis.Core.Services // Extract everything in the same archive directory to the unique plugin directory DirectoryInfo directoryInfo = new(Path.Combine(pluginDirectory.FullName, uniqueTargetDirectory)); - Directory.CreateDirectory(directoryInfo.FullName); + Utilities.CreateAccessibleDirectory(directoryInfo.FullName); string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, ""); foreach (ZipArchiveEntry zipArchiveEntry in archive.Entries) + { if (zipArchiveEntry.FullName.StartsWith(metaDataDirectory)) { string target = Path.Combine(directoryInfo.FullName, zipArchiveEntry.FullName.Remove(0, metaDataDirectory.Length)); - zipArchiveEntry.ExtractToFile(target); + // Create folders + if (zipArchiveEntry.FullName.EndsWith("/")) + Utilities.CreateAccessibleDirectory(Path.GetDirectoryName(target)!); + // Extract files + else + zipArchiveEntry.ExtractToFile(target); } + } // Load the newly extracted plugin and return the result return LoadPlugin(directoryInfo); } + public void RemovePlugin(Plugin plugin) + { + DirectoryInfo directory = plugin.Directory; + lock (_plugins) + { + if (_plugins.Contains(plugin)) + UnloadPlugin(plugin); + } + + directory.Delete(true); + } + #endregion #region Features diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index 3cec517d6..39998684b 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -74,7 +74,7 @@ namespace Artemis.Core.Services _modifyingProviders = true; List toRemove = _devices.Where(a => deviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList(); - Surface.Detach(deviceProvider.Devices); + Surface.Detach(toRemove.Select(d => d.RgbDevice)); foreach (ArtemisDevice device in toRemove) RemoveDevice(device); @@ -118,7 +118,7 @@ namespace Artemis.Core.Services _modifyingProviders = true; List toRemove = _devices.Where(a => deviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList(); - Surface.Detach(deviceProvider.Devices); + Surface.Detach(toRemove.Select(d => d.RgbDevice)); foreach (ArtemisDevice device in toRemove) RemoveDevice(device); diff --git a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs index ca0d1ce60..9448d8f40 100644 --- a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs +++ b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs @@ -79,6 +79,11 @@ namespace Artemis.Core.Services.Models public void Arrange(List devices) { ArrangedDevices.Clear(); + + // Not much to do here + if (!devices.Any()) + return; + foreach (ArtemisDevice surfaceDevice in devices) { surfaceDevice.X = 0; diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/EndpointExceptionEventArgs.cs b/src/Artemis.Core/Services/WebServer/EndPoints/EndpointExceptionEventArgs.cs new file mode 100644 index 000000000..1e17e1a91 --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/EndPoints/EndpointExceptionEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.Core.Services +{ + /// + /// Provides data about endpoint exception related events + /// + public class EndpointExceptionEventArgs : EventArgs + { + internal EndpointExceptionEventArgs(Exception exception) + { + Exception = exception; + } + + /// + /// Gets the exception that occurred + /// + public Exception Exception { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs index c456ff220..504fd0eff 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs @@ -52,15 +52,37 @@ namespace Artemis.Core.Services /// public string? Returns { get; protected set; } + /// + /// Occurs whenever a request threw an unhandled exception + /// + public event EventHandler? RequestException; + /// /// Called whenever the end point has to process a request /// /// The HTTP context of the request protected abstract Task ProcessRequest(IHttpContext context); + /// + /// Invokes the event + /// + /// The exception that occurred during the request + protected virtual void OnRequestException(Exception e) + { + RequestException?.Invoke(this, new EndpointExceptionEventArgs(e)); + } + internal async Task InternalProcessRequest(IHttpContext context) { - await ProcessRequest(context); + try + { + await ProcessRequest(context); + } + catch (Exception e) + { + OnRequestException(e); + throw; + } } private void OnDisabled(object? sender, EventArgs e) diff --git a/src/Artemis.Core/Services/WebServer/PluginsModule.cs b/src/Artemis.Core/Services/WebServer/PluginsModule.cs index 78de97e66..d7f9d49de 100644 --- a/src/Artemis.Core/Services/WebServer/PluginsModule.cs +++ b/src/Artemis.Core/Services/WebServer/PluginsModule.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using EmbedIO; -using Newtonsoft.Json; namespace Artemis.Core.Services { @@ -67,6 +64,12 @@ namespace Artemis.Core.Services if (!endPoints.TryGetValue(pathParts[1], out PluginEndPoint? endPoint)) throw HttpException.NotFound($"Found no endpoint called {pathParts[1]} for plugin with ID {pathParts[0]}."); + // If Accept-Charset contains a wildcard, remove the header so we default to UTF8 + // This is a workaround for an EmbedIO ehh issue + string? acceptCharset = context.Request.Headers["Accept-Charset"]; + if (acceptCharset != null && acceptCharset.Contains("*")) + context.Request.Headers.Remove("Accept-Charset"); + // It is up to the registration how the request is eventually handled, it might even set a response here await endPoint.InternalProcessRequest(context); diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 41b3911de..e19e6a4a9 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -28,7 +28,6 @@ namespace Artemis.Core.Services _webServerPortSetting.SettingChanged += WebServerPortSettingOnSettingChanged; PluginsModule = new PluginsModule("/plugins"); - StartWebServer(); } @@ -42,7 +41,7 @@ namespace Artemis.Core.Services Server?.Dispose(); Server = null; - string url = $"http://localhost:{_webServerPortSetting.Value}/"; + string url = $"http://*:{_webServerPortSetting.Value}/"; WebApiModule apiModule = new("/api/", JsonNetSerializer); PluginsModule.ServerUrl = url; WebServer server = new WebServer(o => o.WithUrlPrefix(url).WithMode(HttpListenerMode.EmbedIO)) diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index a78e9658a..7f8c86dbf 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -138,7 +138,7 @@ - + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabViewModel.cs index 5d17a2c72..787ab9c83 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabViewModel.cs @@ -59,12 +59,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins { Items.Clear(); await Task.Delay(200); - _instances = _pluginManagementService.GetAllPlugins() - .Select(p => _settingsVmFactory.CreatePluginSettingsViewModel(p)) - .OrderBy(i => i.Plugin.Info.Name) - .ToList(); - - UpdatePluginSearch(); + GetPluginInstances(); }); base.OnActivate(); @@ -80,14 +75,21 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins { Plugin plugin = _pluginManagementService.ImportPlugin(dialog.FileName); - _instances = _pluginManagementService.GetAllPlugins() - .Select(p => _settingsVmFactory.CreatePluginSettingsViewModel(p)) - .OrderBy(i => i.Plugin.Info.Name) - .ToList(); + GetPluginInstances(); SearchPluginInput = plugin.Info.Name; - + _messageService.ShowMessage($"Imported plugin: {plugin.Info.Name}"); } } + + public void GetPluginInstances() + { + _instances = _pluginManagementService.GetAllPlugins() + .Select(p => _settingsVmFactory.CreatePluginSettingsViewModel(p)) + .OrderBy(i => i.Plugin.Info.Name) + .ToList(); + + UpdatePluginSearch(); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsView.xaml index 250c2682b..34f106e9f 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsView.xaml @@ -53,15 +53,28 @@ - + SETTINGS + + + + - Plugin enabled + + Plugin enabled + + _pluginManagementService.EnablePlugin(Plugin, true)); diff --git a/src/Artemis.UI/Screens/TrayView.xaml b/src/Artemis.UI/Screens/TrayView.xaml index afac1218c..6e43c046d 100644 --- a/src/Artemis.UI/Screens/TrayView.xaml +++ b/src/Artemis.UI/Screens/TrayView.xaml @@ -1,24 +1,25 @@ - - + + + + + + Artemis + + + @@ -60,4 +61,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index a00f2702b..524e0ec98 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -27,9 +27,13 @@ }, "Hardcodet.NotifyIcon.Wpf.NetCore": { "type": "Direct", - "requested": "[1.0.14, )", - "resolved": "1.0.14", - "contentHash": "aNwwax4+C/xhIxTbwKHJnxh0A6hWGnqXZtppZ+/lkE8VOL+a2WuMOBgWpkiUk6Tv+pi1bc+yuHo0lh4Md6KEFw==" + "requested": "[1.0.18, )", + "resolved": "1.0.18", + "contentHash": "oI8YY/gUQooA0XIIZl4TgueexJcu+MbSvCQ2+ZBZRa+rIvFCWiyk8rjgywQ17sVrhLXRn+xF8+FTrWLxetkx0A==", + "dependencies": { + "H.NotifyIcon": "1.0.18", + "System.Drawing.Common": "5.0.0" + } }, "Humanizer.Core": { "type": "Direct", @@ -225,6 +229,11 @@ "resolved": "3.0.1", "contentHash": "i7CuPSikVroBaWG8sPvO707Ex9C6BP5+r4JufKNU1FGMmiFgLJvNo1ttUg6ZiXIzUNknvIb1VUTIO9iEDucibg==" }, + "H.NotifyIcon": { + "type": "Transitive", + "resolved": "1.0.18", + "contentHash": "vV0lNWD9xGeCH4pCmT8vKtax2QOmo8WwhCBgBDO3BYQtPG7Rjuf5Ua+3O8++XkZB7t9zromzxcT5nYdGDg1Puw==" + }, "HidSharp": { "type": "Transitive", "resolved": "2.1.0",