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 @@
-
+
+
+
+
+
- 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
+
+
+
-
\ 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",