From e09389c6dbf237c3bd25b9f918f062b58db8e675 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 8 Feb 2025 22:48:51 +0100 Subject: [PATCH 01/10] Replace EmbedIO with GenHTTP --- src/Artemis.Core/Artemis.Core.csproj | 3 +- .../EndPoints/DataModelJsonPluginEndPoint.cs | 20 +- .../EventArgs/EndpointRequestEventArgs.cs | 8 +- .../WebServer/EndPoints/JsonPluginEndPoint.cs | 35 ++-- .../WebServer/EndPoints/PluginEndPoint.cs | 33 ++-- .../WebServer/EndPoints/RawPluginEndPoint.cs | 14 +- .../EndPoints/StringPluginEndPoint.cs | 33 ++-- .../WebServer/Interfaces/IWebServerService.cs | 28 +-- .../Services/WebServer/PluginsModule.cs | 78 ++++---- .../WebServer/WebApiControllerRegistration.cs | 24 +-- .../WebServer/WebModuleRegistration.cs | 57 ------ .../Services/WebServer/WebServerService.cs | 185 ++++++------------ .../Controllers/RemoteController.cs | 43 ++-- .../Services/RegistrationService.cs | 2 +- src/Directory.Packages.props | 3 +- 15 files changed, 236 insertions(+), 330 deletions(-) delete mode 100644 src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index 2c50920de..efb61529e 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -37,7 +37,8 @@ - + + diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs index 438a638fc..b66c27d1b 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Linq.Expressions; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using Artemis.Core.Modules; -using EmbedIO; +using GenHTTP.Api.Protocol; namespace Artemis.Core.Services; @@ -27,7 +26,7 @@ public class DataModelJsonPluginEndPoint : PluginEndPoint where T : DataModel _update = CreateUpdateAction(); ThrowOnFail = true; - Accepts = MimeType.Json; + Accepts = ContentType.ApplicationJson; } /// @@ -38,17 +37,16 @@ public class DataModelJsonPluginEndPoint : PluginEndPoint where T : DataModel public bool ThrowOnFail { get; set; } /// - protected override async Task ProcessRequest(IHttpContext context) + protected override async Task ProcessRequest(IRequest request) { - if (context.Request.HttpVerb != HttpVerbs.Post && context.Request.HttpVerb != HttpVerbs.Put) - throw HttpException.MethodNotAllowed("This end point only accepts POST and PUT calls"); + if (request.Method != RequestMethod.Post && request.Method != RequestMethod.Put) + return request.Respond().Status(ResponseStatus.MethodNotAllowed).Build(); + if (request.Content == null) + return request.Respond().Status(ResponseStatus.BadRequest).Build(); - context.Response.ContentType = MimeType.Json; - - using TextReader reader = context.OpenRequestText(); try { - T? dataModel = CoreJson.Deserialize(await reader.ReadToEndAsync()); + T? dataModel = await JsonSerializer.DeserializeAsync(request.Content, WebServerService.JsonOptions); if (dataModel != null) _update(dataModel, _module.DataModel); } @@ -57,6 +55,8 @@ public class DataModelJsonPluginEndPoint : PluginEndPoint where T : DataModel if (ThrowOnFail) throw; } + + return request.Respond().Status(ResponseStatus.NoContent).Build(); } private Action CreateUpdateAction() diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/EventArgs/EndpointRequestEventArgs.cs b/src/Artemis.Core/Services/WebServer/EndPoints/EventArgs/EndpointRequestEventArgs.cs index 9d0c482aa..d1a62b737 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/EventArgs/EndpointRequestEventArgs.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/EventArgs/EndpointRequestEventArgs.cs @@ -1,5 +1,5 @@ using System; -using EmbedIO; +using GenHTTP.Api.Protocol; namespace Artemis.Core.Services; @@ -8,13 +8,13 @@ namespace Artemis.Core.Services; /// public class EndpointRequestEventArgs : EventArgs { - internal EndpointRequestEventArgs(IHttpContext context) + internal EndpointRequestEventArgs(IRequest request) { - Context = context; + Request = request; } /// /// Gets the HTTP context of the request /// - public IHttpContext Context { get; } + public IRequest Request { get; } } \ 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 2fc5c6977..76cd74fc6 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs @@ -1,8 +1,9 @@ using System; -using System.IO; using System.Text.Json; using System.Threading.Tasks; -using EmbedIO; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.Conversion.Serializers.Json; namespace Artemis.Core.Services; @@ -20,15 +21,15 @@ public class JsonPluginEndPoint : PluginEndPoint { _requestHandler = requestHandler; ThrowOnFail = true; - Accepts = MimeType.Json; + Accepts = ContentType.ApplicationJson; } internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func responseRequestHandler) : base(pluginFeature, name, pluginsModule) { _responseRequestHandler = responseRequestHandler; ThrowOnFail = true; - Accepts = MimeType.Json; - Returns = MimeType.Json; + Accepts = ContentType.ApplicationJson; + Returns = ContentType.ApplicationJson; } /// @@ -41,25 +42,25 @@ public class JsonPluginEndPoint : PluginEndPoint #region Overrides of PluginEndPoint /// - protected override async Task ProcessRequest(IHttpContext context) + protected override async Task ProcessRequest(IRequest request) { - if (context.Request.HttpVerb != HttpVerbs.Post && context.Request.HttpVerb != HttpVerbs.Put) - throw HttpException.MethodNotAllowed("This end point only accepts POST and PUT calls"); + if (request.Method != RequestMethod.Post && request.Method != RequestMethod.Put) + return request.Respond().Status(ResponseStatus.MethodNotAllowed).Build(); - context.Response.ContentType = MimeType.Json; + if (request.Content == null) + return request.Respond().Status(ResponseStatus.BadRequest).Build(); - using TextReader reader = context.OpenRequestText(); object? response = null; try { - T? deserialized = JsonSerializer.Deserialize(await reader.ReadToEndAsync()); + T? deserialized = await JsonSerializer.DeserializeAsync(request.Content, WebServerService.JsonOptions); if (deserialized == null) throw new JsonException("Deserialization returned null"); if (_requestHandler != null) { _requestHandler(deserialized); - return; + return request.Respond().Status(ResponseStatus.NoContent).Build(); } if (_responseRequestHandler != null) @@ -73,8 +74,14 @@ public class JsonPluginEndPoint : PluginEndPoint throw; } - await using TextWriter writer = context.OpenResponseText(); - await writer.WriteAsync(JsonSerializer.Serialize(response)); + // TODO: Cache options + if (response == null) + return request.Respond().Status(ResponseStatus.NoContent).Build(); + return request.Respond() + .Status(ResponseStatus.Ok) + .Content(new JsonContent(response, WebServerService.JsonOptions)) + .Type(ContentType.ApplicationJson) + .Build(); } #endregion diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs index fed0e033a..3048f53c7 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs @@ -1,7 +1,11 @@ using System; +using System.Net.Http; using System.Text.Json.Serialization; using System.Threading.Tasks; -using EmbedIO; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.IO; +using StringContent = GenHTTP.Modules.IO.Strings.StringContent; namespace Artemis.Core.Services; @@ -45,12 +49,12 @@ public abstract class PluginEndPoint /// /// Gets the mime type of the input this end point accepts /// - public string? Accepts { get; protected set; } + public ContentType Accepts { get; protected set; } /// /// Gets the mime type of the output this end point returns /// - public string? Returns { get; protected set; } + public ContentType Returns { get; protected set; } /// /// Occurs whenever a request threw an unhandled exception @@ -70,8 +74,8 @@ public abstract class PluginEndPoint /// /// Called whenever the end point has to process a request /// - /// The HTTP context of the request - protected abstract Task ProcessRequest(IHttpContext context); + /// The HTTP context of the request + protected abstract Task ProcessRequest(IRequest request); /// /// Invokes the event @@ -85,31 +89,36 @@ public abstract class PluginEndPoint /// /// Invokes the event /// - protected virtual void OnProcessingRequest(IHttpContext context) + protected virtual void OnProcessingRequest(IRequest request) { - ProcessingRequest?.Invoke(this, new EndpointRequestEventArgs(context)); + ProcessingRequest?.Invoke(this, new EndpointRequestEventArgs(request)); } /// /// Invokes the event /// - protected virtual void OnProcessedRequest(IHttpContext context) + protected virtual void OnProcessedRequest(IRequest request) { - ProcessedRequest?.Invoke(this, new EndpointRequestEventArgs(context)); + ProcessedRequest?.Invoke(this, new EndpointRequestEventArgs(request)); } - internal async Task InternalProcessRequest(IHttpContext context) + internal async Task InternalProcessRequest(IRequest context) { try { OnProcessingRequest(context); - await ProcessRequest(context); + IResponse response = await ProcessRequest(context); OnProcessedRequest(context); + return response; } catch (Exception e) { OnRequestException(e); - throw; + return context.Respond() + .Status(ResponseStatus.InternalServerError) + .Content(new StringContent(e.ToString())) + .Type(ContentType.TextPlain) + .Build(); } } diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs index 01fc44d40..4c11ae899 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using EmbedIO; +using GenHTTP.Api.Protocol; namespace Artemis.Core.Services; @@ -14,7 +14,7 @@ namespace Artemis.Core.Services; public class RawPluginEndPoint : PluginEndPoint { /// - internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func requestHandler) : base(pluginFeature, name, pluginsModule) + internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func> requestHandler) : base(pluginFeature, name, pluginsModule) { RequestHandler = requestHandler; } @@ -22,12 +22,12 @@ public class RawPluginEndPoint : PluginEndPoint /// /// Gets or sets the handler used to handle incoming requests to this endpoint /// - public Func RequestHandler { get; } + public Func> RequestHandler { get; } /// /// Sets the mime type this plugin end point accepts /// - public void SetAcceptType(string type) + public void SetAcceptType(ContentType type) { Accepts = type; } @@ -35,7 +35,7 @@ public class RawPluginEndPoint : PluginEndPoint /// /// Sets the mime type this plugin end point returns /// - public void SetReturnType(string type) + public void SetReturnType(ContentType type) { Returns = type; } @@ -43,9 +43,9 @@ public class RawPluginEndPoint : PluginEndPoint #region Overrides of PluginEndPoint /// - protected override async Task ProcessRequest(IHttpContext context) + protected override async Task ProcessRequest(IRequest request) { - await RequestHandler(context); + return await RequestHandler(request); } #endregion diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs index d4852621e..12e0054c7 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs @@ -1,7 +1,9 @@ using System; using System.IO; using System.Threading.Tasks; -using EmbedIO; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.IO.Strings; namespace Artemis.Core.Services; @@ -17,32 +19,34 @@ public class StringPluginEndPoint : PluginEndPoint internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action requestHandler) : base(pluginFeature, name, pluginsModule) { _requestHandler = requestHandler; - Accepts = MimeType.PlainText; + Accepts = ContentType.TextPlain; } internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func requestHandler) : base(pluginFeature, name, pluginsModule) { _responseRequestHandler = requestHandler; - Accepts = MimeType.PlainText; - Returns = MimeType.PlainText; + Accepts = ContentType.TextPlain; + Returns = ContentType.TextPlain; } #region Overrides of PluginEndPoint /// - protected override async Task ProcessRequest(IHttpContext context) + protected override async Task ProcessRequest(IRequest request) { - if (context.Request.HttpVerb != HttpVerbs.Post && context.Request.HttpVerb != HttpVerbs.Put) - throw HttpException.MethodNotAllowed("This end point only accepts POST and PUT calls"); + if (request.Method != RequestMethod.Post && request.Method != RequestMethod.Put) + return request.Respond().Status(ResponseStatus.MethodNotAllowed).Build(); - context.Response.ContentType = MimeType.PlainText; + if (request.Content == null) + return request.Respond().Status(ResponseStatus.BadRequest).Build(); - using TextReader reader = context.OpenRequestText(); + // Read the request as a string + using StreamReader reader = new(request.Content); string? response; if (_requestHandler != null) { _requestHandler(await reader.ReadToEndAsync()); - return; + return request.Respond().Status(ResponseStatus.Ok).Build(); } if (_responseRequestHandler != null) @@ -50,8 +54,13 @@ public class StringPluginEndPoint : PluginEndPoint else throw new ArtemisCoreException("String plugin end point has no request handler"); - await using TextWriter writer = context.OpenResponseText(); - await writer.WriteAsync(response); + if (response == null) + return request.Respond().Status(ResponseStatus.NoContent).Build(); + return request.Respond() + .Status(ResponseStatus.Ok) + .Content(new StringContent(response)) + .Type(ContentType.TextPlain) + .Build(); } #endregion diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs index e65d626fe..b2ae3e0fa 100644 --- a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -1,8 +1,8 @@ using System; using System.Threading.Tasks; using Artemis.Core.Modules; -using EmbedIO; -using EmbedIO.WebApi; +using GenHTTP.Api.Infrastructure; +using GenHTTP.Api.Protocol; namespace Artemis.Core.Services; @@ -14,7 +14,7 @@ public interface IWebServerService : IArtemisService /// /// Gets the current instance of the web server, replaced when occurs. /// - WebServer? Server { get; } + IServer? Server { get; } /// /// Gets the plugins module containing all plugin end points @@ -84,7 +84,7 @@ public interface IWebServerService : IArtemisService /// The name of the end point, must be unique /// /// The resulting end point - RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Func requestHandler); + RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Func> requestHandler); /// /// Removes an existing endpoint @@ -96,30 +96,14 @@ public interface IWebServerService : IArtemisService /// Adds a new Web API controller and restarts the web server /// /// The type of Web API controller to remove - WebApiControllerRegistration AddController(PluginFeature feature) where T : WebApiController; + WebApiControllerRegistration AddController(PluginFeature feature, string path) where T : class; /// /// Removes an existing Web API controller and restarts the web server /// /// The registration of the controller to remove. void RemoveController(WebApiControllerRegistration registration); - - /// - /// Adds a new EmbedIO module and restarts the web server - /// - WebModuleRegistration AddModule(PluginFeature feature, Func create); - - /// - /// Removes a EmbedIO module and restarts the web server - /// - void RemoveModule(WebModuleRegistration create); - - /// - /// Adds a new EmbedIO module and restarts the web server - /// - /// The type of module to add - WebModuleRegistration AddModule(PluginFeature feature) where T : IWebModule; - + /// /// Occurs when the web server has been created and is about to start. This is the ideal place to add your own modules. /// diff --git a/src/Artemis.Core/Services/WebServer/PluginsModule.cs b/src/Artemis.Core/Services/WebServer/PluginsModule.cs index c9d423760..0f1703fef 100644 --- a/src/Artemis.Core/Services/WebServer/PluginsModule.cs +++ b/src/Artemis.Core/Services/WebServer/PluginsModule.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; -using EmbedIO; +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.Conversion.Serializers.Json; +using GenHTTP.Modules.IO.Strings; namespace Artemis.Core.Services; @@ -10,15 +15,21 @@ namespace Artemis.Core.Services; /// Represents an EmbedIO web module used to process web requests and forward them to the right /// . /// -public class PluginsModule : WebModuleBase +public class PluginsModule : IHandler { private readonly Dictionary> _pluginEndPoints; - internal PluginsModule(string baseRoute) : base(baseRoute) + internal PluginsModule(string baseRoute) { + BaseRoute = baseRoute; _pluginEndPoints = new Dictionary>(comparer: StringComparer.InvariantCultureIgnoreCase); } + /// + /// Gets the base route of the module + /// + public string BaseRoute { get; } + internal void AddPluginEndPoint(PluginEndPoint registration) { string id = registration.PluginFeature.Plugin.Guid.ToString(); @@ -42,44 +53,39 @@ public class PluginsModule : WebModuleBase return; registrations.Remove(registration.Name); } - - #region Overrides of WebModuleBase - + /// - protected override async Task OnRequestAsync(IHttpContext context) + public ValueTask PrepareAsync() { - if (context.Route.SubPath == null) - return; - - // Split the sub path - string[] pathParts = context.Route.SubPath.Substring(1).Split('/'); - // Expect a plugin ID and an endpoint - if (pathParts.Length != 2) - return; - - // 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]}."); - - // If Accept-Charset contains a wildcard, remove the header so we default to UTF8 - // This is a workaround for an EmbedIO ehh issue - string? acceptCharset = context.Request.Headers["Accept-Charset"]; - if (acceptCharset != null && acceptCharset.Contains("*")) - context.Request.Headers.Remove("Accept-Charset"); - - // It is up to the registration how the request is eventually handled, it might even set a response here - await endPoint.InternalProcessRequest(context); - - // No need to return ourselves, assume the request is fully handled by the end point - context.SetHandled(); + return ValueTask.CompletedTask; } /// - public override bool IsFinalHandler => false; + public async ValueTask HandleAsync(IRequest request) + { + // Expect a plugin ID and an endpoint + if (request.Target.Path.Parts.Count != 2) + return null; + + // Find a matching plugin, if none found let another handler have a go :) + if (!_pluginEndPoints.TryGetValue(request.Target.Path.Parts[0].Value, out Dictionary? endPoints)) + return null; + + // Find a matching endpoint + if (!endPoints.TryGetValue(request.Target.Path.Parts[1].Value, out PluginEndPoint? endPoint)) + { + return request.Respond() + .Status(ResponseStatus.NotFound) + .Content(new StringContent($"Found no endpoint called {request.Target.Path.Parts[1].Value} for plugin with ID {request.Target.Path.Parts[0].Value}.")) + .Type(ContentType.TextPlain) + .Build(); + } + + // It is up to the registration how the request is eventually handled + return await endPoint.InternalProcessRequest(request); + } + + #region Overrides of WebModuleBase internal string? ServerUrl { get; set; } diff --git a/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs b/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs index 4a2c7bb50..0abd6bb4d 100644 --- a/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs +++ b/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs @@ -1,5 +1,4 @@ using System; -using EmbedIO.WebApi; namespace Artemis.Core.Services; @@ -7,15 +6,12 @@ namespace Artemis.Core.Services; /// Represents a web API controller registration. /// /// The type of the web API controller. -public class WebApiControllerRegistration : WebApiControllerRegistration where T : WebApiController +public class WebApiControllerRegistration : WebApiControllerRegistration where T : class { - internal WebApiControllerRegistration(IWebServerService webServerService, PluginFeature feature) : base(webServerService, feature, typeof(T)) + internal WebApiControllerRegistration(IWebServerService webServerService, PluginFeature feature, string path) : base(webServerService, feature, typeof(T), path) { Factory = () => feature.Plugin.Resolve(); } - - internal Func Factory { get; set; } - internal override object UntypedFactory => Factory; } /// @@ -28,12 +24,13 @@ public abstract class WebApiControllerRegistration /// /// Creates a new instance of the class. /// - protected internal WebApiControllerRegistration(IWebServerService webServerService, PluginFeature feature, Type controllerType) + protected internal WebApiControllerRegistration(IWebServerService webServerService, PluginFeature feature, Type controllerType, string path) { _webServerService = webServerService; Feature = feature; ControllerType = controllerType; - + Path = path; + Feature.Disabled += FeatureOnDisabled; } @@ -43,15 +40,20 @@ public abstract class WebApiControllerRegistration Feature.Disabled -= FeatureOnDisabled; } - internal abstract object UntypedFactory { get; } - /// /// Gets the type of the web API controller. /// public Type ControllerType { get; } - + /// /// Gets the plugin feature that provided the web API controller. /// public PluginFeature Feature { get; } + + /// + /// Gets the path at which the controller is available. + /// + public string Path { get; } + + internal Func Factory { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs b/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs deleted file mode 100644 index dca340b35..000000000 --- a/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using EmbedIO; - -namespace Artemis.Core.Services; - -/// -/// Represents a registration for a web module. -/// -public class WebModuleRegistration -{ - private readonly IWebServerService _webServerService; - - internal WebModuleRegistration(IWebServerService webServerService, PluginFeature feature, Type webModuleType) - { - _webServerService = webServerService; - Feature = feature ?? throw new ArgumentNullException(nameof(feature)); - WebModuleType = webModuleType ?? throw new ArgumentNullException(nameof(webModuleType)); - - Feature.Disabled += FeatureOnDisabled; - } - - internal WebModuleRegistration(IWebServerService webServerService, PluginFeature feature, Func create) - { - _webServerService = webServerService; - Feature = feature ?? throw new ArgumentNullException(nameof(feature)); - Create = create ?? throw new ArgumentNullException(nameof(create)); - - Feature.Disabled += FeatureOnDisabled; - } - - /// - /// The plugin feature that provided the web module. - /// - public PluginFeature Feature { get; } - - /// - /// The type of the web module. - /// - public Type? WebModuleType { get; } - - internal Func? Create { get; } - - internal IWebModule CreateInstance() - { - if (Create != null) - return Create(); - if (WebModuleType != null) - return (IWebModule) Feature.Plugin.Resolve(WebModuleType); - throw new ArtemisCoreException("WebModuleRegistration doesn't have a create function nor a web module type :("); - } - - private void FeatureOnDisabled(object? sender, EventArgs e) - { - _webServerService.RemoveModule(this); - Feature.Disabled -= FeatureOnDisabled; - } -} \ 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 e0ab36288..ffe72f464 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -2,14 +2,21 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Artemis.Core.Modules; -using EmbedIO; -using EmbedIO.WebApi; +using GenHTTP.Api.Infrastructure; +using GenHTTP.Api.Protocol; +using GenHTTP.Engine.Internal; +using GenHTTP.Modules.Controllers; +using GenHTTP.Modules.ErrorHandling; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Layouting.Provider; +using GenHTTP.Modules.Security; using Serilog; namespace Artemis.Core.Services; @@ -19,19 +26,16 @@ internal class WebServerService : IWebServerService, IDisposable private readonly List _controllers; private readonly ILogger _logger; private readonly ICoreService _coreService; - private readonly List _modules; private readonly PluginSetting _webServerEnabledSetting; private readonly PluginSetting _webServerPortSetting; - private readonly object _webserverLock = new(); - private readonly JsonSerializerOptions _jsonOptions = new(CoreJson.GetJsonSerializerOptions()) {ReferenceHandler = ReferenceHandler.IgnoreCycles, WriteIndented = true}; - private CancellationTokenSource? _cts; + private readonly SemaphoreSlim _webserverSemaphore = new(1, 1); + internal static readonly JsonSerializerOptions JsonOptions = new(CoreJson.GetJsonSerializerOptions()) {ReferenceHandler = ReferenceHandler.IgnoreCycles, WriteIndented = true}; public WebServerService(ILogger logger, ICoreService coreService, ISettingsService settingsService, IPluginManagementService pluginManagementService) { _logger = logger; _coreService = coreService; _controllers = new List(); - _modules = new List(); _webServerEnabledSetting = settingsService.GetSetting("WebServer.Enabled", true); _webServerPortSetting = settingsService.GetSetting("WebServer.Port", 9696); @@ -66,12 +70,12 @@ internal class WebServerService : IWebServerService, IDisposable private void WebServerEnabledSettingOnSettingChanged(object? sender, EventArgs e) { - StartWebServer(); + _ = StartWebServer(); } private void WebServerPortSettingOnSettingChanged(object? sender, EventArgs e) { - StartWebServer(); + _ = StartWebServer(); } private void PluginManagementServiceOnPluginFeatureDisabled(object? sender, PluginFeatureEventArgs e) @@ -82,77 +86,63 @@ internal class WebServerService : IWebServerService, IDisposable mustRestart = true; _controllers.RemoveAll(c => c.Feature == e.PluginFeature); } - - if (_modules.Any(m => m.Feature == e.PluginFeature)) - { - mustRestart = true; - _modules.RemoveAll(m => m.Feature == e.PluginFeature); - } - + if (mustRestart) - StartWebServer(); + _ = StartWebServer(); } /// public void Dispose() { - Server?.Dispose(); + Server?.DisposeAsync(); _webServerPortSetting.SettingChanged -= WebServerPortSettingOnSettingChanged; } - public WebServer? Server { get; private set; } + public IServer? Server { get; private set; } public PluginsModule PluginsModule { get; } public event EventHandler? WebServerStarting; #region Web server managament - private WebServer CreateWebServer() + private async Task CreateWebServer() { if (Server != null) { - if (_cts != null) - { - _cts.Cancel(); - _cts = null; - } - - Server.Dispose(); + await Server.DisposeAsync(); OnWebServerStopped(); Server = null; } - WebApiModule apiModule = new("/", SystemTextJsonSerializer); PluginsModule.ServerUrl = $"http://localhost:{_webServerPortSetting.Value}/"; - WebServer server = new WebServer(o => o.WithUrlPrefix($"http://*:{_webServerPortSetting.Value}/").WithMode(HttpListenerMode.EmbedIO)) - .WithLocalSessionManager() - .WithModule(PluginsModule); - - // Add registered modules - foreach (WebModuleRegistration? webModule in _modules) - server = server.WithModule(webModule.CreateInstance()); - - server = server - .WithModule(apiModule) - .HandleHttpException((context, exception) => HandleHttpExceptionJson(context, exception)) - .HandleUnhandledException(JsonExceptionHandlerCallback); + LayoutBuilder serverLayout = Layout.Create() + .Add(PluginsModule) + .Add(ErrorHandler.Structured()) + .Add(CorsPolicy.Permissive()); + // 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); + { + serverLayout = serverLayout.Add(registration.Path, Controller.From(registration.Factory())); + } + IServer server = Host.Create() + .Handler(serverLayout.Build()) + .Bind(IPAddress.Loopback, (ushort) _webServerPortSetting.Value) + .Development() + .Build(); + // Store the URL in a webserver.txt file so that remote applications can find it - File.WriteAllText(Path.Combine(Constants.DataFolder, "webserver.txt"), PluginsModule.ServerUrl); + await File.WriteAllTextAsync(Path.Combine(Constants.DataFolder, "webserver.txt"), PluginsModule.ServerUrl); return server; } - private void StartWebServer() + private async Task StartWebServer() { - lock (_webserverLock) + await _webserverSemaphore.WaitAsync(); + try { // Don't create the webserver until after the core service is initialized, this avoids lots of useless re-creates during initialize if (!_coreService.IsInitialized) @@ -161,7 +151,7 @@ internal class WebServerService : IWebServerService, IDisposable if (!_webServerEnabledSetting.Value) return; - Server = CreateWebServer(); + Server = await CreateWebServer(); if (Constants.StartupArguments.Contains("--disable-webserver")) { @@ -170,17 +160,28 @@ internal class WebServerService : IWebServerService, IDisposable } OnWebServerStarting(); - _cts = new CancellationTokenSource(); - Server.Start(_cts.Token); + try + { + await Server.StartAsync(); + } + catch (Exception e) + { + _logger.Warning(e, "Failed to start webserver"); + throw; + } OnWebServerStarted(); } + finally + { + _webserverSemaphore.Release(); + } } - private void AutoStartWebServer() + private async Task AutoStartWebServer() { try { - StartWebServer(); + await StartWebServer(); } catch (Exception exception) { @@ -232,7 +233,7 @@ internal class WebServerService : IWebServerService, IDisposable return endPoint; } - public RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Func requestHandler) + 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)); @@ -256,18 +257,18 @@ internal class WebServerService : IWebServerService, IDisposable { PluginsModule.RemovePluginEndPoint(endPoint); } - + #endregion #region Controller management - public WebApiControllerRegistration AddController(PluginFeature feature) where T : WebApiController + public WebApiControllerRegistration AddController(PluginFeature feature, string path) where T : class { if (feature == null) throw new ArgumentNullException(nameof(feature)); - WebApiControllerRegistration registration = new(this, feature); + WebApiControllerRegistration registration = new(this, feature, path); _controllers.Add(registration); - StartWebServer(); + _ = StartWebServer(); return registration; } @@ -275,75 +276,7 @@ internal class WebServerService : IWebServerService, IDisposable public void RemoveController(WebApiControllerRegistration registration) { _controllers.Remove(registration); - StartWebServer(); - } - - #endregion - - #region Module management - - public WebModuleRegistration AddModule(PluginFeature feature, Func create) - { - if (feature == null) throw new ArgumentNullException(nameof(feature)); - - WebModuleRegistration registration = new(this, feature, create); - _modules.Add(registration); - StartWebServer(); - - return registration; - } - - public WebModuleRegistration AddModule(PluginFeature feature) where T : IWebModule - { - if (feature == null) throw new ArgumentNullException(nameof(feature)); - - WebModuleRegistration registration = new(this, feature, typeof(T)); - _modules.Add(registration); - StartWebServer(); - - return registration; - } - - public void RemoveModule(WebModuleRegistration registration) - { - _modules.Remove(registration); - 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 = CoreJson.Serialize(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 SystemTextJsonSerializer(IHttpContext context, object? data) - { - context.Response.ContentType = MimeType.Json; - await using TextWriter writer = context.OpenResponseText(); - await writer.WriteAsync(JsonSerializer.Serialize(data, _jsonOptions)); - } - - private async Task HandleHttpExceptionJson(IHttpContext context, IHttpException httpException) - { - await context.SendStringAsync(JsonSerializer.Serialize(httpException, _jsonOptions), MimeType.Json, Encoding.UTF8); + _ = StartWebServer(); } #endregion diff --git a/src/Artemis.UI/Controllers/RemoteController.cs b/src/Artemis.UI/Controllers/RemoteController.cs index 0e6523736..377767eed 100644 --- a/src/Artemis.UI/Controllers/RemoteController.cs +++ b/src/Artemis.UI/Controllers/RemoteController.cs @@ -1,17 +1,18 @@ using System; +using System.Collections.Generic; using System.IO; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services.MainWindow; using Avalonia.Threading; -using EmbedIO; -using EmbedIO.Routing; -using EmbedIO.WebApi; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Controllers; +using GenHTTP.Modules.Reflection; namespace Artemis.UI.Controllers; -public class RemoteController : WebApiController +public class RemoteController { private readonly ICoreService _coreService; private readonly IMainWindowService _mainWindowService; @@ -24,17 +25,27 @@ public class RemoteController : WebApiController _router = router; } - [Route(HttpVerbs.Any, "/status")] - public void GetStatus() + public void Index() { - HttpContext.Response.StatusCode = 200; + // HTTP 204 No Content } - [Route(HttpVerbs.Post, "/remote/bring-to-foreground")] - public void PostBringToForeground() + [ControllerAction(RequestMethod.Get)] + public void Status() { - using StreamReader reader = new(Request.InputStream); - string route = reader.ReadToEnd(); + // HTTP 204 No Content + } + + [ControllerAction(RequestMethod.Post)] + public void BringToForeground(IRequest request) + { + // Get the route from the request content stream + string? route = null; + if (request.Content != null) + { + using StreamReader reader = new(request.Content); + route = reader.ReadToEnd(); + } Dispatcher.UIThread.InvokeAsync(async () => { @@ -44,14 +55,14 @@ public class RemoteController : WebApiController }); } - [Route(HttpVerbs.Post, "/remote/restart")] - public void PostRestart([FormField] string[] args) + [ControllerAction(RequestMethod.Post)] + public void Restart(List args) { - Utilities.Restart(_coreService.IsElevated, TimeSpan.FromMilliseconds(500), args); + Utilities.Restart(_coreService.IsElevated, TimeSpan.FromMilliseconds(500), args.ToArray()); } - [Route(HttpVerbs.Post, "/remote/shutdown")] - public void PostShutdown() + [ControllerAction(RequestMethod.Post)] + public void Shutdown() { Utilities.Shutdown(); } diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index 0a4447cda..766d9fa35 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -94,6 +94,6 @@ public class RegistrationService : IRegistrationService public void RegisterControllers() { - _webServerService.AddController(Constants.CorePlugin.Features.First().Instance!); + _webServerService.AddController(Constants.CorePlugin.Features.First().Instance!, "remote"); } } \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 95bbeb892..035e5ef05 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -14,6 +14,8 @@ + + @@ -21,7 +23,6 @@ - From eb4a6ceafb60b3f8192c1c92f4f48a171696c0ef Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 13 Feb 2025 21:35:35 +0100 Subject: [PATCH 02/10] Finish implementing GenHTTP --- src/Artemis.Core/Artemis.Core.csproj | 2 +- .../EndPoints/DataModelJsonPluginEndPoint.cs | 84 ------------------- .../WebServer/EndPoints/JsonPluginEndPoint.cs | 10 +-- .../WebServer/EndPoints/PluginEndPoint.cs | 25 ++++-- .../WebServer/EndPoints/RawPluginEndPoint.cs | 6 +- .../EndPoints/StringPluginEndPoint.cs | 10 +-- .../WebServer/Interfaces/IWebServerService.cs | 14 +--- .../{PluginsModule.cs => PluginsHandler.cs} | 28 +++++-- .../Services/WebServer/StatusHandler.cs | 27 ++++++ .../Services/WebServer/WebServerService.cs | 78 ++++++++--------- .../Controllers/RemoteController.cs | 20 +---- src/Directory.Packages.props | 2 +- 12 files changed, 122 insertions(+), 184 deletions(-) delete mode 100644 src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs rename src/Artemis.Core/Services/WebServer/{PluginsModule.cs => PluginsHandler.cs} (76%) create mode 100644 src/Artemis.Core/Services/WebServer/StatusHandler.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index efb61529e..ffea2cad6 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -38,7 +38,7 @@ - + diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs deleted file mode 100644 index b66c27d1b..000000000 --- a/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Artemis.Core.Modules; -using GenHTTP.Api.Protocol; - -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 DataModelJsonPluginEndPoint : PluginEndPoint where T : DataModel, new() -{ - private readonly Module _module; - private readonly Action _update; - - internal DataModelJsonPluginEndPoint(Module module, string name, PluginsModule pluginsModule) : base(module, name, pluginsModule) - { - _module = module ?? throw new ArgumentNullException(nameof(module)); - _update = CreateUpdateAction(); - - ThrowOnFail = true; - Accepts = ContentType.ApplicationJson; - } - - /// - /// 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; } - - /// - protected override async Task ProcessRequest(IRequest request) - { - if (request.Method != RequestMethod.Post && request.Method != RequestMethod.Put) - return request.Respond().Status(ResponseStatus.MethodNotAllowed).Build(); - if (request.Content == null) - return request.Respond().Status(ResponseStatus.BadRequest).Build(); - - try - { - T? dataModel = await JsonSerializer.DeserializeAsync(request.Content, WebServerService.JsonOptions); - if (dataModel != null) - _update(dataModel, _module.DataModel); - } - catch (JsonException) - { - if (ThrowOnFail) - throw; - } - - return request.Respond().Status(ResponseStatus.NoContent).Build(); - } - - private Action CreateUpdateAction() - { - ParameterExpression sourceParameter = Expression.Parameter(typeof(T), "source"); - ParameterExpression targetParameter = Expression.Parameter(typeof(T), "target"); - - IEnumerable assignments = typeof(T) - .GetProperties() - .Where(prop => prop.CanWrite && prop.GetSetMethod() != null && - prop.GetSetMethod()!.IsPublic && - !prop.IsDefined(typeof(JsonIgnoreAttribute), false) && - !prop.PropertyType.IsAssignableTo(typeof(IDataModelEvent))) - .Select(prop => - { - MemberExpression sourceProperty = Expression.Property(sourceParameter, prop); - MemberExpression targetProperty = Expression.Property(targetParameter, prop); - return Expression.Assign(targetProperty, sourceProperty); - }); - - BlockExpression body = Expression.Block(assignments); - - return Expression.Lambda>(body, sourceParameter, targetParameter).Compile(); - } -} \ 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 76cd74fc6..eee990829 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs @@ -17,19 +17,19 @@ 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) + internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsHandler pluginsHandler, Action requestHandler) : base(pluginFeature, name, pluginsHandler) { _requestHandler = requestHandler; ThrowOnFail = true; - Accepts = ContentType.ApplicationJson; + Accepts = FlexibleContentType.Get(ContentType.ApplicationJson); } - internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func responseRequestHandler) : base(pluginFeature, name, pluginsModule) + internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsHandler pluginsHandler, Func responseRequestHandler) : base(pluginFeature, name, pluginsHandler) { _responseRequestHandler = responseRequestHandler; ThrowOnFail = true; - Accepts = ContentType.ApplicationJson; - Returns = ContentType.ApplicationJson; + Accepts = FlexibleContentType.Get(ContentType.ApplicationJson); + Returns = FlexibleContentType.Get(ContentType.ApplicationJson); } /// diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs index 3048f53c7..ccea68640 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs @@ -10,15 +10,15 @@ using StringContent = GenHTTP.Modules.IO.Strings.StringContent; namespace Artemis.Core.Services; /// -/// Represents a base type for plugin end points to be targeted by the +/// Represents a base type for plugin end points to be targeted by the /// public abstract class PluginEndPoint { - private readonly PluginsModule _pluginsModule; + private readonly PluginsHandler _pluginsHandler; - internal PluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule) + internal PluginEndPoint(PluginFeature pluginFeature, string name, PluginsHandler pluginsHandler) { - _pluginsModule = pluginsModule; + _pluginsHandler = pluginsHandler; PluginFeature = pluginFeature; Name = name; @@ -33,7 +33,7 @@ public abstract class PluginEndPoint /// /// Gets the full URL of the end point /// - public string Url => $"{_pluginsModule.ServerUrl?.TrimEnd('/')}{_pluginsModule.BaseRoute}{PluginFeature.Plugin.Guid}/{Name}"; + public string Url => $"{_pluginsHandler.ServerUrl}{_pluginsHandler.BaseRoute}/{PluginFeature.Plugin.Guid}/{Name}"; /// /// Gets the plugin the end point is associated with @@ -46,15 +46,15 @@ public abstract class PluginEndPoint /// public PluginInfo PluginInfo => PluginFeature.Plugin.Info; - /// + /// /// Gets the mime type of the input this end point accepts /// - public ContentType Accepts { get; protected set; } + public FlexibleContentType Accepts { get; protected set; } /// /// Gets the mime type of the output this end point returns /// - public ContentType Returns { get; protected set; } + public FlexibleContentType Returns { get; protected set; } /// /// Occurs whenever a request threw an unhandled exception @@ -107,6 +107,13 @@ public abstract class PluginEndPoint try { OnProcessingRequest(context); + + if (!Equals(context.ContentType, Accepts)) + { + OnRequestException(new Exception("Unsupported media type")); + return context.Respond().Status(ResponseStatus.UnsupportedMediaType).Build(); + } + IResponse response = await ProcessRequest(context); OnProcessedRequest(context); return response; @@ -125,6 +132,6 @@ public abstract class PluginEndPoint private void OnDisabled(object? sender, EventArgs e) { PluginFeature.Disabled -= OnDisabled; - _pluginsModule.RemovePluginEndPoint(this); + _pluginsHandler.RemovePluginEndPoint(this); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs index 4c11ae899..91baffc17 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs @@ -14,7 +14,7 @@ namespace Artemis.Core.Services; public class RawPluginEndPoint : PluginEndPoint { /// - internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func> requestHandler) : base(pluginFeature, name, pluginsModule) + internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsHandler pluginsHandler, Func> requestHandler) : base(pluginFeature, name, pluginsHandler) { RequestHandler = requestHandler; } @@ -29,7 +29,7 @@ public class RawPluginEndPoint : PluginEndPoint /// public void SetAcceptType(ContentType type) { - Accepts = type; + Accepts = FlexibleContentType.Get(type); } /// @@ -37,7 +37,7 @@ public class RawPluginEndPoint : PluginEndPoint /// public void SetReturnType(ContentType type) { - Returns = type; + Returns = FlexibleContentType.Get(type); } #region Overrides of PluginEndPoint diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs index 12e0054c7..83230e8c1 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs @@ -16,17 +16,17 @@ 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) + internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsHandler pluginsHandler, Action requestHandler) : base(pluginFeature, name, pluginsHandler) { _requestHandler = requestHandler; - Accepts = ContentType.TextPlain; + Accepts = FlexibleContentType.Get(ContentType.TextPlain); } - internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func requestHandler) : base(pluginFeature, name, pluginsModule) + internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsHandler pluginsHandler, Func requestHandler) : base(pluginFeature, name, pluginsHandler) { _responseRequestHandler = requestHandler; - Accepts = ContentType.TextPlain; - Returns = ContentType.TextPlain; + Accepts = FlexibleContentType.Get(ContentType.TextPlain); + Returns = FlexibleContentType.Get(ContentType.TextPlain); } #region Overrides of PluginEndPoint diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs index b2ae3e0fa..01a41d1dd 100644 --- a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -19,7 +19,7 @@ public interface IWebServerService : IArtemisService /// /// Gets the plugins module containing all plugin end points /// - PluginsModule PluginsModule { get; } + PluginsHandler PluginsHandler { get; } /// /// Adds a new endpoint for the given plugin feature receiving an object of type @@ -44,16 +44,6 @@ public interface IWebServerService : IArtemisService /// The resulting end point JsonPluginEndPoint AddResponsiveJsonEndPoint(PluginFeature feature, string endPointName, Func requestHandler); - /// - /// Adds a new endpoint that directly maps received JSON to the data model of the provided . - /// - /// The data model type of the module - /// The module whose datamodel to apply the received JSON to - /// The name of the end point, must be unique - /// The resulting end point - [Obsolete("This way of updating is too unpredictable in combination with nested events, use AddJsonEndPoint to update manually instead")] - DataModelJsonPluginEndPoint AddDataModelJsonEndPoint(Module module, string endPointName) where T : DataModel, new(); - /// /// Adds a new endpoint for the given plugin feature receiving an a . /// @@ -95,7 +85,7 @@ public interface IWebServerService : IArtemisService /// /// Adds a new Web API controller and restarts the web server /// - /// The type of Web API controller to remove + /// The type of Web API controller to add WebApiControllerRegistration AddController(PluginFeature feature, string path) where T : class; /// diff --git a/src/Artemis.Core/Services/WebServer/PluginsModule.cs b/src/Artemis.Core/Services/WebServer/PluginsHandler.cs similarity index 76% rename from src/Artemis.Core/Services/WebServer/PluginsModule.cs rename to src/Artemis.Core/Services/WebServer/PluginsHandler.cs index 0f1703fef..7327948d6 100644 --- a/src/Artemis.Core/Services/WebServer/PluginsModule.cs +++ b/src/Artemis.Core/Services/WebServer/PluginsHandler.cs @@ -12,14 +12,14 @@ using GenHTTP.Modules.IO.Strings; namespace Artemis.Core.Services; /// -/// Represents an EmbedIO web module used to process web requests and forward them to the right +/// Represents an GenHTTP handler used to process web requests and forward them to the right /// . /// -public class PluginsModule : IHandler +public class PluginsHandler : IHandler { private readonly Dictionary> _pluginEndPoints; - internal PluginsModule(string baseRoute) + internal PluginsHandler(string baseRoute) { BaseRoute = baseRoute; _pluginEndPoints = new Dictionary>(comparer: StringComparer.InvariantCultureIgnoreCase); @@ -63,20 +63,30 @@ public class PluginsModule : IHandler /// public async ValueTask HandleAsync(IRequest request) { - // Expect a plugin ID and an endpoint - if (request.Target.Path.Parts.Count != 2) + // Used to be part of the RemoteController but moved here to avoid the /remote/ prefix enforced by GenHTTP + if (request.Target.Current?.Value != "plugins") + return null; + + request.Target.Advance(); + string? pluginId = request.Target.Current?.Value; + if (pluginId == null) return null; // Find a matching plugin, if none found let another handler have a go :) - if (!_pluginEndPoints.TryGetValue(request.Target.Path.Parts[0].Value, out Dictionary? endPoints)) + if (!_pluginEndPoints.TryGetValue(pluginId, out Dictionary? endPoints)) return null; - + + request.Target.Advance(); + string? endPointName = request.Target.Current?.Value; + if (endPointName == null) + return null; + // Find a matching endpoint - if (!endPoints.TryGetValue(request.Target.Path.Parts[1].Value, out PluginEndPoint? endPoint)) + if (!endPoints.TryGetValue(endPointName, out PluginEndPoint? endPoint)) { return request.Respond() .Status(ResponseStatus.NotFound) - .Content(new StringContent($"Found no endpoint called {request.Target.Path.Parts[1].Value} for plugin with ID {request.Target.Path.Parts[0].Value}.")) + .Content(new StringContent($"Found no endpoint called {endPointName} for plugin with ID {pluginId}.")) .Type(ContentType.TextPlain) .Build(); } diff --git a/src/Artemis.Core/Services/WebServer/StatusHandler.cs b/src/Artemis.Core/Services/WebServer/StatusHandler.cs new file mode 100644 index 000000000..fbd6d83da --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/StatusHandler.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +namespace Artemis.Core.Services; + +/// +/// Represents an GenHTTP handler used to process web requests and forward them to the right +/// . +/// +public class StatusHandler : IHandler +{ + /// + public ValueTask PrepareAsync() + { + return ValueTask.CompletedTask; + } + + /// + public ValueTask HandleAsync(IRequest request) + { + // Used to be part of the RemoteController but moved here to avoid the /remote/ prefix enforced by GenHTTP + return request.Target.Current?.Value == "status" + ? ValueTask.FromResult(request.Respond().Status(ResponseStatus.NoContent).Build()) + : ValueTask.FromResult(null); + } +} \ 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 ffe72f464..fcce9ef12 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -3,20 +3,21 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Artemis.Core.Modules; using GenHTTP.Api.Infrastructure; using GenHTTP.Api.Protocol; using GenHTTP.Engine.Internal; -using GenHTTP.Modules.Controllers; +using GenHTTP.Modules.Conversion; +using GenHTTP.Modules.Conversion.Serializers; using GenHTTP.Modules.ErrorHandling; using GenHTTP.Modules.Layouting; using GenHTTP.Modules.Layouting.Provider; +using GenHTTP.Modules.Practices; using GenHTTP.Modules.Security; +using GenHTTP.Modules.Webservices; using Serilog; namespace Artemis.Core.Services; @@ -29,7 +30,13 @@ internal class WebServerService : IWebServerService, IDisposable private readonly PluginSetting _webServerEnabledSetting; private readonly PluginSetting _webServerPortSetting; private readonly SemaphoreSlim _webserverSemaphore = new(1, 1); - internal static readonly JsonSerializerOptions JsonOptions = new(CoreJson.GetJsonSerializerOptions()) {ReferenceHandler = ReferenceHandler.IgnoreCycles, WriteIndented = true}; + + internal static readonly JsonSerializerOptions JsonOptions = new(CoreJson.GetJsonSerializerOptions()) + { + ReferenceHandler = ReferenceHandler.IgnoreCycles, + WriteIndented = true, + Converters = {new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)} + }; public WebServerService(ILogger logger, ICoreService coreService, ISettingsService settingsService, IPluginManagementService pluginManagementService) { @@ -43,7 +50,7 @@ internal class WebServerService : IWebServerService, IDisposable _webServerPortSetting.SettingChanged += WebServerPortSettingOnSettingChanged; pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureDisabled; - PluginsModule = new PluginsModule("/plugins"); + PluginsHandler = new PluginsHandler("plugins"); if (coreService.IsInitialized) AutoStartWebServer(); else @@ -86,7 +93,7 @@ internal class WebServerService : IWebServerService, IDisposable mustRestart = true; _controllers.RemoveAll(c => c.Feature == e.PluginFeature); } - + if (mustRestart) _ = StartWebServer(); } @@ -99,13 +106,13 @@ internal class WebServerService : IWebServerService, IDisposable } public IServer? Server { get; private set; } - public PluginsModule PluginsModule { get; } + public PluginsHandler PluginsHandler { get; } public event EventHandler? WebServerStarting; #region Web server managament - private async Task CreateWebServer() + private async Task CreateWebServer() { if (Server != null) { @@ -114,27 +121,29 @@ internal class WebServerService : IWebServerService, IDisposable Server = null; } - PluginsModule.ServerUrl = $"http://localhost:{_webServerPortSetting.Value}/"; - + PluginsHandler.ServerUrl = $"http://localhost:{_webServerPortSetting.Value}/"; + LayoutBuilder serverLayout = Layout.Create() - .Add(PluginsModule) + .Add(PluginsHandler) .Add(ErrorHandler.Structured()) .Add(CorsPolicy.Permissive()); - - // Add registered controllers to the API module + + // Add registered controllers to the API module as services. + // GenHTTP also has controllers but services are more flexible and match EmbedIO's approach more closely. + SerializationBuilder serialization = Serialization.Default(JsonOptions); foreach (WebApiControllerRegistration registration in _controllers) { - serverLayout = serverLayout.Add(registration.Path, Controller.From(registration.Factory())); + serverLayout = serverLayout.AddService(registration.Path, registration.Factory(), serializers: serialization); } IServer server = Host.Create() .Handler(serverLayout.Build()) .Bind(IPAddress.Loopback, (ushort) _webServerPortSetting.Value) - .Development() + .Defaults() .Build(); - + // Store the URL in a webserver.txt file so that remote applications can find it - await File.WriteAllTextAsync(Path.Combine(Constants.DataFolder, "webserver.txt"), PluginsModule.ServerUrl); + await File.WriteAllTextAsync(Path.Combine(Constants.DataFolder, "webserver.txt"), PluginsHandler.ServerUrl); return server; } @@ -169,6 +178,7 @@ internal class WebServerService : IWebServerService, IDisposable _logger.Warning(e, "Failed to start webserver"); throw; } + OnWebServerStarted(); } finally @@ -198,8 +208,8 @@ internal class WebServerService : IWebServerService, IDisposable 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); + JsonPluginEndPoint endPoint = new(feature, endPointName, PluginsHandler, requestHandler); + PluginsHandler.AddPluginEndPoint(endPoint); return endPoint; } @@ -208,8 +218,8 @@ internal class WebServerService : IWebServerService, IDisposable 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); + JsonPluginEndPoint endPoint = new(feature, endPointName, PluginsHandler, requestHandler); + PluginsHandler.AddPluginEndPoint(endPoint); return endPoint; } @@ -218,8 +228,8 @@ internal class WebServerService : IWebServerService, IDisposable 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); + StringPluginEndPoint endPoint = new(feature, endPointName, PluginsHandler, requestHandler); + PluginsHandler.AddPluginEndPoint(endPoint); return endPoint; } @@ -228,8 +238,8 @@ internal class WebServerService : IWebServerService, IDisposable 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); + StringPluginEndPoint endPoint = new(feature, endPointName, PluginsHandler, requestHandler); + PluginsHandler.AddPluginEndPoint(endPoint); return endPoint; } @@ -238,26 +248,16 @@ internal class WebServerService : IWebServerService, IDisposable 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; - } - - [Obsolete("Use AddJsonEndPoint(PluginFeature feature, string endPointName, Action requestHandler) instead")] - public DataModelJsonPluginEndPoint AddDataModelJsonEndPoint(Module module, string endPointName) where T : DataModel, new() - { - if (module == null) throw new ArgumentNullException(nameof(module)); - if (endPointName == null) throw new ArgumentNullException(nameof(endPointName)); - DataModelJsonPluginEndPoint endPoint = new(module, endPointName, PluginsModule); - PluginsModule.AddPluginEndPoint(endPoint); + RawPluginEndPoint endPoint = new(feature, endPointName, PluginsHandler, requestHandler); + PluginsHandler.AddPluginEndPoint(endPoint); return endPoint; } public void RemovePluginEndPoint(PluginEndPoint endPoint) { - PluginsModule.RemovePluginEndPoint(endPoint); + PluginsHandler.RemovePluginEndPoint(endPoint); } - + #endregion #region Controller management diff --git a/src/Artemis.UI/Controllers/RemoteController.cs b/src/Artemis.UI/Controllers/RemoteController.cs index 377767eed..1aa86305f 100644 --- a/src/Artemis.UI/Controllers/RemoteController.cs +++ b/src/Artemis.UI/Controllers/RemoteController.cs @@ -7,8 +7,7 @@ using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services.MainWindow; using Avalonia.Threading; using GenHTTP.Api.Protocol; -using GenHTTP.Modules.Controllers; -using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Webservices; namespace Artemis.UI.Controllers; @@ -25,18 +24,7 @@ public class RemoteController _router = router; } - public void Index() - { - // HTTP 204 No Content - } - - [ControllerAction(RequestMethod.Get)] - public void Status() - { - // HTTP 204 No Content - } - - [ControllerAction(RequestMethod.Post)] + [ResourceMethod(RequestMethod.Post, "bring-to-foreground")] public void BringToForeground(IRequest request) { // Get the route from the request content stream @@ -55,13 +43,13 @@ public class RemoteController }); } - [ControllerAction(RequestMethod.Post)] + [ResourceMethod(RequestMethod.Post, "restart")] public void Restart(List args) { Utilities.Restart(_coreService.IsElevated, TimeSpan.FromMilliseconds(500), args.ToArray()); } - [ControllerAction(RequestMethod.Post)] + [ResourceMethod(RequestMethod.Post, "shutdown")] public void Shutdown() { Utilities.Shutdown(); diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 035e5ef05..28bfcc235 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -15,7 +15,7 @@ - + From 0a26319914520dafd1477fc67059ed90b4404924 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 14 Feb 2025 18:20:54 +0100 Subject: [PATCH 03/10] Remove unneccesary error handler --- src/Artemis.Core/Services/WebServer/WebServerService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index fcce9ef12..03ccb102f 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -125,7 +125,6 @@ internal class WebServerService : IWebServerService, IDisposable LayoutBuilder serverLayout = Layout.Create() .Add(PluginsHandler) - .Add(ErrorHandler.Structured()) .Add(CorsPolicy.Permissive()); // Add registered controllers to the API module as services. From 14c7940a21e8348414de9679c85fac2d541b2c61 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 14 Feb 2025 19:02:56 +0100 Subject: [PATCH 04/10] UI - Fix visual regressions that occured after updating Avalonia at some point --- .../PropertyInput/BrushPropertyInputView.axaml | 2 +- .../Screens/Device/Tabs/Layout/DeviceLayoutTabView.axaml | 2 +- .../Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml | 2 +- .../DisplayCondition/DisplayConditionScriptView.axaml | 8 +++++++- src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml | 2 +- src/Artemis.UI/Styles/Artemis.axaml | 8 ++++++++ 6 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml index c84206870..f9c883e17 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml @@ -10,7 +10,7 @@ x:DataType="propertyInput:BrushPropertyInputViewModel"> - + + + + avares://Artemis.UI/Assets/Fonts#Roboto Mono From 7176aba0d64acd3ffa33fd8b3810c90d4596993e Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 14 Feb 2025 22:15:03 +0100 Subject: [PATCH 05/10] Nodes - Inform nodes they are being loaded using the new IsLoading property --- src/Artemis.Core/VisualScripting/Interfaces/INode.cs | 5 +++++ src/Artemis.Core/VisualScripting/NodeData.cs | 1 + src/Artemis.Core/VisualScripting/NodeScript.cs | 3 +++ src/Artemis.Core/VisualScripting/Nodes/Node.cs | 3 +++ 4 files changed, 12 insertions(+) diff --git a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs index 06c382749..15d21588c 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs @@ -39,6 +39,11 @@ public interface INode : INotifyPropertyChanged, IBreakableModel, IPluginFeature /// Gets a boolean indicating whether the node is a default node that connot be removed /// bool IsDefaultNode { get; } + + /// + /// Gets a boolean indicating whether the node is currently loading, use this to disable pin type changes etc. + /// + bool IsLoading { get; set; } /// /// Gets or sets the X-position of the node diff --git a/src/Artemis.Core/VisualScripting/NodeData.cs b/src/Artemis.Core/VisualScripting/NodeData.cs index dff2a5a2d..133414e9b 100644 --- a/src/Artemis.Core/VisualScripting/NodeData.cs +++ b/src/Artemis.Core/VisualScripting/NodeData.cs @@ -46,6 +46,7 @@ public class NodeData if (entity != null) { + node.IsLoading = true; node.X = entity.X; node.Y = entity.Y; try diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index 8f281f6c3..edf08cdb0 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -206,6 +206,8 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript } LoadConnections(); + foreach (INode node in Nodes) + node.IsLoading = false; } internal void LoadFromEntity(NodeScriptEntity entity) @@ -216,6 +218,7 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript private void LoadExistingNode(INode node, NodeEntity nodeEntity) { + node.IsLoading = true; node.Id = nodeEntity.Id; node.X = nodeEntity.X; node.Y = nodeEntity.Y; diff --git a/src/Artemis.Core/VisualScripting/Nodes/Node.cs b/src/Artemis.Core/VisualScripting/Nodes/Node.cs index b3c4ad3ee..97254c03d 100644 --- a/src/Artemis.Core/VisualScripting/Nodes/Node.cs +++ b/src/Artemis.Core/VisualScripting/Nodes/Node.cs @@ -92,6 +92,9 @@ public abstract class Node : BreakableModel, INode /// public virtual bool IsDefaultNode => false; + /// + public bool IsLoading { get; set; } + private readonly List _pins = new(); /// From fcf00af13014057d48ec03192540427b09866df6 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 15 Feb 2025 16:52:26 +0100 Subject: [PATCH 06/10] Add missing status handler, add TODO --- src/Artemis.Core/Services/WebServer/WebServerService.cs | 1 + src/Artemis.UI/Controllers/RemoteController.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 03ccb102f..9dbdfd283 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -125,6 +125,7 @@ internal class WebServerService : IWebServerService, IDisposable LayoutBuilder serverLayout = Layout.Create() .Add(PluginsHandler) + .Add(new StatusHandler()) .Add(CorsPolicy.Permissive()); // Add registered controllers to the API module as services. diff --git a/src/Artemis.UI/Controllers/RemoteController.cs b/src/Artemis.UI/Controllers/RemoteController.cs index 1aa86305f..1bcbaf032 100644 --- a/src/Artemis.UI/Controllers/RemoteController.cs +++ b/src/Artemis.UI/Controllers/RemoteController.cs @@ -28,6 +28,7 @@ public class RemoteController public void BringToForeground(IRequest request) { // Get the route from the request content stream + // TODO: Use [FromBody] attribute instead once GenHTTP allows omitting null values string? route = null; if (request.Content != null) { From c82f86aed3dad0fdf3e8f22e0b1e77f5ecc8c6cd Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Sat, 15 Feb 2025 15:48:31 +0000 Subject: [PATCH 07/10] handle unplugging devices properly --- .../Providers/Input/LinuxInputDeviceReader.cs | 11 ++++++++--- .../Providers/Input/LinuxInputProvider.cs | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceReader.cs b/src/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceReader.cs index e06f852a2..9f78fd227 100644 --- a/src/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceReader.cs +++ b/src/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceReader.cs @@ -1,20 +1,24 @@ using System; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Serilog; namespace Artemis.UI.Linux.Providers.Input; internal class LinuxInputDeviceReader { + private readonly ILogger _logger; private readonly byte[] _buffer; private readonly CancellationTokenSource _cts; private readonly FileStream _stream; private readonly Task _task; - public LinuxInputDeviceReader(LinuxInputDevice inputDevice) + public LinuxInputDeviceReader(LinuxInputDevice inputDevice, ILogger logger) { + _logger = logger; InputDevice = inputDevice; _buffer = new byte[Marshal.SizeOf()]; @@ -50,9 +54,10 @@ internal class LinuxInputDeviceReader InputEvent?.Invoke(this, MemoryMarshal.Read(_buffer)); } - catch + catch(Exception e) { - // ignored + _logger.Error("Error reading device input from {fileName}. Did you unplug a device? Stopping reader. {e}", InputDevice.EventPath, e); + return; } } } diff --git a/src/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs b/src/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs index 5c4349231..48af97dbf 100644 --- a/src/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs +++ b/src/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs @@ -21,7 +21,7 @@ public class LinuxInputProvider : InputProvider foreach (LinuxInputDevice deviceDefinition in LinuxInputDeviceFinder.Find()) { - LinuxInputDeviceReader? reader = new(deviceDefinition); + LinuxInputDeviceReader? reader = new(deviceDefinition, logger); reader.InputEvent += OnInputEvent; _readers.Add(reader); } From 3367280576c64c9336e8d22ad80d0bba0a2f2520 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 16 Feb 2025 11:15:52 +0100 Subject: [PATCH 08/10] Startup wizard - Correctly remember wizard was compelted Settings - Fix double navigation occuring when opening settings Settings - Fix double navigation occuring when opening releases --- src/Artemis.UI/Screens/Settings/SettingsViewModel.cs | 12 +++++------- .../Screens/Settings/Tabs/ReleasesTabViewModel.cs | 10 +++------- .../Screens/StartupWizard/StartupWizardViewModel.cs | 9 ++++++++- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs index e83502417..4e288c11c 100644 --- a/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Artemis.UI.Routing; @@ -42,14 +43,11 @@ public partial class SettingsViewModel : RoutableHostScreen, IMa public ViewModelBase? TitleBarViewModel => null; /// - public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken) + public override Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken) { - // Display tab change on navigate - SelectedTab = SettingTabs.FirstOrDefault(t => t.Matches(args.Path)); - - // Always show a tab, if there is none forward to the first - if (SelectedTab == null) - await _router.Navigate(SettingTabs.First().Path); + // Display tab change on navigate, if there is none forward to the first + SelectedTab = SettingTabs.FirstOrDefault(t => t.Matches(args.Path)) ?? SettingTabs.FirstOrDefault(); + return Task.CompletedTask; } public void GoBack() diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs index 3373eba8c..10e9d5f59 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs @@ -62,7 +62,7 @@ public partial class ReleasesTabViewModel : RoutableHostScreen ReleaseViewModels { get; } public string Channel { get; } - + public async Task GetReleases(CancellationToken cancellationToken) { try @@ -97,7 +97,7 @@ public partial class ReleasesTabViewModel : RoutableHostScreen public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken) { @@ -109,10 +109,6 @@ public partial class ReleasesTabViewModel : RoutableHostScreen vm.Release.Id == releaseId); // Otherwise forward to the last release else - { - ReleaseViewModel? lastRelease = ReleaseViewModels.FirstOrDefault(r => r.IsCurrentVersion) ?? ReleaseViewModels.FirstOrDefault(); - if (lastRelease != null) - await _router.Navigate($"settings/releases/{lastRelease.Release.Id}"); - } + SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(r => r.IsCurrentVersion) ?? ReleaseViewModels.FirstOrDefault(); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs index e112dacff..75de7eb2e 100644 --- a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs @@ -1,5 +1,6 @@ using System; using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Screens.StartupWizard.Steps; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; @@ -11,11 +12,13 @@ namespace Artemis.UI.Screens.StartupWizard; public partial class StartupWizardViewModel : DialogViewModelBase { private readonly IContainer _container; + private readonly ISettingsService _settingsService; [Notify] private WizardStepViewModel _screen; - public StartupWizardViewModel(IContainer container, IWindowService windowService) + public StartupWizardViewModel(IContainer container, IWindowService windowService, ISettingsService settingsService) { _container = container; + _settingsService = settingsService; _screen = _container.Resolve(); _screen.Wizard = this; @@ -41,6 +44,10 @@ public partial class StartupWizardViewModel : DialogViewModelBase public void SkipOrFinishWizard() { + PluginSetting setting = _settingsService.GetSetting("UI.SetupWizardCompleted", false); + setting.Value = true; + setting.Save(); + Close(true); } } \ No newline at end of file From 37b8c2c3e9e52bb6110beef9540845fd0d262900 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 16 Feb 2025 12:05:26 +0100 Subject: [PATCH 09/10] Webserver - Add option to enable remote access --- .../WebServer/EndPoints/PluginEndPoint.cs | 2 +- .../Services/WebServer/PluginsHandler.cs | 4 +--- .../Services/WebServer/WebServerService.cs | 21 +++++++------------ .../Settings/Tabs/GeneralTabView.axaml | 16 ++++++++++++++ .../Settings/Tabs/GeneralTabViewModel.cs | 1 + 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs index ccea68640..f621ade10 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs @@ -33,7 +33,7 @@ public abstract class PluginEndPoint /// /// Gets the full URL of the end point /// - public string Url => $"{_pluginsHandler.ServerUrl}{_pluginsHandler.BaseRoute}/{PluginFeature.Plugin.Guid}/{Name}"; + public string Url => $"/{_pluginsHandler.BaseRoute}/{PluginFeature.Plugin.Guid}/{Name}"; /// /// Gets the plugin the end point is associated with diff --git a/src/Artemis.Core/Services/WebServer/PluginsHandler.cs b/src/Artemis.Core/Services/WebServer/PluginsHandler.cs index 7327948d6..f7db246b5 100644 --- a/src/Artemis.Core/Services/WebServer/PluginsHandler.cs +++ b/src/Artemis.Core/Services/WebServer/PluginsHandler.cs @@ -96,9 +96,7 @@ public class PluginsHandler : IHandler } #region Overrides of WebModuleBase - - internal string? ServerUrl { get; set; } - + /// /// Gets a read only collection containing all current plugin end points /// diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 9dbdfd283..8a132c8e6 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -28,6 +28,7 @@ internal class WebServerService : IWebServerService, IDisposable private readonly ILogger _logger; private readonly ICoreService _coreService; private readonly PluginSetting _webServerEnabledSetting; + private readonly PluginSetting _webServerRemoteAccessSetting; private readonly PluginSetting _webServerPortSetting; private readonly SemaphoreSlim _webserverSemaphore = new(1, 1); @@ -45,9 +46,11 @@ internal class WebServerService : IWebServerService, IDisposable _controllers = new List(); _webServerEnabledSetting = settingsService.GetSetting("WebServer.Enabled", true); + _webServerRemoteAccessSetting = settingsService.GetSetting("WebServer.RemoteAccess", false); _webServerPortSetting = settingsService.GetSetting("WebServer.Port", 9696); - _webServerEnabledSetting.SettingChanged += WebServerEnabledSettingOnSettingChanged; - _webServerPortSetting.SettingChanged += WebServerPortSettingOnSettingChanged; + _webServerEnabledSetting.SettingChanged += WebServerSettingsOnSettingChanged; + _webServerRemoteAccessSetting.SettingChanged += WebServerSettingsOnSettingChanged; + _webServerPortSetting.SettingChanged += WebServerSettingsOnSettingChanged; pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureDisabled; PluginsHandler = new PluginsHandler("plugins"); @@ -75,12 +78,7 @@ internal class WebServerService : IWebServerService, IDisposable WebServerStarted?.Invoke(this, EventArgs.Empty); } - private void WebServerEnabledSettingOnSettingChanged(object? sender, EventArgs e) - { - _ = StartWebServer(); - } - - private void WebServerPortSettingOnSettingChanged(object? sender, EventArgs e) + private void WebServerSettingsOnSettingChanged(object? sender, EventArgs e) { _ = StartWebServer(); } @@ -102,7 +100,6 @@ internal class WebServerService : IWebServerService, IDisposable public void Dispose() { Server?.DisposeAsync(); - _webServerPortSetting.SettingChanged -= WebServerPortSettingOnSettingChanged; } public IServer? Server { get; private set; } @@ -120,8 +117,6 @@ internal class WebServerService : IWebServerService, IDisposable OnWebServerStopped(); Server = null; } - - PluginsHandler.ServerUrl = $"http://localhost:{_webServerPortSetting.Value}/"; LayoutBuilder serverLayout = Layout.Create() .Add(PluginsHandler) @@ -138,12 +133,12 @@ internal class WebServerService : IWebServerService, IDisposable IServer server = Host.Create() .Handler(serverLayout.Build()) - .Bind(IPAddress.Loopback, (ushort) _webServerPortSetting.Value) + .Bind(_webServerRemoteAccessSetting.Value ? IPAddress.Any : IPAddress.Loopback, (ushort) _webServerPortSetting.Value) .Defaults() .Build(); // Store the URL in a webserver.txt file so that remote applications can find it - await File.WriteAllTextAsync(Path.Combine(Constants.DataFolder, "webserver.txt"), PluginsHandler.ServerUrl); + await File.WriteAllTextAsync(Path.Combine(Constants.DataFolder, "webserver.txt"), $"http://localhost:{_webServerPortSetting.Value}/"); return server; } diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml index c1ee681de..537a950b2 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml @@ -145,6 +145,22 @@ + + + + Enable remote access + + By default the web server can only be accessed by applications running on your own computer, e.g. supported games. + + + Enabling remote access allows you to access Artemis from other devices on your network, depending on your router even the outside world. + + + + + + + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs index 10ff14cc2..2605df33d 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs @@ -167,6 +167,7 @@ public class GeneralTabViewModel : RoutableScreen public PluginSetting CoreRenderScale => _settingsService.GetSetting("Core.RenderScale", 0.5); public PluginSetting CoreTargetFrameRate => _settingsService.GetSetting("Core.TargetFrameRate", 30); public PluginSetting WebServerEnabled => _settingsService.GetSetting("WebServer.Enabled", true); + public PluginSetting WebServerRemoteAccess => _settingsService.GetSetting("WebServer.RemoteAccess", false); public PluginSetting WebServerPort => _settingsService.GetSetting("WebServer.Port", 9696); private void ExecuteShowLogs() From 10a10b9149228dc8c9bb897b62727a90fc1199ff Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 16 Feb 2025 17:06:22 +0100 Subject: [PATCH 10/10] Plugin Settings - When searching, search in plugin and features Plugin Settings - Never scroll inside feature list, just show everything if a plugin has many features --- src/Artemis.Core/Plugins/PluginFeatureInfo.cs | 11 +++++++++++ src/Artemis.Core/Plugins/PluginInfo.cs | 17 ++++++++++++++--- .../Screens/Plugins/PluginSettingsView.axaml | 2 +- .../Screens/Settings/Tabs/PluginsTabView.axaml | 2 +- .../Settings/Tabs/PluginsTabViewModel.cs | 3 +-- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index cda82170d..77cfb84e7 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -95,5 +95,16 @@ public class PluginFeatureInfo : IPrerequisitesSubject return Instance?.Id ?? "Uninitialized feature"; } + /// + /// Returns a boolean indicating whether this feature info matches the provided search string + /// + /// The search string to match + /// A boolean indicating whether this plugin info matches the provided search string + public bool MatchesSearch(string search) + { + return Name.Contains(search, StringComparison.InvariantCultureIgnoreCase) || + (Description != null && Description.Contains(search, StringComparison.InvariantCultureIgnoreCase)); + } + internal PluginFeatureEntity Entity { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index a682fb135..5f668f506 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -155,19 +155,30 @@ public class PluginInfo : IPrerequisitesSubject { return PlatformPrerequisites.All(p => p.IsMet()); } - + /// public override string ToString() { return $"{Name} v{Version} - {Guid}"; } - + private bool MatchesMinimumVersion() { if (Constants.CurrentVersion == "local") return true; - + Version currentVersion = new(Constants.CurrentVersion); return currentVersion >= MinimumVersion; } + + /// + /// Returns a boolean indicating whether this plugin info matches the provided search string + /// + /// The search string to match + /// A boolean indicating whether this plugin info matches the provided search string + public bool MatchesSearch(string search) + { + return Name.Contains(search, StringComparison.InvariantCultureIgnoreCase) || + (Description != null && Description.Contains(search, StringComparison.InvariantCultureIgnoreCase)); + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml b/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml index ffed28a76..aa7b657a3 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml +++ b/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml @@ -13,7 +13,7 @@ Plugin features - + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml index b9377f61b..4fb1a771a 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml @@ -33,7 +33,7 @@ - + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs index 5c9c76cb4..ca289eed7 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs @@ -112,8 +112,7 @@ public partial class PluginsTabViewModel : RoutableScreen if (string.IsNullOrWhiteSpace(text)) return _ => true; - return data => data.Info.Name.Contains(text, StringComparison.InvariantCultureIgnoreCase) || - (data.Info.Description != null && data.Info.Description.Contains(text, StringComparison.InvariantCultureIgnoreCase)); + return data => data.Info.MatchesSearch(text) || data.Features.Any(f => f.MatchesSearch(text)); } public async Task GetMorePlugins()