From 28e15320647b7efb5d0c99dc8740034424dd53b3 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 27 Jan 2021 20:52:51 +0100 Subject: [PATCH 1/6] Web server - Added web server service UI - Added remote management for bringing to foreground, restarting and shutting down UI - Simplified services namespaces --- src/Artemis.Core/Artemis.Core.csproj | 1 + .../Artemis.Core.csproj.DotSettings | 2 + .../Profile/LayerProperties/ILayerProperty.cs | 2 +- .../WebServer/Interfaces/IWebServerService.cs | 34 +++++ .../Services/WebServer/PluginsModule.cs | 25 ++++ .../WebServer/WebApiControllerRegistration.cs | 28 +++++ .../Services/WebServer/WebServerService.cs | 117 ++++++++++++++++++ src/Artemis.Core/packages.lock.json | 14 +++ .../Services/Message/MessageService.cs | 8 +- .../Services/Window/WindowService.cs | 6 +- src/Artemis.UI.Shared/packages.lock.json | 14 +++ src/Artemis.UI/Artemis.UI.csproj.DotSettings | 5 +- src/Artemis.UI/Bootstrapper.cs | 1 + src/Artemis.UI/Ninject/UiModule.cs | 2 +- .../Dialogs/ProfileImportViewModel.cs | 2 - .../Visualization/ProfileLayerViewModel.cs | 2 +- .../Visualization/Tools/EditToolViewModel.cs | 2 +- src/Artemis.UI/Screens/RootViewModel.cs | 1 - .../General/GeneralSettingsTabViewModel.cs | 1 - src/Artemis.UI/Screens/TrayViewModel.cs | 60 ++++----- src/Artemis.UI/Services/DebugService.cs | 1 - .../Services/Interfaces/IArtemisUIService.cs | 2 +- .../Services/Interfaces/IDebugService.cs | 2 +- .../Interfaces/ILayerEditorService.cs | 2 +- src/Artemis.UI/Services/LayerEditorService.cs | 1 - .../Services/RegistrationService.cs | 1 - .../Interfaces/IRemoteManagementService.cs | 6 + .../RemoteManagement/RemoteController.cs | 39 ++++++ .../RemoteManagementService.cs | 12 ++ src/Artemis.UI/Services/UpdateService.cs | 3 +- src/Artemis.UI/packages.lock.json | 14 +++ 31 files changed, 351 insertions(+), 59 deletions(-) create mode 100644 src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs create mode 100644 src/Artemis.Core/Services/WebServer/PluginsModule.cs create mode 100644 src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs create mode 100644 src/Artemis.Core/Services/WebServer/WebServerService.cs create mode 100644 src/Artemis.UI/Services/RemoteManagement/Interfaces/IRemoteManagementService.cs create mode 100644 src/Artemis.UI/Services/RemoteManagement/RemoteController.cs create mode 100644 src/Artemis.UI/Services/RemoteManagement/RemoteManagementService.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index c00b5950c..bd0e5eb30 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -39,6 +39,7 @@ + diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 0ff3e797c..8aa37a05c 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -64,6 +64,8 @@ True True True + True + True True True True \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index d726d4b2e..33153f4ef 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -78,7 +78,7 @@ namespace Artemis.Core public event EventHandler? CurrentValueSet; /// - /// Occurs when the value of the layer property was updated + /// Occurs when the visibility value of the layer property was updated /// public event EventHandler? VisibilityChanged; diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs new file mode 100644 index 000000000..b70ed629d --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -0,0 +1,34 @@ +using System; +using EmbedIO; +using EmbedIO.WebApi; + +namespace Artemis.Core.Services +{ + /// + /// A service that provides access to the local Artemis web server + /// + public interface IWebServerService : IArtemisService + { + /// + /// Gets the currently active instance of the web server + /// + WebServer? Server { get; } + + /// + /// Adds a new Web API controller and restarts the web server + /// + /// The type of Web API controller to remove + void AddController() where T : WebApiController; + + /// + /// Removes an existing Web API controller and restarts the web server + /// + /// The type of Web API controller to remove + void RemoveController() where T : WebApiController; + + /// + /// Occurs when a new instance of the web server was been created + /// + event EventHandler? WebServerCreated; + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/PluginsModule.cs b/src/Artemis.Core/Services/WebServer/PluginsModule.cs new file mode 100644 index 000000000..b3e635153 --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/PluginsModule.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using EmbedIO; + +namespace Artemis.Core.Services +{ + internal class PluginsModule : WebModuleBase + { + /// + public PluginsModule(string baseRoute) : base(baseRoute) + { + } + + #region Overrides of WebModuleBase + + /// + protected override async Task OnRequestAsync(IHttpContext context) + { + } + + /// + public override bool IsFinalHandler => true; + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs b/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs new file mode 100644 index 000000000..ad5fb30dc --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs @@ -0,0 +1,28 @@ +using System; +using EmbedIO.WebApi; +using Ninject; + +namespace Artemis.Core.Services +{ + internal class WebApiControllerRegistration : WebApiControllerRegistration where T : WebApiController + { + public WebApiControllerRegistration(IKernel kernel) : base(typeof(T)) + { + Factory = () => kernel.Get(); + } + + public Func Factory { get; set; } + public override object UntypedFactory => Factory; + } + + internal abstract class WebApiControllerRegistration + { + protected WebApiControllerRegistration(Type controllerType) + { + ControllerType = controllerType; + } + + public abstract object UntypedFactory { get; } + public Type ControllerType { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs new file mode 100644 index 000000000..64af5c2ef --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.IO; +using EmbedIO; +using EmbedIO.Actions; +using EmbedIO.WebApi; +using Ninject; +using Serilog; + +namespace Artemis.Core.Services +{ + internal class WebServerService : IWebServerService, IDisposable + { + private readonly IKernel _kernel; + private readonly ILogger _logger; + private readonly PluginsModule _pluginModule; + private readonly PluginSetting _webServerPortSetting; + private readonly List _controllers; + + public WebServerService(IKernel kernel, ILogger logger, ISettingsService settingsService) + { + _kernel = kernel; + _logger = logger; + _controllers = new List(); + + _webServerPortSetting = settingsService.GetSetting("WebServer.Port", 9696); + _webServerPortSetting.SettingChanged += WebServerPortSettingOnSettingChanged; + + _pluginModule = new PluginsModule("/plugin"); + Server = CreateWebServer(); + Server.Start(); + } + + public WebServer? Server { get; private set; } + + #region Web server managament + + private WebServer CreateWebServer() + { + Server?.Dispose(); + Server = null; + + string url = $"http://localhost:{_webServerPortSetting.Value}/"; + WebApiModule apiModule = new("/api/"); + WebServer server = new WebServer(o => o.WithUrlPrefix(url).WithMode(HttpListenerMode.EmbedIO)) + .WithLocalSessionManager() + .WithModule(apiModule) + .WithModule(_pluginModule) + .WithModule(new ActionModule("/", HttpVerbs.Any, ctx => ctx.SendDataAsync(new {Message = "Error"}))); + + // Add controllers to the API module + foreach (WebApiControllerRegistration registration in _controllers) + apiModule.RegisterController(registration.ControllerType, (Func) registration.UntypedFactory); + + // Listen for state changes. + server.StateChanged += (s, e) => _logger.Verbose("WebServer new state - {state}", e.NewState); + + // Store the URL in a webserver.txt file so that remote applications can find it + File.WriteAllText(Path.Combine(Constants.DataFolder, "webserver.txt"), url); + OnWebServerCreated(); + + return server; + } + + #endregion + + #region Event handlers + + private void WebServerPortSettingOnSettingChanged(object? sender, EventArgs e) + { + Server = CreateWebServer(); + Server.Start(); + } + + #endregion + + #region IDisposable + + /// + public void Dispose() + { + Server?.Dispose(); + _webServerPortSetting.SettingChanged -= WebServerPortSettingOnSettingChanged; + } + + #endregion + + #region Controller management + + public void AddController() where T : WebApiController + { + _controllers.Add(new WebApiControllerRegistration(_kernel)); + Server = CreateWebServer(); + Server.Start(); + } + + public void RemoveController() where T : WebApiController + { + _controllers.RemoveAll(r => r.ControllerType == typeof(T)); + Server = CreateWebServer(); + Server.Start(); + } + + #endregion + + #region Events + + public event EventHandler? WebServerCreated; + + protected virtual void OnWebServerCreated() + { + WebServerCreated?.Invoke(this, EventArgs.Empty); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/packages.lock.json b/src/Artemis.Core/packages.lock.json index 03a69fcd6..c422d273d 100644 --- a/src/Artemis.Core/packages.lock.json +++ b/src/Artemis.Core/packages.lock.json @@ -12,6 +12,15 @@ "System.Threading.Tasks.Extensions": "4.5.3" } }, + "EmbedIO": { + "type": "Direct", + "requested": "[3.4.3, )", + "resolved": "3.4.3", + "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "dependencies": { + "Unosquare.Swan.Lite": "3.0.0" + } + }, "HidSharp": { "type": "Direct", "requested": "[2.1.0, )", @@ -1226,6 +1235,11 @@ "System.Xml.ReaderWriter": "4.3.0" } }, + "Unosquare.Swan.Lite": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==" + }, "artemis.storage": { "type": "Project", "dependencies": { diff --git a/src/Artemis.UI.Shared/Services/Message/MessageService.cs b/src/Artemis.UI.Shared/Services/Message/MessageService.cs index aef6ffc7e..45bd0e447 100644 --- a/src/Artemis.UI.Shared/Services/Message/MessageService.cs +++ b/src/Artemis.UI.Shared/Services/Message/MessageService.cs @@ -5,7 +5,7 @@ namespace Artemis.UI.Shared.Services { internal class MessageService : IMessageService { - private INotificationProvider _notificationProvider; + private INotificationProvider? _notificationProvider; public ISnackbarMessageQueue MainMessageQueue { get; } public MessageService(ISnackbarMessageQueue mainMessageQueue) @@ -74,20 +74,20 @@ namespace Artemis.UI.Shared.Services /// public void ShowNotification(string title, string message) { - _notificationProvider.ShowNotification(title, message, PackIconKind.None); + _notificationProvider?.ShowNotification(title, message, PackIconKind.None); } /// public void ShowNotification(string title, string message, PackIconKind icon) { - _notificationProvider.ShowNotification(title, message, icon); + _notificationProvider?.ShowNotification(title, message, icon); } /// public void ShowNotification(string title, string message, string icon) { Enum.TryParse(typeof(PackIconKind), icon, true, out object? iconKind); - _notificationProvider.ShowNotification(title, message, (PackIconKind) (iconKind ?? PackIconKind.None)); + _notificationProvider?.ShowNotification(title, message, (PackIconKind) (iconKind ?? PackIconKind.None)); } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Window/WindowService.cs b/src/Artemis.UI.Shared/Services/Window/WindowService.cs index c8b2a0ced..e0ce8bd1b 100644 --- a/src/Artemis.UI.Shared/Services/Window/WindowService.cs +++ b/src/Artemis.UI.Shared/Services/Window/WindowService.cs @@ -66,14 +66,12 @@ namespace Artemis.UI.Shared.Services public void OpenMainWindow() { - IsMainWindowOpen = true; - OnMainWindowOpened(); + _mainWindowManager?.OpenMainWindow(); } public void CloseMainWindow() { - IsMainWindowOpen = false; - OnMainWindowClosed(); + _mainWindowManager?.CloseMainWindow(); } public event EventHandler? MainWindowOpened; diff --git a/src/Artemis.UI.Shared/packages.lock.json b/src/Artemis.UI.Shared/packages.lock.json index db871839a..7c48f91d0 100644 --- a/src/Artemis.UI.Shared/packages.lock.json +++ b/src/Artemis.UI.Shared/packages.lock.json @@ -142,6 +142,14 @@ "System.Xml.XmlDocument": "4.3.0" } }, + "EmbedIO": { + "type": "Transitive", + "resolved": "3.4.3", + "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "dependencies": { + "Unosquare.Swan.Lite": "3.0.0" + } + }, "HidSharp": { "type": "Transitive", "resolved": "2.1.0", @@ -1305,11 +1313,17 @@ "System.Xml.ReaderWriter": "4.3.0" } }, + "Unosquare.Swan.Lite": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==" + }, "artemis.core": { "type": "Project", "dependencies": { "Artemis.Storage": "1.0.0", "Ben.Demystifier": "0.1.6", + "EmbedIO": "3.4.3", "HidSharp": "2.1.0", "Humanizer.Core": "2.8.26", "LiteDB": "5.0.9", diff --git a/src/Artemis.UI/Artemis.UI.csproj.DotSettings b/src/Artemis.UI/Artemis.UI.csproj.DotSettings index 7c2d2de36..8bd53f68f 100644 --- a/src/Artemis.UI/Artemis.UI.csproj.DotSettings +++ b/src/Artemis.UI/Artemis.UI.csproj.DotSettings @@ -1,2 +1,5 @@  - True \ No newline at end of file + True + True + True + True \ No newline at end of file diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs index c9c24459a..0e66fd085 100644 --- a/src/Artemis.UI/Bootstrapper.cs +++ b/src/Artemis.UI/Bootstrapper.cs @@ -89,6 +89,7 @@ namespace Artemis.UI }); Kernel.Get().RegisterInputProvider(); + Kernel.Get(); } protected override void ConfigureIoC(IKernel kernel) diff --git a/src/Artemis.UI/Ninject/UiModule.cs b/src/Artemis.UI/Ninject/UiModule.cs index 13ead7e67..627334b0a 100644 --- a/src/Artemis.UI/Ninject/UiModule.cs +++ b/src/Artemis.UI/Ninject/UiModule.cs @@ -4,7 +4,7 @@ using Artemis.UI.Ninject.InstanceProviders; using Artemis.UI.Screens; using Artemis.UI.Screens.ProfileEditor; using Artemis.UI.Screens.Splash; -using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services; using Artemis.UI.Shared.Services; using Artemis.UI.Stylet; using FluentValidation; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs index ef89cef72..92c454c3c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs @@ -3,13 +3,11 @@ using Artemis.Core.Modules; using Artemis.Core.Services; using Artemis.UI.Shared.Services; using ICSharpCode.AvalonEdit.Document; -using MaterialDesignThemes.Wpf; namespace Artemis.UI.Screens.ProfileEditor.Dialogs { public class ProfileImportViewModel : DialogViewModelBase { - private readonly ISnackbarMessageQueue _mainMessageQueue; private readonly IProfileService _profileService; private readonly IMessageService _messageService; private string _profileJson; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs index 7aea3591f..afc9c6c54 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs @@ -6,7 +6,7 @@ using System.Windows.Media; using Artemis.Core; using Artemis.UI.Extensions; using Artemis.UI.Screens.Shared; -using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services; using Artemis.UI.Shared.Services; namespace Artemis.UI.Screens.ProfileEditor.Visualization diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs index 8c49f86c4..40adf63e4 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs @@ -5,7 +5,7 @@ using System.Windows.Media; using Artemis.Core; using Artemis.UI.Events; using Artemis.UI.Screens.Shared; -using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services; using Artemis.UI.Shared.Services; using SkiaSharp; using SkiaSharp.Views.WPF; diff --git a/src/Artemis.UI/Screens/RootViewModel.cs b/src/Artemis.UI/Screens/RootViewModel.cs index 35c55db05..e99e5127e 100644 --- a/src/Artemis.UI/Screens/RootViewModel.cs +++ b/src/Artemis.UI/Screens/RootViewModel.cs @@ -14,7 +14,6 @@ using Artemis.UI.Screens.Settings.Tabs.General; using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Services; -using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared.Services; using Artemis.UI.Utilities; using MaterialDesignExtensions.Controls; diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs index 3a41acea1..4a304194b 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs @@ -14,7 +14,6 @@ using Artemis.Core.Services; using Artemis.UI.Properties; using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Services; -using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using MaterialDesignThemes.Wpf; diff --git a/src/Artemis.UI/Screens/TrayViewModel.cs b/src/Artemis.UI/Screens/TrayViewModel.cs index 666c8cd59..c17ab70ae 100644 --- a/src/Artemis.UI/Screens/TrayViewModel.cs +++ b/src/Artemis.UI/Screens/TrayViewModel.cs @@ -9,7 +9,6 @@ using Artemis.Core.Services; using Artemis.UI.Events; using Artemis.UI.Screens.Splash; using Artemis.UI.Services; -using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared.Services; using Hardcodet.Wpf.TaskbarNotification; using MaterialDesignThemes.Wpf; @@ -25,7 +24,6 @@ namespace Artemis.UI.Screens private readonly IEventAggregator _eventAggregator; private readonly IKernel _kernel; private readonly IWindowManager _windowManager; - private bool _canShowRootViewModel; private RootViewModel _rootViewModel; private SplashViewModel _splashViewModel; private TaskbarIcon _taskBarIcon; @@ -44,7 +42,6 @@ namespace Artemis.UI.Screens _windowManager = windowManager; _eventAggregator = eventAggregator; _debugService = debugService; - CanShowRootViewModel = true; Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; Core.Utilities.RestartRequested += UtilitiesOnShutdownRequested; @@ -64,23 +61,19 @@ namespace Artemis.UI.Screens } } - public bool CanShowRootViewModel - { - get => _canShowRootViewModel; - set => SetAndNotify(ref _canShowRootViewModel, value); - } - public void TrayBringToForeground() { - if (!CanShowRootViewModel) + if (IsMainWindowOpen) + { + Execute.PostToUIThread(FocusMainWindow); return; + } // Initialize the shared UI when first showing the window if (!UI.Shared.Bootstrapper.Initialized) UI.Shared.Bootstrapper.Initialize(_kernel); - CanShowRootViewModel = false; - Execute.OnUIThread(() => + Execute.OnUIThreadSync(() => { _splashViewModel?.RequestClose(); _splashViewModel = null; @@ -115,24 +108,25 @@ namespace Artemis.UI.Screens public void OnTrayBalloonTipClicked(object sender, EventArgs e) { - if (CanShowRootViewModel) - { + if (!IsMainWindowOpen) TrayBringToForeground(); - } else - { - // Wrestle the main window to the front - Window mainWindow = (Window) _rootViewModel.View; - if (mainWindow.WindowState == WindowState.Minimized) - mainWindow.WindowState = WindowState.Normal; - mainWindow.Activate(); - mainWindow.Topmost = true; - mainWindow.Topmost = false; - mainWindow.Focus(); - } + FocusMainWindow(); } - private void UtilitiesOnShutdownRequested(object? sender, EventArgs e) + private void FocusMainWindow() + { + // Wrestle the main window to the front + Window mainWindow = (Window) _rootViewModel.View; + if (mainWindow.WindowState == WindowState.Minimized) + mainWindow.WindowState = WindowState.Normal; + mainWindow.Activate(); + mainWindow.Topmost = true; + mainWindow.Topmost = false; + mainWindow.Focus(); + } + + private void UtilitiesOnShutdownRequested(object sender, EventArgs e) { Execute.OnUIThread(() => _taskBarIcon?.Dispose()); } @@ -150,8 +144,6 @@ namespace Artemis.UI.Screens { _rootViewModel.Closed -= RootViewModelOnClosed; _rootViewModel = null; - - CanShowRootViewModel = true; OnMainWindowClosed(); } @@ -198,16 +190,16 @@ namespace Artemis.UI.Screens public bool OpenMainWindow() { - if (CanShowRootViewModel) - return false; - - TrayBringToForeground(); - return true; + if (IsMainWindowOpen) + Execute.OnUIThread(FocusMainWindow); + else + TrayBringToForeground(); + return _rootViewModel.ScreenState == ScreenState.Active; } public bool CloseMainWindow() { - _rootViewModel.RequestClose(); + Execute.OnUIThread(() => _rootViewModel.RequestClose()); return _rootViewModel.ScreenState == ScreenState.Closed; } diff --git a/src/Artemis.UI/Services/DebugService.cs b/src/Artemis.UI/Services/DebugService.cs index 158457079..060790479 100644 --- a/src/Artemis.UI/Services/DebugService.cs +++ b/src/Artemis.UI/Services/DebugService.cs @@ -1,6 +1,5 @@ using System.Windows; using Artemis.UI.Screens.Settings.Debug; -using Artemis.UI.Services.Interfaces; using MaterialDesignExtensions.Controls; using Ninject; using Stylet; diff --git a/src/Artemis.UI/Services/Interfaces/IArtemisUIService.cs b/src/Artemis.UI/Services/Interfaces/IArtemisUIService.cs index d29486dce..3e2b425b8 100644 --- a/src/Artemis.UI/Services/Interfaces/IArtemisUIService.cs +++ b/src/Artemis.UI/Services/Interfaces/IArtemisUIService.cs @@ -1,4 +1,4 @@ -namespace Artemis.UI.Services.Interfaces +namespace Artemis.UI.Services { // ReSharper disable once InconsistentNaming public interface IArtemisUIService diff --git a/src/Artemis.UI/Services/Interfaces/IDebugService.cs b/src/Artemis.UI/Services/Interfaces/IDebugService.cs index fd349ebba..331687677 100644 --- a/src/Artemis.UI/Services/Interfaces/IDebugService.cs +++ b/src/Artemis.UI/Services/Interfaces/IDebugService.cs @@ -1,4 +1,4 @@ -namespace Artemis.UI.Services.Interfaces +namespace Artemis.UI.Services { public interface IDebugService : IArtemisUIService { diff --git a/src/Artemis.UI/Services/Interfaces/ILayerEditorService.cs b/src/Artemis.UI/Services/Interfaces/ILayerEditorService.cs index 79799d183..1b2c687c7 100644 --- a/src/Artemis.UI/Services/Interfaces/ILayerEditorService.cs +++ b/src/Artemis.UI/Services/Interfaces/ILayerEditorService.cs @@ -3,7 +3,7 @@ using System.Windows.Media; using Artemis.Core; using SkiaSharp; -namespace Artemis.UI.Services.Interfaces +namespace Artemis.UI.Services { public interface ILayerEditorService : IArtemisUIService { diff --git a/src/Artemis.UI/Services/LayerEditorService.cs b/src/Artemis.UI/Services/LayerEditorService.cs index fcaa1143d..4ecde9cd8 100644 --- a/src/Artemis.UI/Services/LayerEditorService.cs +++ b/src/Artemis.UI/Services/LayerEditorService.cs @@ -2,7 +2,6 @@ using System.Windows; using System.Windows.Media; using Artemis.Core; -using Artemis.UI.Services.Interfaces; using SkiaSharp; using SkiaSharp.Views.WPF; diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index f64218a08..6ae335bf4 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -6,7 +6,6 @@ using Artemis.UI.DefaultTypes.DataModel.Input; using Artemis.UI.InputProviders; using Artemis.UI.Ninject; using Artemis.UI.PropertyInput; -using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared.Services; using Serilog; diff --git a/src/Artemis.UI/Services/RemoteManagement/Interfaces/IRemoteManagementService.cs b/src/Artemis.UI/Services/RemoteManagement/Interfaces/IRemoteManagementService.cs new file mode 100644 index 000000000..25cafb0d3 --- /dev/null +++ b/src/Artemis.UI/Services/RemoteManagement/Interfaces/IRemoteManagementService.cs @@ -0,0 +1,6 @@ +namespace Artemis.UI.Services +{ + public interface IRemoteManagementService : IArtemisUIService + { + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/RemoteManagement/RemoteController.cs b/src/Artemis.UI/Services/RemoteManagement/RemoteController.cs new file mode 100644 index 000000000..a39468924 --- /dev/null +++ b/src/Artemis.UI/Services/RemoteManagement/RemoteController.cs @@ -0,0 +1,39 @@ +using System; +using Artemis.Core.Services; +using Artemis.UI.Shared.Services; +using EmbedIO; +using EmbedIO.Routing; +using EmbedIO.WebApi; + +namespace Artemis.UI.Services +{ + public class RemoteController : WebApiController + { + private readonly ICoreService _coreService; + private readonly IWindowService _windowService; + + public RemoteController(ICoreService coreService, IWindowService windowService) + { + _coreService = coreService; + _windowService = windowService; + } + + [Route(HttpVerbs.Post, "/remote/bring-to-foreground")] + public void PostBringToForeground() + { + _windowService.OpenMainWindow(); + } + + [Route(HttpVerbs.Post, "/remote/restart")] + public void PostRestart() + { + Core.Utilities.Restart(_coreService.IsElevated, TimeSpan.FromMilliseconds(500)); + } + + [Route(HttpVerbs.Post, "/remote/shutdown")] + public void PostShutdown() + { + Core.Utilities.Shutdown(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/RemoteManagement/RemoteManagementService.cs b/src/Artemis.UI/Services/RemoteManagement/RemoteManagementService.cs new file mode 100644 index 000000000..956e8a474 --- /dev/null +++ b/src/Artemis.UI/Services/RemoteManagement/RemoteManagementService.cs @@ -0,0 +1,12 @@ +using Artemis.Core.Services; + +namespace Artemis.UI.Services +{ + public class RemoteManagementService : IRemoteManagementService + { + public RemoteManagementService(IWebServerService webServerService) + { + webServerService.AddController(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/UpdateService.cs b/src/Artemis.UI/Services/UpdateService.cs index 21af544b1..69704201a 100644 --- a/src/Artemis.UI/Services/UpdateService.cs +++ b/src/Artemis.UI/Services/UpdateService.cs @@ -10,7 +10,6 @@ using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Exceptions; using Artemis.UI.Screens.Settings.Dialogs; -using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared.Services; using MaterialDesignThemes.Wpf; using Newtonsoft.Json.Linq; @@ -209,7 +208,7 @@ namespace Artemis.UI.Services AutoUpdate(); } - private void WindowServiceOnMainWindowOpened(object? sender, EventArgs e) + private void WindowServiceOnMainWindowOpened(object sender, EventArgs e) { _logger.Information("Main window opened!"); } diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index 83f215a37..8a022ea4a 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -195,6 +195,14 @@ "System.Xml.XmlDocument": "4.3.0" } }, + "EmbedIO": { + "type": "Transitive", + "resolved": "3.4.3", + "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "dependencies": { + "Unosquare.Swan.Lite": "3.0.0" + } + }, "HidSharp": { "type": "Transitive", "resolved": "2.1.0", @@ -1357,6 +1365,11 @@ "System.Xml.ReaderWriter": "4.3.0" } }, + "Unosquare.Swan.Lite": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==" + }, "WriteableBitmapEx": { "type": "Transitive", "resolved": "1.6.7", @@ -1367,6 +1380,7 @@ "dependencies": { "Artemis.Storage": "1.0.0", "Ben.Demystifier": "0.1.6", + "EmbedIO": "3.4.3", "HidSharp": "2.1.0", "Humanizer.Core": "2.8.26", "LiteDB": "5.0.9", From 5c2a96eee0ae23929976ecd1997536f4c5f65605 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 28 Jan 2021 19:14:58 +0100 Subject: [PATCH 2/6] Startup - Bring existing instances to foreground/focus them Bootstrapper - Cleaned up code into a separate state manager --- src/Artemis.UI/ApplicationStateManager.cs | 127 ++++++++++++++++++++++ src/Artemis.UI/Bootstrapper.cs | 85 ++------------- 2 files changed, 137 insertions(+), 75 deletions(-) create mode 100644 src/Artemis.UI/ApplicationStateManager.cs diff --git a/src/Artemis.UI/ApplicationStateManager.cs b/src/Artemis.UI/ApplicationStateManager.cs new file mode 100644 index 000000000..d07a0aa17 --- /dev/null +++ b/src/Artemis.UI/ApplicationStateManager.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using Artemis.Core; +using Artemis.UI.Utilities; +using Stylet; + +namespace Artemis.UI +{ + public class ApplicationStateManager + { + // ReSharper disable once NotAccessedField.Local - Kept in scope to ensure it does not get released + private Mutex _artemisMutex; + + public ApplicationStateManager(string[] startupArguments) + { + StartupArguments = startupArguments; + IsElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + + Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; + Core.Utilities.RestartRequested += UtilitiesOnRestartRequested; + } + + public string[] StartupArguments { get; } + public bool IsElevated { get; } + + public bool FocusExistingInstance() + { + _artemisMutex = new Mutex(true, "Artemis-3c24b502-64e6-4587-84bf-9072970e535d", out bool createdNew); + if (createdNew) + return false; + + try + { + // Blocking is required here otherwise Artemis shuts down before the remote call gets a chance to finish + RemoteFocus().GetAwaiter().GetResult(); + } + catch (Exception) + { + // Not much could go wrong here but this code runs so early it'll crash if something does go wrong + return true; + } + + return true; + } + + private async Task RemoteFocus() + { + // At this point we cannot read the database yet to retrieve the web server port. + // Instead use the method external applications should use as well. + if (!File.Exists(Path.Combine(Constants.DataFolder, "webserver.txt"))) + return; + + string url = await File.ReadAllTextAsync(Path.Combine(Constants.DataFolder, "webserver.txt")); + using HttpClient client = new(); + await client.PostAsync(url + "api/remote/bring-to-foreground", null!); + } + + private void UtilitiesOnRestartRequested(object sender, RestartEventArgs e) + { + List argsList = new(); + argsList.AddRange(StartupArguments); + if (e.ExtraArgs != null) + argsList.AddRange(e.ExtraArgs.Except(argsList)); + string args = argsList.Any() ? "-ArgumentList " + string.Join(',', argsList) : ""; + string command = + $"-Command \"& {{Start-Sleep -Milliseconds {(int) e.Delay.TotalMilliseconds}; " + + "(Get-Process 'Artemis.UI').kill(); " + + $"Start-Process -FilePath '{Constants.ExecutablePath}' -WorkingDirectory '{Constants.ApplicationFolder}' {args}}}\""; + // Elevated always runs with RunAs + if (e.Elevate) + { + ProcessStartInfo info = new() + { + Arguments = command.Replace("}\"", " -Verb RunAs}\""), + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + } + // Non-elevated runs regularly if currently not elevated + else if (!IsElevated) + { + ProcessStartInfo info = new() + { + Arguments = command, + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + } + // Non-elevated runs via a utility method is currently elevated (de-elevating is hacky) + else + { + string powerShell = Path.Combine(Environment.SystemDirectory, "WindowsPowerShell", "v1.0", "powershell.exe"); + ProcessUtilities.RunAsDesktopUser(powerShell, command, true); + } + + // Lets try a graceful shutdown, PowerShell will kill if needed + Execute.OnUIThread(() => Application.Current.Shutdown()); + } + + private void UtilitiesOnShutdownRequested(object sender, EventArgs e) + { + // Use PowerShell to kill the process after 2 sec just in case + ProcessStartInfo info = new() + { + Arguments = "-Command \"& {Start-Sleep -s 2; (Get-Process 'Artemis.UI').kill()}", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + + Execute.OnUIThread(() => Application.Current.Shutdown()); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs index 0e66fd085..446cba8cb 100644 --- a/src/Artemis.UI/Bootstrapper.cs +++ b/src/Artemis.UI/Bootstrapper.cs @@ -1,16 +1,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; -using System.IO; using System.Linq; -using System.Runtime.InteropServices; -using System.Security.Principal; using System.Threading.Tasks; using System.Windows; using System.Windows.Markup; using System.Windows.Threading; -using Artemis.Core; using Artemis.Core.Ninject; using Artemis.Core.Services; using Artemis.UI.Ninject; @@ -19,7 +14,6 @@ using Artemis.UI.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Stylet; -using Artemis.UI.Utilities; using Ninject; using Serilog; using Stylet; @@ -28,6 +22,7 @@ namespace Artemis.UI { public class Bootstrapper : NinjectBootstrapper { + private ApplicationStateManager _applicationStateManager; private ICoreService _core; public static List StartupArguments { get; private set; } @@ -41,14 +36,18 @@ namespace Artemis.UI protected override void Launch() { - // TODO: Move shutdown code out of bootstrapper - Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; - Core.Utilities.RestartRequested += UtilitiesOnRestartRequested; + _applicationStateManager = new ApplicationStateManager(Args); Core.Utilities.PrepareFirstLaunch(); ILogger logger = Kernel.Get(); - IViewManager viewManager = Kernel.Get(); + if (_applicationStateManager.FocusExistingInstance()) + { + logger.Information("Shutting down because a different instance is already running."); + Application.Current.Shutdown(1); + return; + } + IViewManager viewManager = Kernel.Get(); StartupArguments = Args.ToList(); // Create the Artemis core @@ -78,7 +77,7 @@ namespace Artemis.UI try { _core.StartupArguments = StartupArguments; - _core.IsElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + _core.IsElevated = _applicationStateManager.IsElevated; _core.Initialize(); } catch (Exception e) @@ -125,67 +124,6 @@ namespace Artemis.UI e.Handled = true; } - private void UtilitiesOnShutdownRequested(object sender, EventArgs e) - { - // Use PowerShell to kill the process after 2 sec just in case - ProcessStartInfo info = new() - { - Arguments = "-Command \"& {Start-Sleep -s 2; (Get-Process 'Artemis.UI').kill()}", - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - FileName = "PowerShell.exe" - }; - Process.Start(info); - - Execute.OnUIThread(() => Application.Current.Shutdown()); - } - - private void UtilitiesOnRestartRequested(object sender, RestartEventArgs e) - { - List argsList = new(); - argsList.AddRange(Args); - if (e.ExtraArgs != null) - argsList.AddRange(e.ExtraArgs.Except(argsList)); - string args = argsList.Any() ? "-ArgumentList " + string.Join(',', argsList) : ""; - string command = - $"-Command \"& {{Start-Sleep -Milliseconds {(int) e.Delay.TotalMilliseconds}; " + - $"(Get-Process 'Artemis.UI').kill(); " + - $"Start-Process -FilePath '{Constants.ExecutablePath}' -WorkingDirectory '{Constants.ApplicationFolder}' {args}}}\""; - // Elevated always runs with RunAs - if (e.Elevate) - { - ProcessStartInfo info = new() - { - Arguments = command.Replace("}\"", " -Verb RunAs}\""), - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - FileName = "PowerShell.exe" - }; - Process.Start(info); - } - // Non-elevated runs regularly if currently not elevated - else if (!_core.IsElevated) - { - ProcessStartInfo info = new() - { - Arguments = command, - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - FileName = "PowerShell.exe" - }; - Process.Start(info); - } - // Non-elevated runs via a utility method is currently elevated (de-elevating is hacky) - else - { - string powerShell = Path.Combine(Environment.SystemDirectory, "WindowsPowerShell", "v1.0", "powershell.exe"); - ProcessUtilities.RunAsDesktopUser(powerShell, command, true); - } - - // Lets try a graceful shutdown, PowerShell will kill if needed - Execute.OnUIThread(() => Application.Current.Shutdown()); - } - private void HandleFatalException(Exception e, ILogger logger) { logger.Fatal(e, "Fatal exception during initialization, shutting down."); @@ -201,8 +139,5 @@ namespace Artemis.UI Environment.Exit(1); }); } - - [DllImport("user32.dll")] - private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); } } \ No newline at end of file From d6ba5734569325a55665dbea38d1425306684a0a Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 28 Jan 2021 19:54:04 +0100 Subject: [PATCH 3/6] Web server - Plugin end point API WIP --- .../WebServer/Interfaces/IWebServerService.cs | 14 ++++ .../WebServer/PluginEndPointRegistration.cs | 45 ++++++++++++ .../Services/WebServer/PluginsModule.cs | 68 ++++++++++++++++++- .../Services/WebServer/WebServerService.cs | 26 ++++++- 4 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs index b70ed629d..8be29ebfa 100644 --- a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -14,6 +14,20 @@ namespace Artemis.Core.Services /// WebServer? Server { get; } + /// + /// Adds a new endpoint for the given plugin feature + /// + /// The plugin feature the end point is associated with + /// The name of the end point, must be unique + /// The resulting end point + PluginEndPoint AddPluginEndPoint(PluginFeature feature, string endPointName); + + /// + /// Removes an existing endpoint + /// + /// The end point to remove + void RemovePluginEndPoint(PluginEndPoint endPoint); + /// /// Adds a new Web API controller and restarts the web server /// diff --git a/src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs b/src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs new file mode 100644 index 000000000..695bc8991 --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs @@ -0,0 +1,45 @@ +using System; +using EmbedIO; + +namespace Artemis.Core.Services +{ + public class PluginEndPoint + { + private readonly PluginsModule _pluginsModule; + + internal PluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule) + { + _pluginsModule = pluginsModule; + PluginFeature = pluginFeature; + Name = name; + + PluginFeature.Disabled += OnDisabled; + } + + /// + /// Gets the plugin the data model is associated with + /// + public PluginFeature PluginFeature { get; } + + /// + /// Gets the name of the end point + /// + public string Name { get; } + + /// + /// Gets the full URL of the end point + /// + public string Url => $"{_pluginsModule.BaseRoute}{PluginFeature.Plugin.Guid}/{Name}"; + + internal void ProcessRequest(IHttpContext context) + { + throw new NotImplementedException(); + } + + private void OnDisabled(object? sender, EventArgs e) + { + PluginFeature.Disabled -= OnDisabled; + _pluginsModule.RemovePluginEndPoint(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/PluginsModule.cs b/src/Artemis.Core/Services/WebServer/PluginsModule.cs index b3e635153..6208783df 100644 --- a/src/Artemis.Core/Services/WebServer/PluginsModule.cs +++ b/src/Artemis.Core/Services/WebServer/PluginsModule.cs @@ -1,13 +1,54 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; using EmbedIO; +using Newtonsoft.Json; namespace Artemis.Core.Services { internal class PluginsModule : WebModuleBase { + private readonly Dictionary> _pluginEndPoints; + /// public PluginsModule(string baseRoute) : base(baseRoute) { + _pluginEndPoints = new Dictionary>(); + OnUnhandledException += HandleUnhandledExceptionJson; + } + + public void AddPluginEndPoint(PluginEndPoint registration) + { + string id = registration.PluginFeature.Plugin.Guid.ToString(); + if (!_pluginEndPoints.TryGetValue(id, out Dictionary? registrations)) + { + registrations = new Dictionary(); + _pluginEndPoints.Add(id, registrations); + } + + if (registrations.ContainsKey(registration.Name)) + throw new ArtemisPluginException(registration.PluginFeature.Plugin, $"Plugin already registered an endpoint at {registration.Name}."); + registrations.Add(registration.Name, registration); + } + + public void RemovePluginEndPoint(PluginEndPoint registration) + { + string id = registration.PluginFeature.Plugin.Guid.ToString(); + if (!_pluginEndPoints.TryGetValue(id, out Dictionary? registrations)) + return; + if (!registrations.ContainsKey(registration.Name)) + return; + registrations.Remove(registration.Name); + } + + private async Task HandleUnhandledExceptionJson(IHttpContext context, Exception exception) + { + await context.SendStringAsync( + JsonConvert.SerializeObject(new ArtemisPluginException("The plugin failed to process the request", exception), Formatting.Indented), + MimeType.Json, + Encoding.UTF8 + ); } #region Overrides of WebModuleBase @@ -15,6 +56,31 @@ namespace Artemis.Core.Services /// protected override async Task OnRequestAsync(IHttpContext context) { + // Always stick to JSON + context.Response.ContentType = MimeType.Json; + + if (context.Route.SubPath == null) + throw HttpException.NotFound(); + + // Split the sub path + string[] pathParts = context.Route.SubPath.Substring(1).Split('/'); + // Expect a plugin ID and an endpoint + if (pathParts == null || pathParts.Length != 2) + throw HttpException.BadRequest("Path must contain a plugin ID and endpoint and nothing else."); + + // Find a matching plugin + if (!_pluginEndPoints.TryGetValue(pathParts[0], out Dictionary? endPoints)) + throw HttpException.NotFound($"Found no plugin with ID {pathParts[0]}."); + + // Find a matching endpoint + if (!endPoints.TryGetValue(pathParts[1], out PluginEndPoint? endPoint)) + throw HttpException.NotFound($"Found no endpoint called {pathParts[1]} for plugin with ID {pathParts[0]}."); + + // It is up to the registration how the request is eventually handled, it might even set a response here + endPoint.ProcessRequest(context); + + // No need to return ourselves, assume the request is fully handled by the end point + context.SetHandled(); } /// diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 64af5c2ef..07fb5ec73 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; +using System.Threading.Tasks; using EmbedIO; using EmbedIO.Actions; using EmbedIO.WebApi; +using Newtonsoft.Json; using Ninject; using Serilog; @@ -46,7 +49,7 @@ namespace Artemis.Core.Services .WithLocalSessionManager() .WithModule(apiModule) .WithModule(_pluginModule) - .WithModule(new ActionModule("/", HttpVerbs.Any, ctx => ctx.SendDataAsync(new {Message = "Error"}))); + .HandleHttpException((context, exception) => HandleHttpExceptionJson(context, exception)); // Add controllers to the API module foreach (WebApiControllerRegistration registration in _controllers) @@ -62,6 +65,11 @@ namespace Artemis.Core.Services return server; } + private async Task HandleHttpExceptionJson(IHttpContext context, IHttpException httpException) + { + await context.SendStringAsync(JsonConvert.SerializeObject(httpException, Formatting.Indented), MimeType.Json, Encoding.UTF8); + } + #endregion #region Event handlers @@ -85,6 +93,22 @@ namespace Artemis.Core.Services #endregion + #region Plugin endpoint management + + public PluginEndPoint AddPluginEndPoint(PluginFeature feature, string endPointName) + { + PluginEndPoint endPoint = new(feature, endPointName, _pluginModule); + _pluginModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public void RemovePluginEndPoint(PluginEndPoint endPoint) + { + _pluginModule.RemovePluginEndPoint(endPoint); + } + + #endregion + #region Controller management public void AddController() where T : WebApiController From fe847ad8f46730d71d7a52a37eb6460434680251 Mon Sep 17 00:00:00 2001 From: SpoinkyNL Date: Fri, 29 Jan 2021 00:54:27 +0100 Subject: [PATCH 4/6] Web server - Added serveral plugin end point types --- .../Artemis.Core.csproj.DotSettings | 1 + .../WebServer/EndPoints/JsonPluginEndPoint.cs | 76 +++++++++++++++ .../PluginEndPoint.cs} | 11 +-- .../WebServer/EndPoints/RawPluginEndPoint.cs | 36 ++++++++ .../EndPoints/StringPluginEndPoint.cs | 55 +++++++++++ .../WebServer/Interfaces/IWebServerService.cs | 51 +++++++++- .../Services/WebServer/PluginsModule.cs | 11 +-- .../Services/WebServer/WebServerService.cs | 92 ++++++++++++++----- 8 files changed, 292 insertions(+), 41 deletions(-) create mode 100644 src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs rename src/Artemis.Core/Services/WebServer/{PluginEndPointRegistration.cs => EndPoints/PluginEndPoint.cs} (79%) create mode 100644 src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs create mode 100644 src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 8aa37a05c..5ac92d7ad 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -65,6 +65,7 @@ True True True + True True True True diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs new file mode 100644 index 000000000..5a8ebcd98 --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using EmbedIO; +using Newtonsoft.Json; + +namespace Artemis.Core.Services +{ + /// + /// Represents a plugin web endpoint receiving an object of type and returning any + /// or . + /// Note: Both will be deserialized and serialized respectively using JSON. + /// + public class JsonPluginEndPoint : PluginEndPoint + { + private readonly Action? _requestHandler; + private readonly Func? _responseRequestHandler; + + internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action requestHandler) : base(pluginFeature, name, pluginsModule) + { + _requestHandler = requestHandler; + ThrowOnFail = true; + } + + internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func responseRequestHandler) : base(pluginFeature, name, pluginsModule) + { + _responseRequestHandler = responseRequestHandler; + ThrowOnFail = true; + } + + /// + /// Whether or not the end point should throw an exception if deserializing the received JSON fails. + /// If set to malformed JSON is silently ignored; if set to malformed + /// JSON throws a . + /// + public bool ThrowOnFail { get; set; } + + #region Overrides of PluginEndPoint + + /// + internal override void ProcessRequest(IHttpContext context) + { + if (context.Request.HttpVerb != HttpVerbs.Post) + throw HttpException.MethodNotAllowed("This end point only accepts POST calls"); + + context.Response.ContentType = MimeType.Json; + + using TextReader reader = context.OpenRequestText(); + object? response = null; + try + { + T deserialized = JsonConvert.DeserializeObject(reader.ReadToEnd()); + + if (_requestHandler != null) + { + _requestHandler(deserialized); + return; + } + + if (_responseRequestHandler != null) + response = _responseRequestHandler(deserialized); + else + throw new ArtemisCoreException("JSON plugin end point has no request handler"); + } + catch (JsonException) + { + if (ThrowOnFail) + throw; + } + + using TextWriter writer = context.OpenResponseText(); + writer.Write(JsonConvert.SerializeObject(response)); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs similarity index 79% rename from src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs rename to src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs index 695bc8991..e2250aa8b 100644 --- a/src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs @@ -3,7 +3,7 @@ using EmbedIO; namespace Artemis.Core.Services { - public class PluginEndPoint + public abstract class PluginEndPoint { private readonly PluginsModule _pluginsModule; @@ -22,19 +22,16 @@ namespace Artemis.Core.Services public PluginFeature PluginFeature { get; } /// - /// Gets the name of the end point + /// Gets the name of the end point /// public string Name { get; } /// - /// Gets the full URL of the end point + /// Gets the full URL of the end point /// public string Url => $"{_pluginsModule.BaseRoute}{PluginFeature.Plugin.Guid}/{Name}"; - internal void ProcessRequest(IHttpContext context) - { - throw new NotImplementedException(); - } + internal abstract void ProcessRequest(IHttpContext context); private void OnDisabled(object? sender, EventArgs e) { diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs new file mode 100644 index 000000000..49dc63614 --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs @@ -0,0 +1,36 @@ +using System; +using EmbedIO; + +namespace Artemis.Core.Services +{ + /// + /// Represents a plugin web endpoint that handles a raw . + /// + /// Note: This requires that you reference the EmbedIO + /// Nuget package. + /// + /// + public class RawPluginEndPoint : PluginEndPoint + { + /// + internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action requestHandler) : base(pluginFeature, name, pluginsModule) + { + RequestHandler = requestHandler; + } + + /// + /// Gets or sets the handler used to handle incoming requests to this endpoint + /// + public Action RequestHandler { get; } + + #region Overrides of PluginEndPoint + + /// + internal override void ProcessRequest(IHttpContext context) + { + RequestHandler(context); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs new file mode 100644 index 000000000..20cb823c3 --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using EmbedIO; + +namespace Artemis.Core.Services +{ + /// + /// Represents a plugin web endpoint receiving an a and returning a or + /// . + /// + public class StringPluginEndPoint : PluginEndPoint + { + private readonly Action? _requestHandler; + private readonly Func? _responseRequestHandler; + + internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action requestHandler) : base(pluginFeature, name, pluginsModule) + { + _requestHandler = requestHandler; + } + + internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func requestHandler) : base(pluginFeature, name, pluginsModule) + { + _responseRequestHandler = requestHandler; + } + + #region Overrides of PluginEndPoint + + /// + internal override void ProcessRequest(IHttpContext context) + { + if (context.Request.HttpVerb != HttpVerbs.Post) + throw HttpException.MethodNotAllowed("This end point only accepts POST calls"); + + context.Response.ContentType = MimeType.PlainText; + + using TextReader reader = context.OpenRequestText(); + string? response; + if (_requestHandler != null) + { + _requestHandler(reader.ReadToEnd()); + return; + } + + else if (_responseRequestHandler != null) + response = _responseRequestHandler(reader.ReadToEnd()); + else + throw new ArtemisCoreException("String plugin end point has no request handler"); + + using TextWriter writer = context.OpenResponseText(); + writer.Write(response); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs index 8be29ebfa..223f0b505 100644 --- a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -15,12 +15,59 @@ namespace Artemis.Core.Services WebServer? Server { get; } /// - /// Adds a new endpoint for the given plugin feature + /// Adds a new endpoint for the given plugin feature receiving an object of type + /// Note: Object will be deserialized using JSON. + /// + /// The type of object to be received + /// The plugin feature the end point is associated with + /// The name of the end point, must be unique + /// + /// The resulting end point + JsonPluginEndPoint AddJsonEndPoint(PluginFeature feature, string endPointName, Action requestHandler); + + /// + /// Adds a new endpoint for the given plugin feature receiving an object of type and + /// returning any . + /// Note: Both will be deserialized and serialized respectively using JSON. + /// + /// The type of object to be received + /// The plugin feature the end point is associated with + /// The name of the end point, must be unique + /// + /// The resulting end point + JsonPluginEndPoint AddResponsiveJsonEndPoint(PluginFeature feature, string endPointName, Func requestHandler); + + /// + /// Adds a new endpoint for the given plugin feature receiving an a . /// /// The plugin feature the end point is associated with /// The name of the end point, must be unique + /// /// The resulting end point - PluginEndPoint AddPluginEndPoint(PluginFeature feature, string endPointName); + StringPluginEndPoint AddStringEndPoint(PluginFeature feature, string endPointName, Action requestHandler); + + /// + /// Adds a new endpoint for the given plugin feature receiving an a and returning a + /// or . + /// + /// The plugin feature the end point is associated with + /// The name of the end point, must be unique + /// + /// The resulting end point + StringPluginEndPoint AddResponsiveStringEndPoint(PluginFeature feature, string endPointName, Func requestHandler); + + /// + /// Adds a new endpoint for the given plugin feature that handles a raw . + /// + /// Note: This requires that you reference the EmbedIO + /// Nuget package. + /// + /// + /// The plugin feature the end point is associated with + /// The name of the end point, must be unique + /// + /// The resulting end point + RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Action requestHandler); /// /// Removes an existing endpoint diff --git a/src/Artemis.Core/Services/WebServer/PluginsModule.cs b/src/Artemis.Core/Services/WebServer/PluginsModule.cs index 6208783df..c19af8add 100644 --- a/src/Artemis.Core/Services/WebServer/PluginsModule.cs +++ b/src/Artemis.Core/Services/WebServer/PluginsModule.cs @@ -56,26 +56,23 @@ namespace Artemis.Core.Services /// protected override async Task OnRequestAsync(IHttpContext context) { - // Always stick to JSON - context.Response.ContentType = MimeType.Json; - if (context.Route.SubPath == null) throw HttpException.NotFound(); - + // Split the sub path string[] pathParts = context.Route.SubPath.Substring(1).Split('/'); // Expect a plugin ID and an endpoint if (pathParts == null || pathParts.Length != 2) throw HttpException.BadRequest("Path must contain a plugin ID and endpoint and nothing else."); - + // Find a matching plugin if (!_pluginEndPoints.TryGetValue(pathParts[0], out Dictionary? endPoints)) throw HttpException.NotFound($"Found no plugin with ID {pathParts[0]}."); - + // Find a matching endpoint if (!endPoints.TryGetValue(pathParts[1], out PluginEndPoint? endPoint)) throw HttpException.NotFound($"Found no endpoint called {pathParts[1]} for plugin with ID {pathParts[0]}."); - + // It is up to the registration how the request is eventually handled, it might even set a response here endPoint.ProcessRequest(context); diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 07fb5ec73..13d7ed7fb 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -4,7 +4,6 @@ using System.IO; using System.Text; using System.Threading.Tasks; using EmbedIO; -using EmbedIO.Actions; using EmbedIO.WebApi; using Newtonsoft.Json; using Ninject; @@ -14,11 +13,11 @@ namespace Artemis.Core.Services { internal class WebServerService : IWebServerService, IDisposable { + private readonly List _controllers; private readonly IKernel _kernel; private readonly ILogger _logger; private readonly PluginsModule _pluginModule; private readonly PluginSetting _webServerPortSetting; - private readonly List _controllers; public WebServerService(IKernel kernel, ILogger logger, ISettingsService settingsService) { @@ -34,6 +33,27 @@ namespace Artemis.Core.Services Server.Start(); } + #region Event handlers + + private void WebServerPortSettingOnSettingChanged(object? sender, EventArgs e) + { + Server = CreateWebServer(); + Server.Start(); + } + + #endregion + + #region IDisposable + + /// + public void Dispose() + { + Server?.Dispose(); + _webServerPortSetting.SettingChanged -= WebServerPortSettingOnSettingChanged; + } + + #endregion + public WebServer? Server { get; private set; } #region Web server managament @@ -72,32 +92,54 @@ namespace Artemis.Core.Services #endregion - #region Event handlers - - private void WebServerPortSettingOnSettingChanged(object? sender, EventArgs e) - { - Server = CreateWebServer(); - Server.Start(); - } - - #endregion - - #region IDisposable - - /// - public void Dispose() - { - Server?.Dispose(); - _webServerPortSetting.SettingChanged -= WebServerPortSettingOnSettingChanged; - } - - #endregion - #region Plugin endpoint management - public PluginEndPoint AddPluginEndPoint(PluginFeature feature, string endPointName) + public JsonPluginEndPoint AddJsonEndPoint(PluginFeature feature, string endPointName, Action requestHandler) { - PluginEndPoint endPoint = new(feature, endPointName, _pluginModule); + if (feature == null) throw new ArgumentNullException(nameof(feature)); + if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); + if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); + JsonPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler); + _pluginModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public JsonPluginEndPoint AddResponsiveJsonEndPoint(PluginFeature feature, string endPointName, Func requestHandler) + { + if (feature == null) throw new ArgumentNullException(nameof(feature)); + if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); + if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); + JsonPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler); + _pluginModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public StringPluginEndPoint AddStringEndPoint(PluginFeature feature, string endPointName, Action requestHandler) + { + if (feature == null) throw new ArgumentNullException(nameof(feature)); + if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); + if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); + StringPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler); + _pluginModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public StringPluginEndPoint AddResponsiveStringEndPoint(PluginFeature feature, string endPointName, Func requestHandler) + { + if (feature == null) throw new ArgumentNullException(nameof(feature)); + if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); + if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); + StringPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler); + _pluginModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Action requestHandler) + { + if (feature == null) throw new ArgumentNullException(nameof(feature)); + if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); + if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); + RawPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler); _pluginModule.AddPluginEndPoint(endPoint); return endPoint; } From 46958338b981df68889415a78cfde81cff2f0af6 Mon Sep 17 00:00:00 2001 From: SpoinkyNL Date: Fri, 29 Jan 2021 20:48:40 +0100 Subject: [PATCH 5/6] Web API - Added end points API Web API - Return unhandled exceptions in JSON Plugin end points - Made built-in end points async --- .../Artemis.Core.csproj.DotSettings | 1 + .../Controllers/PluginsController.cs | 35 ++ .../WebServer/EndPoints/JsonPluginEndPoint.cs | 12 +- .../WebServer/EndPoints/PluginEndPoint.cs | 44 ++- .../WebServer/EndPoints/RawPluginEndPoint.cs | 25 +- .../EndPoints/StringPluginEndPoint.cs | 20 +- .../WebServer/Interfaces/IWebServerService.cs | 14 +- .../Services/WebServer/PluginsModule.cs | 41 +-- .../Services/WebServer/WebServerService.cs | 298 ++++++++++-------- .../Tabs/General/GeneralSettingsTabView.xaml | 29 +- .../General/GeneralSettingsTabViewModel.cs | 5 + 11 files changed, 346 insertions(+), 178 deletions(-) create mode 100644 src/Artemis.Core/Services/WebServer/Controllers/PluginsController.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 5ac92d7ad..61728d988 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -65,6 +65,7 @@ True True True + True True True True diff --git a/src/Artemis.Core/Services/WebServer/Controllers/PluginsController.cs b/src/Artemis.Core/Services/WebServer/Controllers/PluginsController.cs new file mode 100644 index 000000000..7c4e8c29e --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/Controllers/PluginsController.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EmbedIO; +using EmbedIO.Routing; +using EmbedIO.WebApi; + +namespace Artemis.Core.Services +{ + internal class PluginsController : WebApiController + { + private readonly IWebServerService _webServerService; + + public PluginsController(IWebServerService webServerService) + { + _webServerService = webServerService; + } + + [Route(HttpVerbs.Get, "/plugins/endpoints")] + public IReadOnlyCollection GetPluginEndPoints() + { + return _webServerService.PluginsModule.PluginEndPoints; + } + + [Route(HttpVerbs.Get, "/plugins/endpoints/{plugin}/{endPoint}")] + public PluginEndPoint GetPluginEndPoint(Guid plugin, string endPoint) + { + PluginEndPoint? pluginEndPoint = _webServerService.PluginsModule.PluginEndPoints.FirstOrDefault(e => e.PluginFeature.Plugin.Guid == plugin && e.Name == endPoint); + if (pluginEndPoint == null) + throw HttpException.NotFound(); + + return pluginEndPoint; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs index 5a8ebcd98..061bd984e 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; using EmbedIO; using Newtonsoft.Json; @@ -19,12 +20,15 @@ namespace Artemis.Core.Services { _requestHandler = requestHandler; ThrowOnFail = true; + Accepts = MimeType.Json; } internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func responseRequestHandler) : base(pluginFeature, name, pluginsModule) { _responseRequestHandler = responseRequestHandler; ThrowOnFail = true; + Accepts = MimeType.Json; + Returns = MimeType.Json; } /// @@ -37,7 +41,7 @@ namespace Artemis.Core.Services #region Overrides of PluginEndPoint /// - internal override void ProcessRequest(IHttpContext context) + protected override async Task ProcessRequest(IHttpContext context) { if (context.Request.HttpVerb != HttpVerbs.Post) throw HttpException.MethodNotAllowed("This end point only accepts POST calls"); @@ -48,7 +52,7 @@ namespace Artemis.Core.Services object? response = null; try { - T deserialized = JsonConvert.DeserializeObject(reader.ReadToEnd()); + T deserialized = JsonConvert.DeserializeObject(await reader.ReadToEndAsync()); if (_requestHandler != null) { @@ -67,8 +71,8 @@ namespace Artemis.Core.Services throw; } - using TextWriter writer = context.OpenResponseText(); - writer.Write(JsonConvert.SerializeObject(response)); + await using TextWriter writer = context.OpenResponseText(); + await writer.WriteAsync(JsonConvert.SerializeObject(response)); } #endregion diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs index e2250aa8b..c81c772ee 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs @@ -1,8 +1,13 @@ using System; +using System.Threading.Tasks; using EmbedIO; +using Newtonsoft.Json; namespace Artemis.Core.Services { + /// + /// Represents a base type for plugin end points to be targeted by the + /// public abstract class PluginEndPoint { private readonly PluginsModule _pluginsModule; @@ -16,11 +21,6 @@ namespace Artemis.Core.Services PluginFeature.Disabled += OnDisabled; } - /// - /// Gets the plugin the data model is associated with - /// - public PluginFeature PluginFeature { get; } - /// /// Gets the name of the end point /// @@ -29,9 +29,39 @@ namespace Artemis.Core.Services /// /// Gets the full URL of the end point /// - public string Url => $"{_pluginsModule.BaseRoute}{PluginFeature.Plugin.Guid}/{Name}"; + public string Url => $"{_pluginsModule.ServerUrl.TrimEnd('/')}{_pluginsModule.BaseRoute}{PluginFeature.Plugin.Guid}/{Name}"; - internal abstract void ProcessRequest(IHttpContext context); + /// + /// Gets the plugin the end point is associated with + /// + [JsonIgnore] + public PluginFeature PluginFeature { get; } + + /// + /// Gets the plugin info of the plugin the end point is associated with + /// + public PluginInfo PluginInfo => PluginFeature.Plugin.Info; + + /// + /// Gets the mime type of the input this end point accepts + /// + public string Accepts { get; protected set; } + + /// + /// Gets the mime type of the output this end point returns + /// + public string Returns { get; protected set; } + + /// + /// Called whenever the end point has to process a request + /// + /// The HTTP context of the request + protected abstract Task ProcessRequest(IHttpContext context); + + internal async Task InternalProcessRequest(IHttpContext context) + { + await ProcessRequest(context); + } private void OnDisabled(object? sender, EventArgs e) { diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs index 49dc63614..1aaca2ac3 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using EmbedIO; namespace Artemis.Core.Services @@ -13,7 +14,7 @@ namespace Artemis.Core.Services public class RawPluginEndPoint : PluginEndPoint { /// - internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action requestHandler) : base(pluginFeature, name, pluginsModule) + internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func requestHandler) : base(pluginFeature, name, pluginsModule) { RequestHandler = requestHandler; } @@ -21,14 +22,30 @@ namespace Artemis.Core.Services /// /// Gets or sets the handler used to handle incoming requests to this endpoint /// - public Action RequestHandler { get; } + public Func RequestHandler { get; } + + /// + /// Sets the mime type this plugin end point accepts + /// + public void SetAcceptType(string type) + { + Accepts = type; + } + + /// + /// Sets the mime type this plugin end point returns + /// + public void SetReturnType(string type) + { + Returns = type; + } #region Overrides of PluginEndPoint /// - internal override void ProcessRequest(IHttpContext context) + protected override async Task ProcessRequest(IHttpContext context) { - RequestHandler(context); + await RequestHandler(context); } #endregion diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs index 20cb823c3..b5f65bb2d 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; using EmbedIO; namespace Artemis.Core.Services @@ -16,38 +17,41 @@ namespace Artemis.Core.Services internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action requestHandler) : base(pluginFeature, name, pluginsModule) { _requestHandler = requestHandler; + Accepts = MimeType.PlainText; } internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func requestHandler) : base(pluginFeature, name, pluginsModule) { _responseRequestHandler = requestHandler; + Accepts = MimeType.PlainText; + Returns = MimeType.PlainText; } #region Overrides of PluginEndPoint /// - internal override void ProcessRequest(IHttpContext context) + protected override async Task ProcessRequest(IHttpContext context) { if (context.Request.HttpVerb != HttpVerbs.Post) throw HttpException.MethodNotAllowed("This end point only accepts POST calls"); - + context.Response.ContentType = MimeType.PlainText; using TextReader reader = context.OpenRequestText(); string? response; if (_requestHandler != null) { - _requestHandler(reader.ReadToEnd()); + _requestHandler(await reader.ReadToEndAsync()); return; } - - else if (_responseRequestHandler != null) - response = _responseRequestHandler(reader.ReadToEnd()); + + if (_responseRequestHandler != null) + response = _responseRequestHandler(await reader.ReadToEndAsync()); else throw new ArtemisCoreException("String plugin end point has no request handler"); - using TextWriter writer = context.OpenResponseText(); - writer.Write(response); + await using TextWriter writer = context.OpenResponseText(); + await writer.WriteAsync(response); } #endregion diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs index 223f0b505..704a4a19c 100644 --- a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using EmbedIO; using EmbedIO.WebApi; @@ -10,10 +11,15 @@ namespace Artemis.Core.Services public interface IWebServerService : IArtemisService { /// - /// Gets the currently active instance of the web server + /// Gets the current instance of the web server, replaced when occurs. /// WebServer? Server { get; } + /// + /// Gets the plugins module containing all plugin end points + /// + PluginsModule PluginsModule { get; } + /// /// Adds a new endpoint for the given plugin feature receiving an object of type /// Note: Object will be deserialized using JSON. @@ -67,7 +73,7 @@ namespace Artemis.Core.Services /// The name of the end point, must be unique /// /// The resulting end point - RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Action requestHandler); + RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Func requestHandler); /// /// Removes an existing endpoint @@ -88,8 +94,8 @@ namespace Artemis.Core.Services void RemoveController() where T : WebApiController; /// - /// Occurs when a new instance of the web server was been created + /// Occurs when the web server has been created and is about to start. This is the ideal place to add your own modules. /// - event EventHandler? WebServerCreated; + event EventHandler? WebServerStarting; } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/PluginsModule.cs b/src/Artemis.Core/Services/WebServer/PluginsModule.cs index c19af8add..78de97e66 100644 --- a/src/Artemis.Core/Services/WebServer/PluginsModule.cs +++ b/src/Artemis.Core/Services/WebServer/PluginsModule.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading.Tasks; using EmbedIO; @@ -7,18 +8,20 @@ using Newtonsoft.Json; namespace Artemis.Core.Services { - internal class PluginsModule : WebModuleBase + /// + /// Represents an EmbedIO web module used to process web requests and forward them to the right + /// . + /// + public class PluginsModule : WebModuleBase { private readonly Dictionary> _pluginEndPoints; - /// - public PluginsModule(string baseRoute) : base(baseRoute) + internal PluginsModule(string baseRoute) : base(baseRoute) { _pluginEndPoints = new Dictionary>(); - OnUnhandledException += HandleUnhandledExceptionJson; } - public void AddPluginEndPoint(PluginEndPoint registration) + internal void AddPluginEndPoint(PluginEndPoint registration) { string id = registration.PluginFeature.Plugin.Guid.ToString(); if (!_pluginEndPoints.TryGetValue(id, out Dictionary? registrations)) @@ -32,7 +35,7 @@ namespace Artemis.Core.Services registrations.Add(registration.Name, registration); } - public void RemovePluginEndPoint(PluginEndPoint registration) + internal void RemovePluginEndPoint(PluginEndPoint registration) { string id = registration.PluginFeature.Plugin.Guid.ToString(); if (!_pluginEndPoints.TryGetValue(id, out Dictionary? registrations)) @@ -42,15 +45,6 @@ namespace Artemis.Core.Services registrations.Remove(registration.Name); } - private async Task HandleUnhandledExceptionJson(IHttpContext context, Exception exception) - { - await context.SendStringAsync( - JsonConvert.SerializeObject(new ArtemisPluginException("The plugin failed to process the request", exception), Formatting.Indented), - MimeType.Json, - Encoding.UTF8 - ); - } - #region Overrides of WebModuleBase /// @@ -58,23 +52,23 @@ namespace Artemis.Core.Services { if (context.Route.SubPath == null) throw HttpException.NotFound(); - + // Split the sub path string[] pathParts = context.Route.SubPath.Substring(1).Split('/'); // Expect a plugin ID and an endpoint if (pathParts == null || pathParts.Length != 2) throw HttpException.BadRequest("Path must contain a plugin ID and endpoint and nothing else."); - + // Find a matching plugin if (!_pluginEndPoints.TryGetValue(pathParts[0], out Dictionary? endPoints)) throw HttpException.NotFound($"Found no plugin with ID {pathParts[0]}."); - + // Find a matching endpoint if (!endPoints.TryGetValue(pathParts[1], out PluginEndPoint? endPoint)) throw HttpException.NotFound($"Found no endpoint called {pathParts[1]} for plugin with ID {pathParts[0]}."); - + // It is up to the registration how the request is eventually handled, it might even set a response here - endPoint.ProcessRequest(context); + await endPoint.InternalProcessRequest(context); // No need to return ourselves, assume the request is fully handled by the end point context.SetHandled(); @@ -83,6 +77,13 @@ namespace Artemis.Core.Services /// public override bool IsFinalHandler => true; + internal string? ServerUrl { get; set; } + + /// + /// Gets a read only collection containing all current plugin end points + /// + public IReadOnlyCollection PluginEndPoints => new List(_pluginEndPoints.SelectMany(p => p.Value.Values)); + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 13d7ed7fb..79f7959d7 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -16,7 +16,6 @@ namespace Artemis.Core.Services private readonly List _controllers; private readonly IKernel _kernel; private readonly ILogger _logger; - private readonly PluginsModule _pluginModule; private readonly PluginSetting _webServerPortSetting; public WebServerService(IKernel kernel, ILogger logger, ISettingsService settingsService) @@ -28,17 +27,182 @@ namespace Artemis.Core.Services _webServerPortSetting = settingsService.GetSetting("WebServer.Port", 9696); _webServerPortSetting.SettingChanged += WebServerPortSettingOnSettingChanged; - _pluginModule = new PluginsModule("/plugin"); + PluginsModule = new PluginsModule("/plugins"); + + StartWebServer(); + } + + public WebServer? Server { get; private set; } + public PluginsModule PluginsModule { get; } + + #region Web server managament + + private WebServer CreateWebServer() + { + Server?.Dispose(); + Server = null; + + string url = $"http://localhost:{_webServerPortSetting.Value}/"; + WebApiModule apiModule = new("/api/", JsonNetSerializer); + PluginsModule.ServerUrl = url; + WebServer server = new WebServer(o => o.WithUrlPrefix(url).WithMode(HttpListenerMode.EmbedIO)) + .WithLocalSessionManager() + .WithModule(apiModule) + .WithModule(PluginsModule) + .HandleHttpException((context, exception) => HandleHttpExceptionJson(context, exception)) + .HandleUnhandledException(JsonExceptionHandlerCallback); + + // Add built-in core controllers to the API module + apiModule.RegisterController(() => _kernel.Get()); + // Add registered controllers to the API module + foreach (WebApiControllerRegistration registration in _controllers) + apiModule.RegisterController(registration.ControllerType, (Func) registration.UntypedFactory); + + // Listen for state changes. + server.StateChanged += (s, e) => _logger.Verbose("WebServer new state - {state}", e.NewState); + + // Store the URL in a webserver.txt file so that remote applications can find it + File.WriteAllText(Path.Combine(Constants.DataFolder, "webserver.txt"), url); + + return server; + } + + private void StartWebServer() + { Server = CreateWebServer(); + OnWebServerStarting(); Server.Start(); } + #endregion + + #region Plugin endpoint management + + public JsonPluginEndPoint AddJsonEndPoint(PluginFeature feature, string endPointName, Action requestHandler) + { + if (feature == null) throw new ArgumentNullException(nameof(feature)); + if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); + if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); + JsonPluginEndPoint endPoint = new(feature, endPointName, PluginsModule, requestHandler); + PluginsModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public JsonPluginEndPoint AddResponsiveJsonEndPoint(PluginFeature feature, string endPointName, Func requestHandler) + { + if (feature == null) throw new ArgumentNullException(nameof(feature)); + if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); + if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); + JsonPluginEndPoint endPoint = new(feature, endPointName, PluginsModule, requestHandler); + PluginsModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public StringPluginEndPoint AddStringEndPoint(PluginFeature feature, string endPointName, Action requestHandler) + { + if (feature == null) throw new ArgumentNullException(nameof(feature)); + if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); + if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); + StringPluginEndPoint endPoint = new(feature, endPointName, PluginsModule, requestHandler); + PluginsModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public StringPluginEndPoint AddResponsiveStringEndPoint(PluginFeature feature, string endPointName, Func requestHandler) + { + if (feature == null) throw new ArgumentNullException(nameof(feature)); + if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); + if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); + StringPluginEndPoint endPoint = new(feature, endPointName, PluginsModule, requestHandler); + PluginsModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Func requestHandler) + { + if (feature == null) throw new ArgumentNullException(nameof(feature)); + if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); + if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); + RawPluginEndPoint endPoint = new(feature, endPointName, PluginsModule, requestHandler); + PluginsModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public void RemovePluginEndPoint(PluginEndPoint endPoint) + { + PluginsModule.RemovePluginEndPoint(endPoint); + } + + #endregion + + #region Controller management + + public void AddController() where T : WebApiController + { + _controllers.Add(new WebApiControllerRegistration(_kernel)); + StartWebServer(); + } + + public void RemoveController() where T : WebApiController + { + _controllers.RemoveAll(r => r.ControllerType == typeof(T)); + StartWebServer(); + } + + #endregion + + #region Handlers + + private async Task JsonExceptionHandlerCallback(IHttpContext context, Exception exception) + { + context.Response.ContentType = MimeType.Json; + await using TextWriter writer = context.OpenResponseText(); + + string response = JsonConvert.SerializeObject(new Dictionary() + { + {"StatusCode", context.Response.StatusCode}, + {"StackTrace", exception.StackTrace}, + {"Type", exception.GetType().FullName}, + {"Message", exception.Message}, + {"Data", exception.Data}, + {"InnerException", exception.InnerException}, + {"HelpLink", exception.HelpLink}, + {"Source", exception.Source}, + {"HResult", exception.HResult} + }); + await writer.WriteAsync(response); + } + + private async Task JsonNetSerializer(IHttpContext context, object? data) + { + context.Response.ContentType = MimeType.Json; + await using TextWriter writer = context.OpenResponseText(); + await writer.WriteAsync(JsonConvert.SerializeObject(data)); + } + + private async Task HandleHttpExceptionJson(IHttpContext context, IHttpException httpException) + { + await context.SendStringAsync(JsonConvert.SerializeObject(httpException, Formatting.Indented), MimeType.Json, Encoding.UTF8); + } + + #endregion + + #region Events + + public event EventHandler? WebServerStarting; + + protected virtual void OnWebServerStarting() + { + WebServerStarting?.Invoke(this, EventArgs.Empty); + } + + #endregion + #region Event handlers private void WebServerPortSettingOnSettingChanged(object? sender, EventArgs e) { - Server = CreateWebServer(); - Server.Start(); + StartWebServer(); } #endregion @@ -53,131 +217,5 @@ namespace Artemis.Core.Services } #endregion - - public WebServer? Server { get; private set; } - - #region Web server managament - - private WebServer CreateWebServer() - { - Server?.Dispose(); - Server = null; - - string url = $"http://localhost:{_webServerPortSetting.Value}/"; - WebApiModule apiModule = new("/api/"); - WebServer server = new WebServer(o => o.WithUrlPrefix(url).WithMode(HttpListenerMode.EmbedIO)) - .WithLocalSessionManager() - .WithModule(apiModule) - .WithModule(_pluginModule) - .HandleHttpException((context, exception) => HandleHttpExceptionJson(context, exception)); - - // Add controllers to the API module - foreach (WebApiControllerRegistration registration in _controllers) - apiModule.RegisterController(registration.ControllerType, (Func) registration.UntypedFactory); - - // Listen for state changes. - server.StateChanged += (s, e) => _logger.Verbose("WebServer new state - {state}", e.NewState); - - // Store the URL in a webserver.txt file so that remote applications can find it - File.WriteAllText(Path.Combine(Constants.DataFolder, "webserver.txt"), url); - OnWebServerCreated(); - - return server; - } - - private async Task HandleHttpExceptionJson(IHttpContext context, IHttpException httpException) - { - await context.SendStringAsync(JsonConvert.SerializeObject(httpException, Formatting.Indented), MimeType.Json, Encoding.UTF8); - } - - #endregion - - #region Plugin endpoint management - - public JsonPluginEndPoint AddJsonEndPoint(PluginFeature feature, string endPointName, Action requestHandler) - { - if (feature == null) throw new ArgumentNullException(nameof(feature)); - if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); - if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); - JsonPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler); - _pluginModule.AddPluginEndPoint(endPoint); - return endPoint; - } - - public JsonPluginEndPoint AddResponsiveJsonEndPoint(PluginFeature feature, string endPointName, Func requestHandler) - { - if (feature == null) throw new ArgumentNullException(nameof(feature)); - if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); - if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); - JsonPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler); - _pluginModule.AddPluginEndPoint(endPoint); - return endPoint; - } - - public StringPluginEndPoint AddStringEndPoint(PluginFeature feature, string endPointName, Action requestHandler) - { - if (feature == null) throw new ArgumentNullException(nameof(feature)); - if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); - if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); - StringPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler); - _pluginModule.AddPluginEndPoint(endPoint); - return endPoint; - } - - public StringPluginEndPoint AddResponsiveStringEndPoint(PluginFeature feature, string endPointName, Func requestHandler) - { - if (feature == null) throw new ArgumentNullException(nameof(feature)); - if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); - if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); - StringPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler); - _pluginModule.AddPluginEndPoint(endPoint); - return endPoint; - } - - public RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Action requestHandler) - { - if (feature == null) throw new ArgumentNullException(nameof(feature)); - if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); - if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler)); - RawPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler); - _pluginModule.AddPluginEndPoint(endPoint); - return endPoint; - } - - public void RemovePluginEndPoint(PluginEndPoint endPoint) - { - _pluginModule.RemovePluginEndPoint(endPoint); - } - - #endregion - - #region Controller management - - public void AddController() where T : WebApiController - { - _controllers.Add(new WebApiControllerRegistration(_kernel)); - Server = CreateWebServer(); - Server.Start(); - } - - public void RemoveController() where T : WebApiController - { - _controllers.RemoveAll(r => r.ControllerType == typeof(T)); - Server = CreateWebServer(); - Server.Start(); - } - - #endregion - - #region Events - - public event EventHandler? WebServerCreated; - - protected virtual void OnWebServerCreated() - { - WebServerCreated?.Invoke(this, EventArgs.Empty); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml index ac964893d..f7c14eebf 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml @@ -171,7 +171,7 @@ - + @@ -195,6 +195,33 @@ + + Web server + + + + + + + + + + + + + Web server port + + Artemis runs a local web server that can be used to externally interact with the application. + This web server can only be accessed by applications running on your own computer, e.g. supported games. + + + + + + + + + Updating diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs index 4a304194b..9318f27f8 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs @@ -79,6 +79,9 @@ namespace Artemis.UI.Screens.Settings.Tabs.General LayerBrushProviderId = "Artemis.Plugins.LayerBrushes.Color.ColorBrushProvider-92a9d6ba", BrushType = "ColorBrush" }); + + WebServerPortSetting = _settingsService.GetSetting("WebServer.Port", 9696); + WebServerPortSetting.AutoSave = true; } public BindableCollection LayerBrushDescriptors { get; } @@ -234,6 +237,8 @@ namespace Artemis.UI.Screens.Settings.Tabs.General } } + public PluginSetting WebServerPortSetting { get; } + public bool CanOfferUpdatesIfFound { get => _canOfferUpdatesIfFound; From c1b002723108adf3c6a08a3009c4c056ba490222 Mon Sep 17 00:00:00 2001 From: SpoinkyNL Date: Sat, 30 Jan 2021 21:55:29 +0100 Subject: [PATCH 6/6] Plugin features - Added PluginFeature attribute Plugin features -Added PluginFeatureInfo to plugin features Web server - Moved REST APIs to plugins --- .../Artemis.Core.csproj.DotSettings | 1 + .../Plugins/DataModelExpansions/DataModel.cs | 3 + src/Artemis.Core/Plugins/Plugin.cs | 1 - src/Artemis.Core/Plugins/PluginFeature.cs | 5 + .../Plugins/PluginFeatureAttribute.cs | 27 +++ src/Artemis.Core/Plugins/PluginFeatureInfo.cs | 92 +++++++++ .../Services/PluginManagementService.cs | 5 + .../Controllers/PluginsController.cs | 35 ---- .../Services/WebServer/WebServerService.cs | 5 +- src/Artemis.UI/Bootstrapper.cs | 5 +- .../RemoteController.cs | 2 +- .../Tabs/General/GeneralSettingsTabView.xaml | 178 ++++++++++-------- .../General/GeneralSettingsTabViewModel.cs | 51 +++-- .../Tabs/Plugins/PluginFeatureView.xaml | 8 +- .../Tabs/Plugins/PluginFeatureViewModel.cs | 32 +--- .../Services/RegistrationService.cs | 12 +- .../Interfaces/IRemoteManagementService.cs | 6 - .../RemoteManagementService.cs | 12 -- 18 files changed, 297 insertions(+), 183 deletions(-) create mode 100644 src/Artemis.Core/Plugins/PluginFeatureAttribute.cs create mode 100644 src/Artemis.Core/Plugins/PluginFeatureInfo.cs delete mode 100644 src/Artemis.Core/Services/WebServer/Controllers/PluginsController.cs rename src/Artemis.UI/{Services/RemoteManagement => Controllers}/RemoteController.cs (96%) delete mode 100644 src/Artemis.UI/Services/RemoteManagement/Interfaces/IRemoteManagementService.cs delete mode 100644 src/Artemis.UI/Services/RemoteManagement/RemoteManagementService.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 61728d988..b1ea041c1 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -68,6 +68,7 @@ True True True + True True True True \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs b/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs index 27418f2d1..e207c18eb 100644 --- a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs +++ b/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using Artemis.Core.Modules; using Humanizer; +using Newtonsoft.Json; namespace Artemis.Core.DataModelExpansions { @@ -28,12 +29,14 @@ namespace Artemis.Core.DataModelExpansions /// /// Gets the plugin feature this data model belongs to /// + [JsonIgnore] [DataModelIgnore] public DataModelPluginFeature Feature { get; internal set; } /// /// Gets the describing this data model /// + [JsonIgnore] [DataModelIgnore] public DataModelPropertyAttribute DataModelDescription { get; internal set; } diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index 853649b0b..0575a80ac 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -44,7 +44,6 @@ namespace Artemis.Core /// public DirectoryInfo Directory { get; } - /// /// Gets or sets a configuration dialog for this plugin that is accessible in the UI under Settings > Plugins /// diff --git a/src/Artemis.Core/Plugins/PluginFeature.cs b/src/Artemis.Core/Plugins/PluginFeature.cs index 733c3ee9b..b0e08554f 100644 --- a/src/Artemis.Core/Plugins/PluginFeature.cs +++ b/src/Artemis.Core/Plugins/PluginFeature.cs @@ -13,6 +13,11 @@ namespace Artemis.Core private bool _isEnabled; private Exception? _loadException; + /// + /// Gets the plugin feature info related to this feature + /// + public PluginFeatureInfo Info { get; internal set; } = null!; // Will be set right after construction + /// /// Gets the plugin that provides this feature /// diff --git a/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs new file mode 100644 index 000000000..0bb78fb75 --- /dev/null +++ b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs @@ -0,0 +1,27 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Represents an attribute that describes a plugin feature + /// + [AttributeUsage(AttributeTargets.Class)] + public class PluginFeatureAttribute : Attribute + { + /// + /// Gets or sets the user-friendly name for this property, shown in the UI. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the user-friendly description for this property, shown in the UI. + /// + public string? Description { get; set; } + + /// + /// The plugins display icon that's shown in the settings see for + /// available icons + /// + public string? Icon { get; set; } + } +} diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs new file mode 100644 index 000000000..7fe5a20de --- /dev/null +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -0,0 +1,92 @@ +using Artemis.Core.DataModelExpansions; +using Artemis.Core.DeviceProviders; +using Artemis.Core.LayerBrushes; +using Artemis.Core.LayerEffects; +using Artemis.Core.Modules; +using Humanizer; +using Newtonsoft.Json; + +namespace Artemis.Core +{ + /// + /// Represents basic info about a plugin feature and contains a reference to the instance of said feature + /// + [JsonObject(MemberSerialization.OptIn)] + public class PluginFeatureInfo : CorePropertyChanged + { + private string? _description; + private string? _icon; + private string _name = null!; + private PluginFeature _pluginFeature = null!; + + internal PluginFeatureInfo() + { + } + + internal PluginFeatureInfo(PluginFeature instance, PluginFeatureAttribute? attribute) + { + Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title); + Description = attribute?.Description; + Icon = attribute?.Icon; + PluginFeature = instance; + + if (Icon != null) return; + Icon = PluginFeature switch + { + BaseDataModelExpansion => "TableAdd", + DeviceProvider => "Devices", + ProfileModule => "VectorRectangle", + Module => "GearBox", + LayerBrushProvider => "Brush", + LayerEffectProvider => "AutoAwesome", + _ => "Plugin" + }; + } + + /// + /// The name of the plugin + /// + [JsonProperty(Required = Required.Always)] + public string Name + { + get => _name; + internal set => SetAndNotify(ref _name, value); + } + + /// + /// A short description of the plugin + /// + [JsonProperty] + public string? Description + { + get => _description; + set => SetAndNotify(ref _description, value); + } + + /// + /// The plugins display icon that's shown in the settings see for + /// available icons + /// + [JsonProperty] + public string? Icon + { + get => _icon; + set => SetAndNotify(ref _icon, value); + } + + /// + /// Gets the plugin this info is associated with + /// + public PluginFeature PluginFeature + { + get => _pluginFeature; + internal set => SetAndNotify(ref _pluginFeature, value); + } + + /// + public override string ToString() + { + return PluginFeature.Id; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 39ef62b89..c15122d1d 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -9,6 +9,7 @@ using Artemis.Core.DeviceProviders; using Artemis.Core.Ninject; using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Repositories.Interfaces; +using Humanizer; using McMaster.NETCore.Plugins; using Ninject; using Ninject.Extensions.ChildKernel; @@ -342,6 +343,10 @@ namespace Artemis.Core.Services // Include Plugin as a parameter for the PluginSettingsProvider IParameter[] parameters = {new Parameter("Plugin", plugin, false)}; PluginFeature instance = (PluginFeature) plugin.Kernel.Get(featureType, parameters); + + // Get the PluginFeature attribute which contains extra info on the feature + PluginFeatureAttribute? pluginFeatureAttribute = (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute)); + instance.Info = new PluginFeatureInfo(instance, pluginFeatureAttribute); plugin.AddFeature(instance); // Load the enabled state and if not found, default to true diff --git a/src/Artemis.Core/Services/WebServer/Controllers/PluginsController.cs b/src/Artemis.Core/Services/WebServer/Controllers/PluginsController.cs deleted file mode 100644 index 7c4e8c29e..000000000 --- a/src/Artemis.Core/Services/WebServer/Controllers/PluginsController.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using EmbedIO; -using EmbedIO.Routing; -using EmbedIO.WebApi; - -namespace Artemis.Core.Services -{ - internal class PluginsController : WebApiController - { - private readonly IWebServerService _webServerService; - - public PluginsController(IWebServerService webServerService) - { - _webServerService = webServerService; - } - - [Route(HttpVerbs.Get, "/plugins/endpoints")] - public IReadOnlyCollection GetPluginEndPoints() - { - return _webServerService.PluginsModule.PluginEndPoints; - } - - [Route(HttpVerbs.Get, "/plugins/endpoints/{plugin}/{endPoint}")] - public PluginEndPoint GetPluginEndPoint(Guid plugin, string endPoint) - { - PluginEndPoint? pluginEndPoint = _webServerService.PluginsModule.PluginEndPoints.FirstOrDefault(e => e.PluginFeature.Plugin.Guid == plugin && e.Name == endPoint); - if (pluginEndPoint == null) - throw HttpException.NotFound(); - - return pluginEndPoint; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 79f7959d7..41b3911de 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -52,8 +52,6 @@ namespace Artemis.Core.Services .HandleHttpException((context, exception) => HandleHttpExceptionJson(context, exception)) .HandleUnhandledException(JsonExceptionHandlerCallback); - // Add built-in core controllers to the API module - apiModule.RegisterController(() => _kernel.Get()); // Add registered controllers to the API module foreach (WebApiControllerRegistration registration in _controllers) apiModule.RegisterController(registration.ControllerType, (Func) registration.UntypedFactory); @@ -177,7 +175,8 @@ namespace Artemis.Core.Services { context.Response.ContentType = MimeType.Json; await using TextWriter writer = context.OpenResponseText(); - await writer.WriteAsync(JsonConvert.SerializeObject(data)); + string json = JsonConvert.SerializeObject(data, new JsonSerializerSettings {PreserveReferencesHandling = PreserveReferencesHandling.Objects}); + await writer.WriteAsync(json); } private async Task HandleHttpExceptionJson(IHttpContext context, IHttpException httpException) diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs index 446cba8cb..5544c2307 100644 --- a/src/Artemis.UI/Bootstrapper.cs +++ b/src/Artemis.UI/Bootstrapper.cs @@ -87,8 +87,9 @@ namespace Artemis.UI } }); - Kernel.Get().RegisterInputProvider(); - Kernel.Get(); + IRegistrationService registrationService = Kernel.Get(); + registrationService.RegisterInputProvider(); + registrationService.RegisterControllers(); } protected override void ConfigureIoC(IKernel kernel) diff --git a/src/Artemis.UI/Services/RemoteManagement/RemoteController.cs b/src/Artemis.UI/Controllers/RemoteController.cs similarity index 96% rename from src/Artemis.UI/Services/RemoteManagement/RemoteController.cs rename to src/Artemis.UI/Controllers/RemoteController.cs index a39468924..2c8c793d5 100644 --- a/src/Artemis.UI/Services/RemoteManagement/RemoteController.cs +++ b/src/Artemis.UI/Controllers/RemoteController.cs @@ -5,7 +5,7 @@ using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; -namespace Artemis.UI.Services +namespace Artemis.UI.Controllers { public class RemoteController : WebApiController { diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml index f7c14eebf..9f0564084 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml @@ -59,6 +59,28 @@ + + + + + + + + + + + Startup delay + + Set the amount of seconds to wait before running Artemis with Windows. + If some devices don't work because Artemis starts before the manufacturer's software, try increasing this value. + + + + + + + + @@ -89,62 +111,14 @@ - - Setup wizard - - Opens the startup wizard usually shown when Artemis first starts. + + Log level + + Sets the logging level, a higher logging level will result in more log files. - - - - - - - - - - - - - - - - Debugger - - Use the debugger to see the raw image Artemis is rendering on the surface. - - - - - - - - - - - - - - - - - - - Application files - - Opens the directory where application files like plugins and settings are stored. - - - - + @@ -170,28 +144,6 @@ - - - - - - - - - - - - - Log level - - Sets the logging level, a higher logging level will result in more log files. - - - - - - - @@ -418,6 +370,82 @@ + + + + Tools + + + + + + + + + + + + + Setup wizard + + Opens the startup wizard usually shown when Artemis first starts. + + + + + + + + + + + + + + + + + + + Debugger + + Use the debugger to see the raw image Artemis is rendering on the surface. + + + + + + + + + + + + + + + + + + + Application files + + Opens the directory where application files like plugins and settings are stored. + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs index 9318f27f8..21a54084c 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs @@ -125,7 +125,19 @@ namespace Artemis.UI.Screens.Settings.Tabs.General _settingsService.GetSetting("UI.AutoRun", false).Value = value; _settingsService.GetSetting("UI.AutoRun", false).Save(); NotifyOfPropertyChange(nameof(StartWithWindows)); - Task.Run(ApplyAutorun); + Task.Run(() => ApplyAutorun(false)); + } + } + + public int AutoRunDelay + { + get => _settingsService.GetSetting("UI.AutoRunDelay", 15).Value; + set + { + _settingsService.GetSetting("UI.AutoRunDelay", 15).Value = value; + _settingsService.GetSetting("UI.AutoRunDelay", 15).Save(); + NotifyOfPropertyChange(nameof(AutoRunDelay)); + Task.Run(() => ApplyAutorun(true)); } } @@ -293,11 +305,11 @@ namespace Artemis.UI.Screens.Settings.Tabs.General protected override void OnInitialActivate() { - Task.Run(ApplyAutorun); + Task.Run(() => ApplyAutorun(false)); base.OnInitialActivate(); } - private void ApplyAutorun() + private void ApplyAutorun(bool recreate) { if (!StartWithWindows) StartMinimized = false; @@ -307,23 +319,29 @@ namespace Artemis.UI.Screens.Settings.Tabs.General if (File.Exists(autoRunFile)) File.Delete(autoRunFile); + // TODO: Don't do anything if running a development build, only auto-run release builds + // Create or remove the task if necessary try { - Process schtasks = new() + bool taskCreated = false; + if (!recreate) { - StartInfo = + Process schtasks = new() { - WindowStyle = ProcessWindowStyle.Hidden, - UseShellExecute = true, - FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"), - Arguments = "/TN \"Artemis 2 autorun\"" - } - }; + StartInfo = + { + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = true, + FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"), + Arguments = "/TN \"Artemis 2 autorun\"" + } + }; - schtasks.Start(); - schtasks.WaitForExit(); - bool taskCreated = schtasks.ExitCode == 0; + schtasks.Start(); + schtasks.WaitForExit(); + taskCreated = schtasks.ExitCode == 0; + } if (StartWithWindows && !taskCreated) CreateAutoRunTask(); @@ -347,6 +365,9 @@ namespace Artemis.UI.Screens.Settings.Tabs.General task.Descendants().First(d => d.Name.LocalName == "RegistrationInfo").Descendants().First(d => d.Name.LocalName == "Author") .SetValue(System.Security.Principal.WindowsIdentity.GetCurrent().Name); + task.Descendants().First(d => d.Name.LocalName == "Triggers").Descendants().First(d => d.Name.LocalName == "LogonTrigger").Descendants().First(d => d.Name.LocalName == "Delay") + .SetValue(TimeSpan.FromSeconds(AutoRunDelay)); + task.Descendants().First(d => d.Name.LocalName == "Principals").Descendants().First(d => d.Name.LocalName == "Principal").Descendants().First(d => d.Name.LocalName == "UserId") .SetValue(System.Security.Principal.WindowsIdentity.GetCurrent().User.Value); @@ -369,7 +390,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.General UseShellExecute = true, Verb = "runas", FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"), - Arguments = $"/Create /XML \"{xmlPath}\" /tn \"Artemis 2 autorun\"" + Arguments = $"/Create /XML \"{xmlPath}\" /tn \"Artemis 2 autorun\" /F" } }; diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml index 9e88a2346..eb8f06d5f 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml @@ -7,6 +7,7 @@ xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:s="https://github.com/canton7/Stylet" xmlns:converters="clr-namespace:Artemis.UI.Converters" + xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance local:PluginFeatureViewModel}"> @@ -21,8 +22,9 @@ - @@ -40,7 +42,7 @@ - + Feature.GetType().Name.Humanize(); - public Exception LoadException => Feature.LoadException; public bool Enabling @@ -109,7 +97,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } catch (Exception e) { - _messageService.ShowMessage($"Failed to enable {Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder); + _messageService.ShowMessage($"Failed to enable {Feature.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder); } finally { @@ -123,20 +111,6 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } } - private PackIconKind GetIconKind() - { - return Feature switch - { - BaseDataModelExpansion => PackIconKind.TableAdd, - DeviceProvider => PackIconKind.Devices, - ProfileModule => PackIconKind.VectorRectangle, - Module => PackIconKind.GearBox, - LayerBrushProvider => PackIconKind.Brush, - LayerEffectProvider => PackIconKind.AutoAwesome, - _ => PackIconKind.Plugin - }; - } - #region Event handlers private void OnFeatureEnabling(object sender, PluginFeatureEventArgs e) diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index 6ae335bf4..c6c80e900 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -1,6 +1,7 @@ using System.Linq; using Artemis.Core; using Artemis.Core.Services; +using Artemis.UI.Controllers; using Artemis.UI.DefaultTypes.DataModel.Display; using Artemis.UI.DefaultTypes.DataModel.Input; using Artemis.UI.InputProviders; @@ -19,6 +20,7 @@ namespace Artemis.UI.Services private readonly IPluginManagementService _pluginManagementService; private readonly ISurfaceService _surfaceService; private readonly IInputService _inputService; + private readonly IWebServerService _webServerService; private bool _registeredBuiltInDataModelDisplays; private bool _registeredBuiltInDataModelInputs; private bool _registeredBuiltInPropertyEditors; @@ -28,7 +30,8 @@ namespace Artemis.UI.Services IProfileEditorService profileEditorService, IPluginManagementService pluginManagementService, ISurfaceService surfaceService, - IInputService inputService) + IInputService inputService, + IWebServerService webServerService) { _logger = logger; _dataModelUIService = dataModelUIService; @@ -36,6 +39,7 @@ namespace Artemis.UI.Services _pluginManagementService = pluginManagementService; _surfaceService = surfaceService; _inputService = inputService; + _webServerService = webServerService; LoadPluginModules(); pluginManagementService.PluginEnabling += PluginServiceOnPluginEnabling; @@ -91,6 +95,11 @@ namespace Artemis.UI.Services _inputService.AddInputProvider(new NativeWindowInputProvider(_logger, _inputService)); } + public void RegisterControllers() + { + _webServerService.AddController(); + } + private void PluginServiceOnPluginEnabling(object sender, PluginEventArgs e) { e.Plugin.Kernel.Load(new[] {new PluginUIModule(e.Plugin)}); @@ -109,5 +118,6 @@ namespace Artemis.UI.Services void RegisterBuiltInDataModelInputs(); void RegisterBuiltInPropertyEditors(); void RegisterInputProvider(); + void RegisterControllers(); } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/RemoteManagement/Interfaces/IRemoteManagementService.cs b/src/Artemis.UI/Services/RemoteManagement/Interfaces/IRemoteManagementService.cs deleted file mode 100644 index 25cafb0d3..000000000 --- a/src/Artemis.UI/Services/RemoteManagement/Interfaces/IRemoteManagementService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Artemis.UI.Services -{ - public interface IRemoteManagementService : IArtemisUIService - { - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Services/RemoteManagement/RemoteManagementService.cs b/src/Artemis.UI/Services/RemoteManagement/RemoteManagementService.cs deleted file mode 100644 index 956e8a474..000000000 --- a/src/Artemis.UI/Services/RemoteManagement/RemoteManagementService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Artemis.Core.Services; - -namespace Artemis.UI.Services -{ - public class RemoteManagementService : IRemoteManagementService - { - public RemoteManagementService(IWebServerService webServerService) - { - webServerService.AddController(); - } - } -} \ No newline at end of file