mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-12 13:28:33 +00:00
Replace EmbedIO with GenHTTP
This commit is contained in:
parent
1ff509aac9
commit
e09389c6db
@ -37,7 +37,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" />
|
||||
<PackageReference Include="EmbedIO" />
|
||||
<PackageReference Include="GenHTTP.Core" />
|
||||
<PackageReference Include="GenHTTP.Modules.Controllers" />
|
||||
<PackageReference Include="HidSharp" />
|
||||
<PackageReference Include="HPPH.SkiaSharp" />
|
||||
<PackageReference Include="Humanizer.Core" />
|
||||
|
||||
@ -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<T> : PluginEndPoint where T : DataModel
|
||||
_update = CreateUpdateAction();
|
||||
|
||||
ThrowOnFail = true;
|
||||
Accepts = MimeType.Json;
|
||||
Accepts = ContentType.ApplicationJson;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -38,17 +37,16 @@ public class DataModelJsonPluginEndPoint<T> : PluginEndPoint where T : DataModel
|
||||
public bool ThrowOnFail { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ProcessRequest(IHttpContext context)
|
||||
protected override async Task<IResponse> 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<T>(await reader.ReadToEndAsync());
|
||||
T? dataModel = await JsonSerializer.DeserializeAsync<T>(request.Content, WebServerService.JsonOptions);
|
||||
if (dataModel != null)
|
||||
_update(dataModel, _module.DataModel);
|
||||
}
|
||||
@ -57,6 +55,8 @@ public class DataModelJsonPluginEndPoint<T> : PluginEndPoint where T : DataModel
|
||||
if (ThrowOnFail)
|
||||
throw;
|
||||
}
|
||||
|
||||
return request.Respond().Status(ResponseStatus.NoContent).Build();
|
||||
}
|
||||
|
||||
private Action<T, T> CreateUpdateAction()
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using EmbedIO;
|
||||
using GenHTTP.Api.Protocol;
|
||||
|
||||
namespace Artemis.Core.Services;
|
||||
|
||||
@ -8,13 +8,13 @@ namespace Artemis.Core.Services;
|
||||
/// </summary>
|
||||
public class EndpointRequestEventArgs : EventArgs
|
||||
{
|
||||
internal EndpointRequestEventArgs(IHttpContext context)
|
||||
internal EndpointRequestEventArgs(IRequest request)
|
||||
{
|
||||
Context = context;
|
||||
Request = request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the HTTP context of the request
|
||||
/// </summary>
|
||||
public IHttpContext Context { get; }
|
||||
public IRequest Request { get; }
|
||||
}
|
||||
@ -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<T> : PluginEndPoint
|
||||
{
|
||||
_requestHandler = requestHandler;
|
||||
ThrowOnFail = true;
|
||||
Accepts = MimeType.Json;
|
||||
Accepts = ContentType.ApplicationJson;
|
||||
}
|
||||
|
||||
internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func<T, object?> responseRequestHandler) : base(pluginFeature, name, pluginsModule)
|
||||
{
|
||||
_responseRequestHandler = responseRequestHandler;
|
||||
ThrowOnFail = true;
|
||||
Accepts = MimeType.Json;
|
||||
Returns = MimeType.Json;
|
||||
Accepts = ContentType.ApplicationJson;
|
||||
Returns = ContentType.ApplicationJson;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -41,25 +42,25 @@ public class JsonPluginEndPoint<T> : PluginEndPoint
|
||||
#region Overrides of PluginEndPoint
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ProcessRequest(IHttpContext context)
|
||||
protected override async Task<IResponse> 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<T>(await reader.ReadToEndAsync());
|
||||
T? deserialized = await JsonSerializer.DeserializeAsync<T>(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<T> : 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
|
||||
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// Gets the mime type of the input this end point accepts
|
||||
/// </summary>
|
||||
public string? Accepts { get; protected set; }
|
||||
public ContentType Accepts { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mime type of the output this end point returns
|
||||
/// </summary>
|
||||
public string? Returns { get; protected set; }
|
||||
public ContentType Returns { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs whenever a request threw an unhandled exception
|
||||
@ -70,8 +74,8 @@ public abstract class PluginEndPoint
|
||||
/// <summary>
|
||||
/// Called whenever the end point has to process a request
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP context of the request</param>
|
||||
protected abstract Task ProcessRequest(IHttpContext context);
|
||||
/// <param name="request">The HTTP context of the request</param>
|
||||
protected abstract Task<IResponse> ProcessRequest(IRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="RequestException" /> event
|
||||
@ -85,31 +89,36 @@ public abstract class PluginEndPoint
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="ProcessingRequest" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnProcessingRequest(IHttpContext context)
|
||||
protected virtual void OnProcessingRequest(IRequest request)
|
||||
{
|
||||
ProcessingRequest?.Invoke(this, new EndpointRequestEventArgs(context));
|
||||
ProcessingRequest?.Invoke(this, new EndpointRequestEventArgs(request));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="ProcessedRequest" /> event
|
||||
/// </summary>
|
||||
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<IResponse> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func<IHttpContext, Task> requestHandler) : base(pluginFeature, name, pluginsModule)
|
||||
internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func<IRequest, Task<IResponse>> requestHandler) : base(pluginFeature, name, pluginsModule)
|
||||
{
|
||||
RequestHandler = requestHandler;
|
||||
}
|
||||
@ -22,12 +22,12 @@ public class RawPluginEndPoint : PluginEndPoint
|
||||
/// <summary>
|
||||
/// Gets or sets the handler used to handle incoming requests to this endpoint
|
||||
/// </summary>
|
||||
public Func<IHttpContext, Task> RequestHandler { get; }
|
||||
public Func<IRequest, Task<IResponse>> RequestHandler { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the mime type this plugin end point accepts
|
||||
/// </summary>
|
||||
public void SetAcceptType(string type)
|
||||
public void SetAcceptType(ContentType type)
|
||||
{
|
||||
Accepts = type;
|
||||
}
|
||||
@ -35,7 +35,7 @@ public class RawPluginEndPoint : PluginEndPoint
|
||||
/// <summary>
|
||||
/// Sets the mime type this plugin end point returns
|
||||
/// </summary>
|
||||
public void SetReturnType(string type)
|
||||
public void SetReturnType(ContentType type)
|
||||
{
|
||||
Returns = type;
|
||||
}
|
||||
@ -43,9 +43,9 @@ public class RawPluginEndPoint : PluginEndPoint
|
||||
#region Overrides of PluginEndPoint
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ProcessRequest(IHttpContext context)
|
||||
protected override async Task<IResponse> ProcessRequest(IRequest request)
|
||||
{
|
||||
await RequestHandler(context);
|
||||
return await RequestHandler(request);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -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<string> requestHandler) : base(pluginFeature, name, pluginsModule)
|
||||
{
|
||||
_requestHandler = requestHandler;
|
||||
Accepts = MimeType.PlainText;
|
||||
Accepts = ContentType.TextPlain;
|
||||
}
|
||||
|
||||
internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func<string, string?> requestHandler) : base(pluginFeature, name, pluginsModule)
|
||||
{
|
||||
_responseRequestHandler = requestHandler;
|
||||
Accepts = MimeType.PlainText;
|
||||
Returns = MimeType.PlainText;
|
||||
Accepts = ContentType.TextPlain;
|
||||
Returns = ContentType.TextPlain;
|
||||
}
|
||||
|
||||
#region Overrides of PluginEndPoint
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ProcessRequest(IHttpContext context)
|
||||
protected override async Task<IResponse> 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
|
||||
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// Gets the current instance of the web server, replaced when <see cref="WebServerStarting" /> occurs.
|
||||
/// </summary>
|
||||
WebServer? Server { get; }
|
||||
IServer? Server { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugins module containing all plugin end points
|
||||
@ -84,7 +84,7 @@ public interface IWebServerService : IArtemisService
|
||||
/// <param name="endPointName">The name of the end point, must be unique</param>
|
||||
/// <param name="requestHandler"></param>
|
||||
/// <returns>The resulting end point</returns>
|
||||
RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Func<IHttpContext, Task> requestHandler);
|
||||
RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Func<IRequest, Task<IResponse>> requestHandler);
|
||||
|
||||
/// <summary>
|
||||
/// Removes an existing endpoint
|
||||
@ -96,30 +96,14 @@ public interface IWebServerService : IArtemisService
|
||||
/// Adds a new Web API controller and restarts the web server
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of Web API controller to remove</typeparam>
|
||||
WebApiControllerRegistration AddController<T>(PluginFeature feature) where T : WebApiController;
|
||||
WebApiControllerRegistration AddController<T>(PluginFeature feature, string path) where T : class;
|
||||
|
||||
/// <summary>
|
||||
/// Removes an existing Web API controller and restarts the web server
|
||||
/// </summary>
|
||||
/// <param name="registration">The registration of the controller to remove.</param>
|
||||
void RemoveController(WebApiControllerRegistration registration);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new EmbedIO module and restarts the web server
|
||||
/// </summary>
|
||||
WebModuleRegistration AddModule(PluginFeature feature, Func<IWebModule> create);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a EmbedIO module and restarts the web server
|
||||
/// </summary>
|
||||
void RemoveModule(WebModuleRegistration create);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new EmbedIO module and restarts the web server
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of module to add</typeparam>
|
||||
WebModuleRegistration AddModule<T>(PluginFeature feature) where T : IWebModule;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the web server has been created and is about to start. This is the ideal place to add your own modules.
|
||||
/// </summary>
|
||||
|
||||
@ -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
|
||||
/// <see cref="PluginEndPoint" />.
|
||||
/// </summary>
|
||||
public class PluginsModule : WebModuleBase
|
||||
public class PluginsModule : IHandler
|
||||
{
|
||||
private readonly Dictionary<string, Dictionary<string, PluginEndPoint>> _pluginEndPoints;
|
||||
|
||||
internal PluginsModule(string baseRoute) : base(baseRoute)
|
||||
internal PluginsModule(string baseRoute)
|
||||
{
|
||||
BaseRoute = baseRoute;
|
||||
_pluginEndPoints = new Dictionary<string, Dictionary<string, PluginEndPoint>>(comparer: StringComparer.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base route of the module
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
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<string, PluginEndPoint>? 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsFinalHandler => false;
|
||||
public async ValueTask<IResponse?> 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<string, PluginEndPoint>? 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; }
|
||||
|
||||
|
||||
@ -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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the web API controller.</typeparam>
|
||||
public class WebApiControllerRegistration<T> : WebApiControllerRegistration where T : WebApiController
|
||||
public class WebApiControllerRegistration<T> : 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<T>();
|
||||
}
|
||||
|
||||
internal Func<T> Factory { get; set; }
|
||||
internal override object UntypedFactory => Factory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -28,12 +24,13 @@ public abstract class WebApiControllerRegistration
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="WebApiControllerRegistration"/> class.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the web API controller.
|
||||
/// </summary>
|
||||
public Type ControllerType { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin feature that provided the web API controller.
|
||||
/// </summary>
|
||||
public PluginFeature Feature { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path at which the controller is available.
|
||||
/// </summary>
|
||||
public string Path { get; }
|
||||
|
||||
internal Func<object> Factory { get; set; }
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
using System;
|
||||
using EmbedIO;
|
||||
|
||||
namespace Artemis.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a registration for a web module.
|
||||
/// </summary>
|
||||
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<IWebModule> create)
|
||||
{
|
||||
_webServerService = webServerService;
|
||||
Feature = feature ?? throw new ArgumentNullException(nameof(feature));
|
||||
Create = create ?? throw new ArgumentNullException(nameof(create));
|
||||
|
||||
Feature.Disabled += FeatureOnDisabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The plugin feature that provided the web module.
|
||||
/// </summary>
|
||||
public PluginFeature Feature { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the web module.
|
||||
/// </summary>
|
||||
public Type? WebModuleType { get; }
|
||||
|
||||
internal Func<IWebModule>? 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;
|
||||
}
|
||||
}
|
||||
@ -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<WebApiControllerRegistration> _controllers;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ICoreService _coreService;
|
||||
private readonly List<WebModuleRegistration> _modules;
|
||||
private readonly PluginSetting<bool> _webServerEnabledSetting;
|
||||
private readonly PluginSetting<int> _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<WebApiControllerRegistration>();
|
||||
_modules = new List<WebModuleRegistration>();
|
||||
|
||||
_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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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 <IServer> 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<WebApiController>) 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<IHttpContext, Task> requestHandler)
|
||||
public RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Func<IRequest, Task<IResponse>> 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<T>(PluginFeature feature) where T : WebApiController
|
||||
public WebApiControllerRegistration AddController<T>(PluginFeature feature, string path) where T : class
|
||||
{
|
||||
if (feature == null) throw new ArgumentNullException(nameof(feature));
|
||||
|
||||
WebApiControllerRegistration<T> registration = new(this, feature);
|
||||
WebApiControllerRegistration<T> 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<IWebModule> create)
|
||||
{
|
||||
if (feature == null) throw new ArgumentNullException(nameof(feature));
|
||||
|
||||
WebModuleRegistration registration = new(this, feature, create);
|
||||
_modules.Add(registration);
|
||||
StartWebServer();
|
||||
|
||||
return registration;
|
||||
}
|
||||
|
||||
public WebModuleRegistration AddModule<T>(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<string, object?>
|
||||
{
|
||||
{"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
|
||||
|
||||
@ -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<string> 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();
|
||||
}
|
||||
|
||||
@ -94,6 +94,6 @@ public class RegistrationService : IRegistrationService
|
||||
|
||||
public void RegisterControllers()
|
||||
{
|
||||
_webServerService.AddController<RemoteController>(Constants.CorePlugin.Features.First().Instance!);
|
||||
_webServerService.AddController<RemoteController>(Constants.CorePlugin.Features.First().Instance!, "remote");
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,8 @@
|
||||
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.2.3" />
|
||||
<PackageVersion Include="Avalonia.Skia.Lottie" Version="11.0.0" />
|
||||
<PackageVersion Include="Avalonia.Win32" Version="11.2.3" />
|
||||
<PackageVersion Include="GenHTTP.Core" Version="9.6.2" />
|
||||
<PackageVersion Include="GenHTTP.Modules.Controllers" Version="9.6.2" />
|
||||
<PackageVersion Include="HPPH.SkiaSharp" Version="1.0.0" />
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.1" />
|
||||
<PackageVersion Include="Avalonia.Xaml.Behaviors" Version="11.2.0.8" />
|
||||
@ -21,7 +23,6 @@
|
||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||
<PackageVersion Include="DryIoc.dll" Version="5.4.3" />
|
||||
<PackageVersion Include="DynamicData" Version="9.1.1" />
|
||||
<PackageVersion Include="EmbedIO" Version="3.5.2" />
|
||||
<PackageVersion Include="FluentAvalonia.ProgressRing" Version="1.69.2" />
|
||||
<PackageVersion Include="FluentAvaloniaUI" Version="2.2.0" />
|
||||
<PackageVersion Include="HidSharp" Version="2.1.0" />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user