mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-12 13:28:33 +00:00
Merge branch 'development'
This commit is contained in:
commit
acd35176e1
@ -37,7 +37,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" />
|
<PackageReference Include="DryIoc.dll" />
|
||||||
<PackageReference Include="EmbedIO" />
|
<PackageReference Include="GenHTTP.Core" />
|
||||||
|
<PackageReference Include="GenHTTP.Modules.Webservices" />
|
||||||
<PackageReference Include="HidSharp" />
|
<PackageReference Include="HidSharp" />
|
||||||
<PackageReference Include="HPPH.SkiaSharp" />
|
<PackageReference Include="HPPH.SkiaSharp" />
|
||||||
<PackageReference Include="Humanizer.Core" />
|
<PackageReference Include="Humanizer.Core" />
|
||||||
|
|||||||
@ -95,5 +95,16 @@ public class PluginFeatureInfo : IPrerequisitesSubject
|
|||||||
return Instance?.Id ?? "Uninitialized feature";
|
return Instance?.Id ?? "Uninitialized feature";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a boolean indicating whether this feature info matches the provided search string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="search">The search string to match</param>
|
||||||
|
/// <returns>A boolean indicating whether this plugin info matches the provided search string</returns>
|
||||||
|
public bool MatchesSearch(string search)
|
||||||
|
{
|
||||||
|
return Name.Contains(search, StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
|
(Description != null && Description.Contains(search, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
internal PluginFeatureEntity Entity { get; }
|
internal PluginFeatureEntity Entity { get; }
|
||||||
}
|
}
|
||||||
@ -155,19 +155,30 @@ public class PluginInfo : IPrerequisitesSubject
|
|||||||
{
|
{
|
||||||
return PlatformPrerequisites.All(p => p.IsMet());
|
return PlatformPrerequisites.All(p => p.IsMet());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{Name} v{Version} - {Guid}";
|
return $"{Name} v{Version} - {Guid}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool MatchesMinimumVersion()
|
private bool MatchesMinimumVersion()
|
||||||
{
|
{
|
||||||
if (Constants.CurrentVersion == "local")
|
if (Constants.CurrentVersion == "local")
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
Version currentVersion = new(Constants.CurrentVersion);
|
Version currentVersion = new(Constants.CurrentVersion);
|
||||||
return currentVersion >= MinimumVersion;
|
return currentVersion >= MinimumVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a boolean indicating whether this plugin info matches the provided search string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="search">The search string to match</param>
|
||||||
|
/// <returns>A boolean indicating whether this plugin info matches the provided search string</returns>
|
||||||
|
public bool MatchesSearch(string search)
|
||||||
|
{
|
||||||
|
return Name.Contains(search, StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
|
(Description != null && Description.Contains(search, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,84 +0,0 @@
|
|||||||
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;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a plugin web endpoint receiving an object of type <typeparamref name="T" /> and returning any
|
|
||||||
/// <see cref="object" /> or <see langword="null" />.
|
|
||||||
/// <para>Note: Both will be deserialized and serialized respectively using JSON.</para>
|
|
||||||
/// </summary>
|
|
||||||
public class DataModelJsonPluginEndPoint<T> : PluginEndPoint where T : DataModel, new()
|
|
||||||
{
|
|
||||||
private readonly Module<T> _module;
|
|
||||||
private readonly Action<T, T> _update;
|
|
||||||
|
|
||||||
internal DataModelJsonPluginEndPoint(Module<T> module, string name, PluginsModule pluginsModule) : base(module, name, pluginsModule)
|
|
||||||
{
|
|
||||||
_module = module ?? throw new ArgumentNullException(nameof(module));
|
|
||||||
_update = CreateUpdateAction();
|
|
||||||
|
|
||||||
ThrowOnFail = true;
|
|
||||||
Accepts = MimeType.Json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether or not the end point should throw an exception if deserializing the received JSON fails.
|
|
||||||
/// If set to <see langword="false" /> malformed JSON is silently ignored; if set to <see langword="true" /> malformed
|
|
||||||
/// JSON throws a <see cref="JsonException" />.
|
|
||||||
/// </summary>
|
|
||||||
public bool ThrowOnFail { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override async Task ProcessRequest(IHttpContext context)
|
|
||||||
{
|
|
||||||
if (context.Request.HttpVerb != HttpVerbs.Post && context.Request.HttpVerb != HttpVerbs.Put)
|
|
||||||
throw HttpException.MethodNotAllowed("This end point only accepts POST and PUT calls");
|
|
||||||
|
|
||||||
context.Response.ContentType = MimeType.Json;
|
|
||||||
|
|
||||||
using TextReader reader = context.OpenRequestText();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
T? dataModel = CoreJson.Deserialize<T>(await reader.ReadToEndAsync());
|
|
||||||
if (dataModel != null)
|
|
||||||
_update(dataModel, _module.DataModel);
|
|
||||||
}
|
|
||||||
catch (JsonException)
|
|
||||||
{
|
|
||||||
if (ThrowOnFail)
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Action<T, T> CreateUpdateAction()
|
|
||||||
{
|
|
||||||
ParameterExpression sourceParameter = Expression.Parameter(typeof(T), "source");
|
|
||||||
ParameterExpression targetParameter = Expression.Parameter(typeof(T), "target");
|
|
||||||
|
|
||||||
IEnumerable<BinaryExpression> 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<Action<T, T>>(body, sourceParameter, targetParameter).Compile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using EmbedIO;
|
using GenHTTP.Api.Protocol;
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
@ -8,13 +8,13 @@ namespace Artemis.Core.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class EndpointRequestEventArgs : EventArgs
|
public class EndpointRequestEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
internal EndpointRequestEventArgs(IHttpContext context)
|
internal EndpointRequestEventArgs(IRequest request)
|
||||||
{
|
{
|
||||||
Context = context;
|
Request = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the HTTP context of the request
|
/// Gets the HTTP context of the request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IHttpContext Context { get; }
|
public IRequest Request { get; }
|
||||||
}
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using EmbedIO;
|
using GenHTTP.Api.Protocol;
|
||||||
|
using GenHTTP.Modules.Basics;
|
||||||
|
using GenHTTP.Modules.Conversion.Serializers.Json;
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
@ -16,19 +17,19 @@ public class JsonPluginEndPoint<T> : PluginEndPoint
|
|||||||
private readonly Action<T>? _requestHandler;
|
private readonly Action<T>? _requestHandler;
|
||||||
private readonly Func<T, object?>? _responseRequestHandler;
|
private readonly Func<T, object?>? _responseRequestHandler;
|
||||||
|
|
||||||
internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action<T> requestHandler) : base(pluginFeature, name, pluginsModule)
|
internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsHandler pluginsHandler, Action<T> requestHandler) : base(pluginFeature, name, pluginsHandler)
|
||||||
{
|
{
|
||||||
_requestHandler = requestHandler;
|
_requestHandler = requestHandler;
|
||||||
ThrowOnFail = true;
|
ThrowOnFail = true;
|
||||||
Accepts = MimeType.Json;
|
Accepts = FlexibleContentType.Get(ContentType.ApplicationJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func<T, object?> responseRequestHandler) : base(pluginFeature, name, pluginsModule)
|
internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsHandler pluginsHandler, Func<T, object?> responseRequestHandler) : base(pluginFeature, name, pluginsHandler)
|
||||||
{
|
{
|
||||||
_responseRequestHandler = responseRequestHandler;
|
_responseRequestHandler = responseRequestHandler;
|
||||||
ThrowOnFail = true;
|
ThrowOnFail = true;
|
||||||
Accepts = MimeType.Json;
|
Accepts = FlexibleContentType.Get(ContentType.ApplicationJson);
|
||||||
Returns = MimeType.Json;
|
Returns = FlexibleContentType.Get(ContentType.ApplicationJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -41,25 +42,25 @@ public class JsonPluginEndPoint<T> : PluginEndPoint
|
|||||||
#region Overrides of PluginEndPoint
|
#region Overrides of PluginEndPoint
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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)
|
if (request.Method != RequestMethod.Post && request.Method != RequestMethod.Put)
|
||||||
throw HttpException.MethodNotAllowed("This end point only accepts POST and PUT calls");
|
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;
|
object? response = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
T? deserialized = JsonSerializer.Deserialize<T>(await reader.ReadToEndAsync());
|
T? deserialized = await JsonSerializer.DeserializeAsync<T>(request.Content, WebServerService.JsonOptions);
|
||||||
if (deserialized == null)
|
if (deserialized == null)
|
||||||
throw new JsonException("Deserialization returned null");
|
throw new JsonException("Deserialization returned null");
|
||||||
|
|
||||||
if (_requestHandler != null)
|
if (_requestHandler != null)
|
||||||
{
|
{
|
||||||
_requestHandler(deserialized);
|
_requestHandler(deserialized);
|
||||||
return;
|
return request.Respond().Status(ResponseStatus.NoContent).Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_responseRequestHandler != null)
|
if (_responseRequestHandler != null)
|
||||||
@ -73,8 +74,14 @@ public class JsonPluginEndPoint<T> : PluginEndPoint
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
await using TextWriter writer = context.OpenResponseText();
|
// TODO: Cache options
|
||||||
await writer.WriteAsync(JsonSerializer.Serialize(response));
|
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
|
#endregion
|
||||||
|
|||||||
@ -1,20 +1,24 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
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;
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a base type for plugin end points to be targeted by the <see cref="PluginsModule" />
|
/// Represents a base type for plugin end points to be targeted by the <see cref="PluginsHandler" />
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class PluginEndPoint
|
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;
|
PluginFeature = pluginFeature;
|
||||||
Name = name;
|
Name = name;
|
||||||
|
|
||||||
@ -29,7 +33,7 @@ public abstract class PluginEndPoint
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the full URL of the end point
|
/// Gets the full URL of the end point
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Url => $"{_pluginsModule.ServerUrl?.TrimEnd('/')}{_pluginsModule.BaseRoute}{PluginFeature.Plugin.Guid}/{Name}";
|
public string Url => $"/{_pluginsHandler.BaseRoute}/{PluginFeature.Plugin.Guid}/{Name}";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the plugin the end point is associated with
|
/// Gets the plugin the end point is associated with
|
||||||
@ -42,15 +46,15 @@ public abstract class PluginEndPoint
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public PluginInfo PluginInfo => PluginFeature.Plugin.Info;
|
public PluginInfo PluginInfo => PluginFeature.Plugin.Info;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary><summary>
|
||||||
/// Gets the mime type of the input this end point accepts
|
/// Gets the mime type of the input this end point accepts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Accepts { get; protected set; }
|
public FlexibleContentType Accepts { get; protected set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the mime type of the output this end point returns
|
/// Gets the mime type of the output this end point returns
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Returns { get; protected set; }
|
public FlexibleContentType Returns { get; protected set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs whenever a request threw an unhandled exception
|
/// Occurs whenever a request threw an unhandled exception
|
||||||
@ -70,8 +74,8 @@ public abstract class PluginEndPoint
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called whenever the end point has to process a request
|
/// Called whenever the end point has to process a request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">The HTTP context of the request</param>
|
/// <param name="request">The HTTP context of the request</param>
|
||||||
protected abstract Task ProcessRequest(IHttpContext context);
|
protected abstract Task<IResponse> ProcessRequest(IRequest request);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invokes the <see cref="RequestException" /> event
|
/// Invokes the <see cref="RequestException" /> event
|
||||||
@ -85,37 +89,49 @@ public abstract class PluginEndPoint
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invokes the <see cref="ProcessingRequest" /> event
|
/// Invokes the <see cref="ProcessingRequest" /> event
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Invokes the <see cref="ProcessedRequest" /> event
|
/// Invokes the <see cref="ProcessedRequest" /> event
|
||||||
/// </summary>
|
/// </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
|
try
|
||||||
{
|
{
|
||||||
OnProcessingRequest(context);
|
OnProcessingRequest(context);
|
||||||
await ProcessRequest(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);
|
OnProcessedRequest(context);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
OnRequestException(e);
|
OnRequestException(e);
|
||||||
throw;
|
return context.Respond()
|
||||||
|
.Status(ResponseStatus.InternalServerError)
|
||||||
|
.Content(new StringContent(e.ToString()))
|
||||||
|
.Type(ContentType.TextPlain)
|
||||||
|
.Build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDisabled(object? sender, EventArgs e)
|
private void OnDisabled(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
PluginFeature.Disabled -= OnDisabled;
|
PluginFeature.Disabled -= OnDisabled;
|
||||||
_pluginsModule.RemovePluginEndPoint(this);
|
_pluginsHandler.RemovePluginEndPoint(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using EmbedIO;
|
using GenHTTP.Api.Protocol;
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ namespace Artemis.Core.Services;
|
|||||||
public class RawPluginEndPoint : PluginEndPoint
|
public class RawPluginEndPoint : PluginEndPoint
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func<IHttpContext, Task> requestHandler) : base(pluginFeature, name, pluginsModule)
|
internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsHandler pluginsHandler, Func<IRequest, Task<IResponse>> requestHandler) : base(pluginFeature, name, pluginsHandler)
|
||||||
{
|
{
|
||||||
RequestHandler = requestHandler;
|
RequestHandler = requestHandler;
|
||||||
}
|
}
|
||||||
@ -22,30 +22,30 @@ public class RawPluginEndPoint : PluginEndPoint
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler used to handle incoming requests to this endpoint
|
/// Gets or sets the handler used to handle incoming requests to this endpoint
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<IHttpContext, Task> RequestHandler { get; }
|
public Func<IRequest, Task<IResponse>> RequestHandler { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the mime type this plugin end point accepts
|
/// Sets the mime type this plugin end point accepts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetAcceptType(string type)
|
public void SetAcceptType(ContentType type)
|
||||||
{
|
{
|
||||||
Accepts = type;
|
Accepts = FlexibleContentType.Get(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the mime type this plugin end point returns
|
/// Sets the mime type this plugin end point returns
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetReturnType(string type)
|
public void SetReturnType(ContentType type)
|
||||||
{
|
{
|
||||||
Returns = type;
|
Returns = FlexibleContentType.Get(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Overrides of PluginEndPoint
|
#region Overrides of PluginEndPoint
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override async Task ProcessRequest(IHttpContext context)
|
protected override async Task<IResponse> ProcessRequest(IRequest request)
|
||||||
{
|
{
|
||||||
await RequestHandler(context);
|
return await RequestHandler(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using EmbedIO;
|
using GenHTTP.Api.Protocol;
|
||||||
|
using GenHTTP.Modules.Basics;
|
||||||
|
using GenHTTP.Modules.IO.Strings;
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
@ -14,35 +16,37 @@ public class StringPluginEndPoint : PluginEndPoint
|
|||||||
private readonly Action<string>? _requestHandler;
|
private readonly Action<string>? _requestHandler;
|
||||||
private readonly Func<string, string?>? _responseRequestHandler;
|
private readonly Func<string, string?>? _responseRequestHandler;
|
||||||
|
|
||||||
internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action<string> requestHandler) : base(pluginFeature, name, pluginsModule)
|
internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsHandler pluginsHandler, Action<string> requestHandler) : base(pluginFeature, name, pluginsHandler)
|
||||||
{
|
{
|
||||||
_requestHandler = requestHandler;
|
_requestHandler = requestHandler;
|
||||||
Accepts = MimeType.PlainText;
|
Accepts = FlexibleContentType.Get(ContentType.TextPlain);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func<string, string?> requestHandler) : base(pluginFeature, name, pluginsModule)
|
internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsHandler pluginsHandler, Func<string, string?> requestHandler) : base(pluginFeature, name, pluginsHandler)
|
||||||
{
|
{
|
||||||
_responseRequestHandler = requestHandler;
|
_responseRequestHandler = requestHandler;
|
||||||
Accepts = MimeType.PlainText;
|
Accepts = FlexibleContentType.Get(ContentType.TextPlain);
|
||||||
Returns = MimeType.PlainText;
|
Returns = FlexibleContentType.Get(ContentType.TextPlain);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Overrides of PluginEndPoint
|
#region Overrides of PluginEndPoint
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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)
|
if (request.Method != RequestMethod.Post && request.Method != RequestMethod.Put)
|
||||||
throw HttpException.MethodNotAllowed("This end point only accepts POST and PUT calls");
|
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;
|
string? response;
|
||||||
if (_requestHandler != null)
|
if (_requestHandler != null)
|
||||||
{
|
{
|
||||||
_requestHandler(await reader.ReadToEndAsync());
|
_requestHandler(await reader.ReadToEndAsync());
|
||||||
return;
|
return request.Respond().Status(ResponseStatus.Ok).Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_responseRequestHandler != null)
|
if (_responseRequestHandler != null)
|
||||||
@ -50,8 +54,13 @@ public class StringPluginEndPoint : PluginEndPoint
|
|||||||
else
|
else
|
||||||
throw new ArtemisCoreException("String plugin end point has no request handler");
|
throw new ArtemisCoreException("String plugin end point has no request handler");
|
||||||
|
|
||||||
await using TextWriter writer = context.OpenResponseText();
|
if (response == null)
|
||||||
await writer.WriteAsync(response);
|
return request.Respond().Status(ResponseStatus.NoContent).Build();
|
||||||
|
return request.Respond()
|
||||||
|
.Status(ResponseStatus.Ok)
|
||||||
|
.Content(new StringContent(response))
|
||||||
|
.Type(ContentType.TextPlain)
|
||||||
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.Core.Modules;
|
using Artemis.Core.Modules;
|
||||||
using EmbedIO;
|
using GenHTTP.Api.Infrastructure;
|
||||||
using EmbedIO.WebApi;
|
using GenHTTP.Api.Protocol;
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
@ -14,12 +14,12 @@ public interface IWebServerService : IArtemisService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current instance of the web server, replaced when <see cref="WebServerStarting" /> occurs.
|
/// Gets the current instance of the web server, replaced when <see cref="WebServerStarting" /> occurs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
WebServer? Server { get; }
|
IServer? Server { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the plugins module containing all plugin end points
|
/// Gets the plugins module containing all plugin end points
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PluginsModule PluginsModule { get; }
|
PluginsHandler PluginsHandler { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a new endpoint for the given plugin feature receiving an object of type <typeparamref name="T" />
|
/// Adds a new endpoint for the given plugin feature receiving an object of type <typeparamref name="T" />
|
||||||
@ -44,16 +44,6 @@ public interface IWebServerService : IArtemisService
|
|||||||
/// <returns>The resulting end point</returns>
|
/// <returns>The resulting end point</returns>
|
||||||
JsonPluginEndPoint<T> AddResponsiveJsonEndPoint<T>(PluginFeature feature, string endPointName, Func<T, object?> requestHandler);
|
JsonPluginEndPoint<T> AddResponsiveJsonEndPoint<T>(PluginFeature feature, string endPointName, Func<T, object?> requestHandler);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a new endpoint that directly maps received JSON to the data model of the provided <paramref name="module" />.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The data model type of the module</typeparam>
|
|
||||||
/// <param name="module">The module whose datamodel to apply the received JSON to</param>
|
|
||||||
/// <param name="endPointName">The name of the end point, must be unique</param>
|
|
||||||
/// <returns>The resulting end point</returns>
|
|
||||||
[Obsolete("This way of updating is too unpredictable in combination with nested events, use AddJsonEndPoint<T> to update manually instead")]
|
|
||||||
DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(Module<T> module, string endPointName) where T : DataModel, new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a new endpoint for the given plugin feature receiving an a <see cref="string" />.
|
/// Adds a new endpoint for the given plugin feature receiving an a <see cref="string" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -84,7 +74,7 @@ public interface IWebServerService : IArtemisService
|
|||||||
/// <param name="endPointName">The name of the end point, must be unique</param>
|
/// <param name="endPointName">The name of the end point, must be unique</param>
|
||||||
/// <param name="requestHandler"></param>
|
/// <param name="requestHandler"></param>
|
||||||
/// <returns>The resulting end point</returns>
|
/// <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>
|
/// <summary>
|
||||||
/// Removes an existing endpoint
|
/// Removes an existing endpoint
|
||||||
@ -95,31 +85,15 @@ public interface IWebServerService : IArtemisService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a new Web API controller and restarts the web server
|
/// Adds a new Web API controller and restarts the web server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of Web API controller to remove</typeparam>
|
/// <typeparam name="T">The type of Web API controller to add</typeparam>
|
||||||
WebApiControllerRegistration AddController<T>(PluginFeature feature) where T : WebApiController;
|
WebApiControllerRegistration AddController<T>(PluginFeature feature, string path) where T : class;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes an existing Web API controller and restarts the web server
|
/// Removes an existing Web API controller and restarts the web server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="registration">The registration of the controller to remove.</param>
|
/// <param name="registration">The registration of the controller to remove.</param>
|
||||||
void RemoveController(WebApiControllerRegistration registration);
|
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>
|
/// <summary>
|
||||||
/// Occurs when the web server has been created and is about to start. This is the ideal place to add your own modules.
|
/// Occurs when the web server has been created and is about to start. This is the ideal place to add your own modules.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,24 +1,35 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
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;
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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
|
||||||
/// <see cref="PluginEndPoint" />.
|
/// <see cref="PluginEndPoint" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PluginsModule : WebModuleBase
|
public class PluginsHandler : IHandler
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, Dictionary<string, PluginEndPoint>> _pluginEndPoints;
|
private readonly Dictionary<string, Dictionary<string, PluginEndPoint>> _pluginEndPoints;
|
||||||
|
|
||||||
internal PluginsModule(string baseRoute) : base(baseRoute)
|
internal PluginsHandler(string baseRoute)
|
||||||
{
|
{
|
||||||
|
BaseRoute = baseRoute;
|
||||||
_pluginEndPoints = new Dictionary<string, Dictionary<string, PluginEndPoint>>(comparer: StringComparer.InvariantCultureIgnoreCase);
|
_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)
|
internal void AddPluginEndPoint(PluginEndPoint registration)
|
||||||
{
|
{
|
||||||
string id = registration.PluginFeature.Plugin.Guid.ToString();
|
string id = registration.PluginFeature.Plugin.Guid.ToString();
|
||||||
@ -42,47 +53,50 @@ public class PluginsModule : WebModuleBase
|
|||||||
return;
|
return;
|
||||||
registrations.Remove(registration.Name);
|
registrations.Remove(registration.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Overrides of WebModuleBase
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override async Task OnRequestAsync(IHttpContext context)
|
public ValueTask PrepareAsync()
|
||||||
{
|
{
|
||||||
if (context.Route.SubPath == null)
|
return ValueTask.CompletedTask;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool IsFinalHandler => false;
|
public async ValueTask<IResponse?> HandleAsync(IRequest request)
|
||||||
|
{
|
||||||
|
// 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(pluginId, out Dictionary<string, PluginEndPoint>? endPoints))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
request.Target.Advance();
|
||||||
|
string? endPointName = request.Target.Current?.Value;
|
||||||
|
if (endPointName == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Find a matching endpoint
|
||||||
|
if (!endPoints.TryGetValue(endPointName, out PluginEndPoint? endPoint))
|
||||||
|
{
|
||||||
|
return request.Respond()
|
||||||
|
.Status(ResponseStatus.NotFound)
|
||||||
|
.Content(new StringContent($"Found no endpoint called {endPointName} for plugin with ID {pluginId}."))
|
||||||
|
.Type(ContentType.TextPlain)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
internal string? ServerUrl { get; set; }
|
// It is up to the registration how the request is eventually handled
|
||||||
|
return await endPoint.InternalProcessRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Overrides of WebModuleBase
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a read only collection containing all current plugin end points
|
/// Gets a read only collection containing all current plugin end points
|
||||||
/// </summary>
|
/// </summary>
|
||||||
27
src/Artemis.Core/Services/WebServer/StatusHandler.cs
Normal file
27
src/Artemis.Core/Services/WebServer/StatusHandler.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using GenHTTP.Api.Content;
|
||||||
|
using GenHTTP.Api.Protocol;
|
||||||
|
|
||||||
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an GenHTTP handler used to process web requests and forward them to the right
|
||||||
|
/// <see cref="PluginEndPoint" />.
|
||||||
|
/// </summary>
|
||||||
|
public class StatusHandler : IHandler
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ValueTask PrepareAsync()
|
||||||
|
{
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ValueTask<IResponse?> 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<IResponse?>(request.Respond().Status(ResponseStatus.NoContent).Build())
|
||||||
|
: ValueTask.FromResult<IResponse?>(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using EmbedIO.WebApi;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
namespace Artemis.Core.Services;
|
||||||
|
|
||||||
@ -7,15 +6,12 @@ namespace Artemis.Core.Services;
|
|||||||
/// Represents a web API controller registration.
|
/// Represents a web API controller registration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of the web API controller.</typeparam>
|
/// <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>();
|
Factory = () => feature.Plugin.Resolve<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Func<T> Factory { get; set; }
|
|
||||||
internal override object UntypedFactory => Factory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -28,12 +24,13 @@ public abstract class WebApiControllerRegistration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new instance of the <see cref="WebApiControllerRegistration"/> class.
|
/// Creates a new instance of the <see cref="WebApiControllerRegistration"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal WebApiControllerRegistration(IWebServerService webServerService, PluginFeature feature, Type controllerType)
|
protected internal WebApiControllerRegistration(IWebServerService webServerService, PluginFeature feature, Type controllerType, string path)
|
||||||
{
|
{
|
||||||
_webServerService = webServerService;
|
_webServerService = webServerService;
|
||||||
Feature = feature;
|
Feature = feature;
|
||||||
ControllerType = controllerType;
|
ControllerType = controllerType;
|
||||||
|
Path = path;
|
||||||
|
|
||||||
Feature.Disabled += FeatureOnDisabled;
|
Feature.Disabled += FeatureOnDisabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,15 +40,20 @@ public abstract class WebApiControllerRegistration
|
|||||||
Feature.Disabled -= FeatureOnDisabled;
|
Feature.Disabled -= FeatureOnDisabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal abstract object UntypedFactory { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the type of the web API controller.
|
/// Gets the type of the web API controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Type ControllerType { get; }
|
public Type ControllerType { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the plugin feature that provided the web API controller.
|
/// Gets the plugin feature that provided the web API controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PluginFeature Feature { get; }
|
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,22 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Net;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.Core.Modules;
|
using GenHTTP.Api.Infrastructure;
|
||||||
using EmbedIO;
|
using GenHTTP.Api.Protocol;
|
||||||
using EmbedIO.WebApi;
|
using GenHTTP.Engine.Internal;
|
||||||
|
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;
|
using Serilog;
|
||||||
|
|
||||||
namespace Artemis.Core.Services;
|
namespace Artemis.Core.Services;
|
||||||
@ -19,27 +27,33 @@ internal class WebServerService : IWebServerService, IDisposable
|
|||||||
private readonly List<WebApiControllerRegistration> _controllers;
|
private readonly List<WebApiControllerRegistration> _controllers;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly ICoreService _coreService;
|
private readonly ICoreService _coreService;
|
||||||
private readonly List<WebModuleRegistration> _modules;
|
|
||||||
private readonly PluginSetting<bool> _webServerEnabledSetting;
|
private readonly PluginSetting<bool> _webServerEnabledSetting;
|
||||||
|
private readonly PluginSetting<bool> _webServerRemoteAccessSetting;
|
||||||
private readonly PluginSetting<int> _webServerPortSetting;
|
private readonly PluginSetting<int> _webServerPortSetting;
|
||||||
private readonly object _webserverLock = new();
|
private readonly SemaphoreSlim _webserverSemaphore = new(1, 1);
|
||||||
private readonly JsonSerializerOptions _jsonOptions = new(CoreJson.GetJsonSerializerOptions()) {ReferenceHandler = ReferenceHandler.IgnoreCycles, WriteIndented = true};
|
|
||||||
private CancellationTokenSource? _cts;
|
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)
|
public WebServerService(ILogger logger, ICoreService coreService, ISettingsService settingsService, IPluginManagementService pluginManagementService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_coreService = coreService;
|
_coreService = coreService;
|
||||||
_controllers = new List<WebApiControllerRegistration>();
|
_controllers = new List<WebApiControllerRegistration>();
|
||||||
_modules = new List<WebModuleRegistration>();
|
|
||||||
|
|
||||||
_webServerEnabledSetting = settingsService.GetSetting("WebServer.Enabled", true);
|
_webServerEnabledSetting = settingsService.GetSetting("WebServer.Enabled", true);
|
||||||
|
_webServerRemoteAccessSetting = settingsService.GetSetting("WebServer.RemoteAccess", false);
|
||||||
_webServerPortSetting = settingsService.GetSetting("WebServer.Port", 9696);
|
_webServerPortSetting = settingsService.GetSetting("WebServer.Port", 9696);
|
||||||
_webServerEnabledSetting.SettingChanged += WebServerEnabledSettingOnSettingChanged;
|
_webServerEnabledSetting.SettingChanged += WebServerSettingsOnSettingChanged;
|
||||||
_webServerPortSetting.SettingChanged += WebServerPortSettingOnSettingChanged;
|
_webServerRemoteAccessSetting.SettingChanged += WebServerSettingsOnSettingChanged;
|
||||||
|
_webServerPortSetting.SettingChanged += WebServerSettingsOnSettingChanged;
|
||||||
pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureDisabled;
|
pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureDisabled;
|
||||||
|
|
||||||
PluginsModule = new PluginsModule("/plugins");
|
PluginsHandler = new PluginsHandler("plugins");
|
||||||
if (coreService.IsInitialized)
|
if (coreService.IsInitialized)
|
||||||
AutoStartWebServer();
|
AutoStartWebServer();
|
||||||
else
|
else
|
||||||
@ -64,14 +78,9 @@ internal class WebServerService : IWebServerService, IDisposable
|
|||||||
WebServerStarted?.Invoke(this, EventArgs.Empty);
|
WebServerStarted?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WebServerEnabledSettingOnSettingChanged(object? sender, EventArgs e)
|
private void WebServerSettingsOnSettingChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
StartWebServer();
|
_ = StartWebServer();
|
||||||
}
|
|
||||||
|
|
||||||
private void WebServerPortSettingOnSettingChanged(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
StartWebServer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PluginManagementServiceOnPluginFeatureDisabled(object? sender, PluginFeatureEventArgs e)
|
private void PluginManagementServiceOnPluginFeatureDisabled(object? sender, PluginFeatureEventArgs e)
|
||||||
@ -83,76 +92,61 @@ internal class WebServerService : IWebServerService, IDisposable
|
|||||||
_controllers.RemoveAll(c => c.Feature == e.PluginFeature);
|
_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)
|
if (mustRestart)
|
||||||
StartWebServer();
|
_ = StartWebServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
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 PluginsHandler PluginsHandler { get; }
|
||||||
public event EventHandler? WebServerStarting;
|
public event EventHandler? WebServerStarting;
|
||||||
|
|
||||||
|
|
||||||
#region Web server managament
|
#region Web server managament
|
||||||
|
|
||||||
private WebServer CreateWebServer()
|
private async Task<IServer> CreateWebServer()
|
||||||
{
|
{
|
||||||
if (Server != null)
|
if (Server != null)
|
||||||
{
|
{
|
||||||
if (_cts != null)
|
await Server.DisposeAsync();
|
||||||
{
|
|
||||||
_cts.Cancel();
|
|
||||||
_cts = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.Dispose();
|
|
||||||
OnWebServerStopped();
|
OnWebServerStopped();
|
||||||
Server = null;
|
Server = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LayoutBuilder serverLayout = Layout.Create()
|
||||||
|
.Add(PluginsHandler)
|
||||||
|
.Add(new StatusHandler())
|
||||||
|
.Add(CorsPolicy.Permissive());
|
||||||
|
|
||||||
WebApiModule apiModule = new("/", SystemTextJsonSerializer);
|
// Add registered controllers to the API module as services.
|
||||||
PluginsModule.ServerUrl = $"http://localhost:{_webServerPortSetting.Value}/";
|
// GenHTTP also has controllers but services are more flexible and match EmbedIO's approach more closely.
|
||||||
WebServer server = new WebServer(o => o.WithUrlPrefix($"http://*:{_webServerPortSetting.Value}/").WithMode(HttpListenerMode.EmbedIO))
|
SerializationBuilder serialization = Serialization.Default(JsonOptions);
|
||||||
.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);
|
|
||||||
|
|
||||||
// Add registered controllers to the API module
|
|
||||||
foreach (WebApiControllerRegistration registration in _controllers)
|
foreach (WebApiControllerRegistration registration in _controllers)
|
||||||
apiModule.RegisterController(registration.ControllerType, (Func<WebApiController>) registration.UntypedFactory);
|
{
|
||||||
|
serverLayout = serverLayout.AddService(registration.Path, registration.Factory(), serializers: serialization);
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for state changes.
|
IServer server = Host.Create()
|
||||||
server.StateChanged += (s, e) => _logger.Verbose("WebServer new state - {state}", e.NewState);
|
.Handler(serverLayout.Build())
|
||||||
|
.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
|
// 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"), $"http://localhost:{_webServerPortSetting.Value}/");
|
||||||
|
|
||||||
return server;
|
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
|
// Don't create the webserver until after the core service is initialized, this avoids lots of useless re-creates during initialize
|
||||||
if (!_coreService.IsInitialized)
|
if (!_coreService.IsInitialized)
|
||||||
@ -161,7 +155,7 @@ internal class WebServerService : IWebServerService, IDisposable
|
|||||||
if (!_webServerEnabledSetting.Value)
|
if (!_webServerEnabledSetting.Value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Server = CreateWebServer();
|
Server = await CreateWebServer();
|
||||||
|
|
||||||
if (Constants.StartupArguments.Contains("--disable-webserver"))
|
if (Constants.StartupArguments.Contains("--disable-webserver"))
|
||||||
{
|
{
|
||||||
@ -170,17 +164,29 @@ internal class WebServerService : IWebServerService, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
OnWebServerStarting();
|
OnWebServerStarting();
|
||||||
_cts = new CancellationTokenSource();
|
try
|
||||||
Server.Start(_cts.Token);
|
{
|
||||||
|
await Server.StartAsync();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Warning(e, "Failed to start webserver");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
OnWebServerStarted();
|
OnWebServerStarted();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_webserverSemaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AutoStartWebServer()
|
private async Task AutoStartWebServer()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
StartWebServer();
|
await StartWebServer();
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@ -197,8 +203,8 @@ internal class WebServerService : IWebServerService, IDisposable
|
|||||||
if (feature == null) throw new ArgumentNullException(nameof(feature));
|
if (feature == null) throw new ArgumentNullException(nameof(feature));
|
||||||
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
|
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
|
||||||
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
|
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
|
||||||
JsonPluginEndPoint<T> endPoint = new(feature, endPointName, PluginsModule, requestHandler);
|
JsonPluginEndPoint<T> endPoint = new(feature, endPointName, PluginsHandler, requestHandler);
|
||||||
PluginsModule.AddPluginEndPoint(endPoint);
|
PluginsHandler.AddPluginEndPoint(endPoint);
|
||||||
return endPoint;
|
return endPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,8 +213,8 @@ internal class WebServerService : IWebServerService, IDisposable
|
|||||||
if (feature == null) throw new ArgumentNullException(nameof(feature));
|
if (feature == null) throw new ArgumentNullException(nameof(feature));
|
||||||
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
|
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
|
||||||
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
|
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
|
||||||
JsonPluginEndPoint<T> endPoint = new(feature, endPointName, PluginsModule, requestHandler);
|
JsonPluginEndPoint<T> endPoint = new(feature, endPointName, PluginsHandler, requestHandler);
|
||||||
PluginsModule.AddPluginEndPoint(endPoint);
|
PluginsHandler.AddPluginEndPoint(endPoint);
|
||||||
return endPoint;
|
return endPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,8 +223,8 @@ internal class WebServerService : IWebServerService, IDisposable
|
|||||||
if (feature == null) throw new ArgumentNullException(nameof(feature));
|
if (feature == null) throw new ArgumentNullException(nameof(feature));
|
||||||
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
|
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
|
||||||
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
|
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
|
||||||
StringPluginEndPoint endPoint = new(feature, endPointName, PluginsModule, requestHandler);
|
StringPluginEndPoint endPoint = new(feature, endPointName, PluginsHandler, requestHandler);
|
||||||
PluginsModule.AddPluginEndPoint(endPoint);
|
PluginsHandler.AddPluginEndPoint(endPoint);
|
||||||
return endPoint;
|
return endPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,47 +233,37 @@ internal class WebServerService : IWebServerService, IDisposable
|
|||||||
if (feature == null) throw new ArgumentNullException(nameof(feature));
|
if (feature == null) throw new ArgumentNullException(nameof(feature));
|
||||||
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
|
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
|
||||||
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
|
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
|
||||||
StringPluginEndPoint endPoint = new(feature, endPointName, PluginsModule, requestHandler);
|
StringPluginEndPoint endPoint = new(feature, endPointName, PluginsHandler, requestHandler);
|
||||||
PluginsModule.AddPluginEndPoint(endPoint);
|
PluginsHandler.AddPluginEndPoint(endPoint);
|
||||||
return endPoint;
|
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 (feature == null) throw new ArgumentNullException(nameof(feature));
|
||||||
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
|
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
|
||||||
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
|
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
|
||||||
RawPluginEndPoint endPoint = new(feature, endPointName, PluginsModule, requestHandler);
|
RawPluginEndPoint endPoint = new(feature, endPointName, PluginsHandler, requestHandler);
|
||||||
PluginsModule.AddPluginEndPoint(endPoint);
|
PluginsHandler.AddPluginEndPoint(endPoint);
|
||||||
return endPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Obsolete("Use AddJsonEndPoint<T>(PluginFeature feature, string endPointName, Action<T> requestHandler) instead")]
|
|
||||||
public DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(Module<T> module, string endPointName) where T : DataModel, new()
|
|
||||||
{
|
|
||||||
if (module == null) throw new ArgumentNullException(nameof(module));
|
|
||||||
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
|
|
||||||
DataModelJsonPluginEndPoint<T> endPoint = new(module, endPointName, PluginsModule);
|
|
||||||
PluginsModule.AddPluginEndPoint(endPoint);
|
|
||||||
return endPoint;
|
return endPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemovePluginEndPoint(PluginEndPoint endPoint)
|
public void RemovePluginEndPoint(PluginEndPoint endPoint)
|
||||||
{
|
{
|
||||||
PluginsModule.RemovePluginEndPoint(endPoint);
|
PluginsHandler.RemovePluginEndPoint(endPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Controller management
|
#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));
|
if (feature == null) throw new ArgumentNullException(nameof(feature));
|
||||||
|
|
||||||
WebApiControllerRegistration<T> registration = new(this, feature);
|
WebApiControllerRegistration<T> registration = new(this, feature, path);
|
||||||
_controllers.Add(registration);
|
_controllers.Add(registration);
|
||||||
StartWebServer();
|
_ = StartWebServer();
|
||||||
|
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
@ -275,75 +271,7 @@ internal class WebServerService : IWebServerService, IDisposable
|
|||||||
public void RemoveController(WebApiControllerRegistration registration)
|
public void RemoveController(WebApiControllerRegistration registration)
|
||||||
{
|
{
|
||||||
_controllers.Remove(registration);
|
_controllers.Remove(registration);
|
||||||
StartWebServer();
|
_ = 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@ -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
|
/// Gets a boolean indicating whether the node is a default node that connot be removed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsDefaultNode { get; }
|
bool IsDefaultNode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a boolean indicating whether the node is currently loading, use this to disable pin type changes etc.
|
||||||
|
/// </summary>
|
||||||
|
bool IsLoading { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the X-position of the node
|
/// Gets or sets the X-position of the node
|
||||||
|
|||||||
@ -46,6 +46,7 @@ public class NodeData
|
|||||||
|
|
||||||
if (entity != null)
|
if (entity != null)
|
||||||
{
|
{
|
||||||
|
node.IsLoading = true;
|
||||||
node.X = entity.X;
|
node.X = entity.X;
|
||||||
node.Y = entity.Y;
|
node.Y = entity.Y;
|
||||||
try
|
try
|
||||||
|
|||||||
@ -206,6 +206,8 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
|
|||||||
}
|
}
|
||||||
|
|
||||||
LoadConnections();
|
LoadConnections();
|
||||||
|
foreach (INode node in Nodes)
|
||||||
|
node.IsLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void LoadFromEntity(NodeScriptEntity entity)
|
internal void LoadFromEntity(NodeScriptEntity entity)
|
||||||
@ -216,6 +218,7 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
|
|||||||
|
|
||||||
private void LoadExistingNode(INode node, NodeEntity nodeEntity)
|
private void LoadExistingNode(INode node, NodeEntity nodeEntity)
|
||||||
{
|
{
|
||||||
|
node.IsLoading = true;
|
||||||
node.Id = nodeEntity.Id;
|
node.Id = nodeEntity.Id;
|
||||||
node.X = nodeEntity.X;
|
node.X = nodeEntity.X;
|
||||||
node.Y = nodeEntity.Y;
|
node.Y = nodeEntity.Y;
|
||||||
|
|||||||
@ -92,6 +92,9 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual bool IsDefaultNode => false;
|
public virtual bool IsDefaultNode => false;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsLoading { get; set; }
|
||||||
|
|
||||||
private readonly List<IPin> _pins = new();
|
private readonly List<IPin> _pins = new();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -1,20 +1,24 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace Artemis.UI.Linux.Providers.Input;
|
namespace Artemis.UI.Linux.Providers.Input;
|
||||||
|
|
||||||
internal class LinuxInputDeviceReader
|
internal class LinuxInputDeviceReader
|
||||||
{
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
private readonly byte[] _buffer;
|
private readonly byte[] _buffer;
|
||||||
private readonly CancellationTokenSource _cts;
|
private readonly CancellationTokenSource _cts;
|
||||||
private readonly FileStream _stream;
|
private readonly FileStream _stream;
|
||||||
private readonly Task _task;
|
private readonly Task _task;
|
||||||
|
|
||||||
public LinuxInputDeviceReader(LinuxInputDevice inputDevice)
|
public LinuxInputDeviceReader(LinuxInputDevice inputDevice, ILogger logger)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
InputDevice = inputDevice;
|
InputDevice = inputDevice;
|
||||||
|
|
||||||
_buffer = new byte[Marshal.SizeOf<LinuxInputEventArgs>()];
|
_buffer = new byte[Marshal.SizeOf<LinuxInputEventArgs>()];
|
||||||
@ -50,9 +54,10 @@ internal class LinuxInputDeviceReader
|
|||||||
|
|
||||||
InputEvent?.Invoke(this, MemoryMarshal.Read<LinuxInputEventArgs>(_buffer));
|
InputEvent?.Invoke(this, MemoryMarshal.Read<LinuxInputEventArgs>(_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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ public class LinuxInputProvider : InputProvider
|
|||||||
|
|
||||||
foreach (LinuxInputDevice deviceDefinition in LinuxInputDeviceFinder.Find())
|
foreach (LinuxInputDevice deviceDefinition in LinuxInputDeviceFinder.Find())
|
||||||
{
|
{
|
||||||
LinuxInputDeviceReader? reader = new(deviceDefinition);
|
LinuxInputDeviceReader? reader = new(deviceDefinition, logger);
|
||||||
reader.InputEvent += OnInputEvent;
|
reader.InputEvent += OnInputEvent;
|
||||||
_readers.Add(reader);
|
_readers.Add(reader);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services.MainWindow;
|
using Artemis.UI.Shared.Services.MainWindow;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using EmbedIO;
|
using GenHTTP.Api.Protocol;
|
||||||
using EmbedIO.Routing;
|
using GenHTTP.Modules.Webservices;
|
||||||
using EmbedIO.WebApi;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Controllers;
|
namespace Artemis.UI.Controllers;
|
||||||
|
|
||||||
public class RemoteController : WebApiController
|
public class RemoteController
|
||||||
{
|
{
|
||||||
private readonly ICoreService _coreService;
|
private readonly ICoreService _coreService;
|
||||||
private readonly IMainWindowService _mainWindowService;
|
private readonly IMainWindowService _mainWindowService;
|
||||||
@ -24,17 +24,17 @@ public class RemoteController : WebApiController
|
|||||||
_router = router;
|
_router = router;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route(HttpVerbs.Any, "/status")]
|
[ResourceMethod(RequestMethod.Post, "bring-to-foreground")]
|
||||||
public void GetStatus()
|
public void BringToForeground(IRequest request)
|
||||||
{
|
{
|
||||||
HttpContext.Response.StatusCode = 200;
|
// Get the route from the request content stream
|
||||||
}
|
// TODO: Use [FromBody] attribute instead once GenHTTP allows omitting null values
|
||||||
|
string? route = null;
|
||||||
[Route(HttpVerbs.Post, "/remote/bring-to-foreground")]
|
if (request.Content != null)
|
||||||
public void PostBringToForeground()
|
{
|
||||||
{
|
using StreamReader reader = new(request.Content);
|
||||||
using StreamReader reader = new(Request.InputStream);
|
route = reader.ReadToEnd();
|
||||||
string route = reader.ReadToEnd();
|
}
|
||||||
|
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
@ -44,14 +44,14 @@ public class RemoteController : WebApiController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route(HttpVerbs.Post, "/remote/restart")]
|
[ResourceMethod(RequestMethod.Post, "restart")]
|
||||||
public void PostRestart([FormField] string[] args)
|
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")]
|
[ResourceMethod(RequestMethod.Post, "shutdown")]
|
||||||
public void PostShutdown()
|
public void Shutdown()
|
||||||
{
|
{
|
||||||
Utilities.Shutdown();
|
Utilities.Shutdown();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
x:DataType="propertyInput:BrushPropertyInputViewModel">
|
x:DataType="propertyInput:BrushPropertyInputViewModel">
|
||||||
|
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Style Selector="ComboBox.brush /template/ ContentControl#ContentPresenter">
|
<Style Selector="ComboBox.brush /template/ ContentPresenter#ContentPresenter">
|
||||||
<Setter Property="ContentTemplate">
|
<Setter Property="ContentTemplate">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<DataTemplate DataType="{x:Type layerBrushes:LayerBrushDescriptor}">
|
<DataTemplate DataType="{x:Type layerBrushes:LayerBrushDescriptor}">
|
||||||
|
|||||||
@ -65,7 +65,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
|
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
|
||||||
<StackPanel.Styles>
|
<StackPanel.Styles>
|
||||||
<Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
|
<Style Selector="ComboBox.layoutProvider /template/ ContentPresenter#ContentPresenter">
|
||||||
<Setter Property="ContentTemplate">
|
<Setter Property="ContentTemplate">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
|
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Spacing="5">
|
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Spacing="5">
|
||||||
<StackPanel.Styles>
|
<StackPanel.Styles>
|
||||||
<Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
|
<Style Selector="ComboBox.layoutProvider /template/ ContentPresenter#ContentPresenter">
|
||||||
<Setter Property="ContentTemplate">
|
<Setter Property="ContentTemplate">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<DataTemplate x:DataType="models:InstalledEntry">
|
<DataTemplate x:DataType="models:InstalledEntry">
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
<Border Grid.Column="1" BorderBrush="{DynamicResource ButtonBorderBrush}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0">
|
<Border Grid.Column="1" BorderBrush="{DynamicResource ButtonBorderBrush}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0">
|
||||||
<Grid RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,*">
|
||||||
<TextBlock Classes="h5">Plugin features</TextBlock>
|
<TextBlock Classes="h5">Plugin features</TextBlock>
|
||||||
<ListBox Grid.Row="1" MaxHeight="135" ItemsSource="{CompiledBinding PluginFeatures}" />
|
<ListBox Grid.Row="1" ItemsSource="{CompiledBinding PluginFeatures}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.DisplayConditionScriptView"
|
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.DisplayConditionScriptView"
|
||||||
x:DataType="displayCondition:DisplayConditionScriptViewModel">
|
x:DataType="displayCondition:DisplayConditionScriptViewModel">
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Style Selector="ComboBox.condition-type /template/ ContentControl#ContentPresenter">
|
<Style Selector="ComboBox.condition-type /template/ ContentPresenter#ContentPresenter">
|
||||||
<Setter Property="ContentTemplate">
|
<Setter Property="ContentTemplate">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<DataTemplate DataType="{x:Type displayCondition:ConditionTypeViewModel}">
|
<DataTemplate DataType="{x:Type displayCondition:ConditionTypeViewModel}">
|
||||||
@ -35,7 +35,13 @@
|
|||||||
<TextBlock Text="{CompiledBinding Description}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" TextWrapping="Wrap" MaxWidth="350" />
|
<TextBlock Text="{CompiledBinding Description}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" TextWrapping="Wrap" MaxWidth="350" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
</ComboBox.ItemTemplate>
|
</ComboBox.ItemTemplate>
|
||||||
|
<ComboBox.SelectionBoxItemTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type displayCondition:ConditionTypeViewModel}">
|
||||||
|
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.SelectionBoxItemTemplate>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
<ContentControl Grid.Row="2" Content="{CompiledBinding ConditionViewModel}" />
|
<ContentControl Grid.Row="2" Content="{CompiledBinding ConditionViewModel}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.UI.Routing;
|
using Artemis.UI.Routing;
|
||||||
@ -42,14 +43,11 @@ public partial class SettingsViewModel : RoutableHostScreen<RoutableScreen>, IMa
|
|||||||
public ViewModelBase? TitleBarViewModel => null;
|
public ViewModelBase? TitleBarViewModel => null;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
public override Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Display tab change on navigate
|
// Display tab change on navigate, if there is none forward to the first
|
||||||
SelectedTab = SettingTabs.FirstOrDefault(t => t.Matches(args.Path));
|
SelectedTab = SettingTabs.FirstOrDefault(t => t.Matches(args.Path)) ?? SettingTabs.FirstOrDefault();
|
||||||
|
return Task.CompletedTask;
|
||||||
// Always show a tab, if there is none forward to the first
|
|
||||||
if (SelectedTab == null)
|
|
||||||
await _router.Navigate(SettingTabs.First().Path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GoBack()
|
public void GoBack()
|
||||||
|
|||||||
@ -145,6 +145,22 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Border Classes="card-separator" />
|
<Border Classes="card-separator" />
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock>Enable remote access</TextBlock>
|
||||||
|
<TextBlock Classes="subtitle" TextWrapping="Wrap">
|
||||||
|
By default the web server can only be accessed by applications running on your own computer, e.g. supported games.
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Classes="subtitle warning" TextWrapping="Wrap">
|
||||||
|
Enabling remote access allows you to access Artemis from other devices on your network, depending on your router even the outside world.
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||||
|
<ToggleSwitch IsChecked="{CompiledBinding WebServerRemoteAccess.Value}" OnContent="Yes" OffContent="No" MinWidth="0" Margin="0 -10" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<Border Classes="card-separator" />
|
||||||
|
|
||||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||||
<StackPanel Grid.Column="0">
|
<StackPanel Grid.Column="0">
|
||||||
@ -267,7 +283,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||||
<StackPanel.Styles>
|
<StackPanel.Styles>
|
||||||
<Style Selector="ComboBox.brush /template/ ContentControl#ContentPresenter">
|
<Style Selector="ComboBox.brush /template/ ContentPresenter#ContentPresenter">
|
||||||
<Setter Property="ContentTemplate">
|
<Setter Property="ContentTemplate">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<DataTemplate DataType="{x:Type layerBrushes:LayerBrushDescriptor}">
|
<DataTemplate DataType="{x:Type layerBrushes:LayerBrushDescriptor}">
|
||||||
|
|||||||
@ -167,6 +167,7 @@ public class GeneralTabViewModel : RoutableScreen
|
|||||||
public PluginSetting<double> CoreRenderScale => _settingsService.GetSetting("Core.RenderScale", 0.5);
|
public PluginSetting<double> CoreRenderScale => _settingsService.GetSetting("Core.RenderScale", 0.5);
|
||||||
public PluginSetting<int> CoreTargetFrameRate => _settingsService.GetSetting("Core.TargetFrameRate", 30);
|
public PluginSetting<int> CoreTargetFrameRate => _settingsService.GetSetting("Core.TargetFrameRate", 30);
|
||||||
public PluginSetting<bool> WebServerEnabled => _settingsService.GetSetting("WebServer.Enabled", true);
|
public PluginSetting<bool> WebServerEnabled => _settingsService.GetSetting("WebServer.Enabled", true);
|
||||||
|
public PluginSetting<bool> WebServerRemoteAccess => _settingsService.GetSetting("WebServer.RemoteAccess", false);
|
||||||
public PluginSetting<int> WebServerPort => _settingsService.GetSetting("WebServer.Port", 9696);
|
public PluginSetting<int> WebServerPort => _settingsService.GetSetting("WebServer.Port", 9696);
|
||||||
|
|
||||||
private void ExecuteShowLogs()
|
private void ExecuteShowLogs()
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="plugins:PluginSettingsViewModel">
|
<DataTemplate x:DataType="plugins:PluginSettingsViewModel">
|
||||||
<ContentControl Content="{CompiledBinding}" Height="200" />
|
<ContentControl Content="{CompiledBinding}" MinHeight="200" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|||||||
@ -112,8 +112,7 @@ public partial class PluginsTabViewModel : RoutableScreen
|
|||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
return _ => true;
|
return _ => true;
|
||||||
|
|
||||||
return data => data.Info.Name.Contains(text, StringComparison.InvariantCultureIgnoreCase) ||
|
return data => data.Info.MatchesSearch(text) || data.Features.Any(f => f.MatchesSearch(text));
|
||||||
(data.Info.Description != null && data.Info.Description.Contains(text, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task GetMorePlugins()
|
public async Task GetMorePlugins()
|
||||||
|
|||||||
@ -62,7 +62,7 @@ public partial class ReleasesTabViewModel : RoutableHostScreen<ReleaseDetailsVie
|
|||||||
|
|
||||||
public ReadOnlyObservableCollection<ReleaseViewModel> ReleaseViewModels { get; }
|
public ReadOnlyObservableCollection<ReleaseViewModel> ReleaseViewModels { get; }
|
||||||
public string Channel { get; }
|
public string Channel { get; }
|
||||||
|
|
||||||
public async Task GetReleases(CancellationToken cancellationToken)
|
public async Task GetReleases(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -97,7 +97,7 @@ public partial class ReleasesTabViewModel : RoutableHostScreen<ReleaseDetailsVie
|
|||||||
Loading = false;
|
Loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@ -109,10 +109,6 @@ public partial class ReleasesTabViewModel : RoutableHostScreen<ReleaseDetailsVie
|
|||||||
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(vm => vm.Release.Id == releaseId);
|
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(vm => vm.Release.Id == releaseId);
|
||||||
// Otherwise forward to the last release
|
// Otherwise forward to the last release
|
||||||
else
|
else
|
||||||
{
|
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(r => r.IsCurrentVersion) ?? ReleaseViewModels.FirstOrDefault();
|
||||||
ReleaseViewModel? lastRelease = ReleaseViewModels.FirstOrDefault(r => r.IsCurrentVersion) ?? ReleaseViewModels.FirstOrDefault();
|
|
||||||
if (lastRelease != null)
|
|
||||||
await _router.Navigate($"settings/releases/{lastRelease.Release.Id}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Screens.StartupWizard.Steps;
|
using Artemis.UI.Screens.StartupWizard.Steps;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
@ -11,11 +12,13 @@ namespace Artemis.UI.Screens.StartupWizard;
|
|||||||
public partial class StartupWizardViewModel : DialogViewModelBase<bool>
|
public partial class StartupWizardViewModel : DialogViewModelBase<bool>
|
||||||
{
|
{
|
||||||
private readonly IContainer _container;
|
private readonly IContainer _container;
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
[Notify] private WizardStepViewModel _screen;
|
[Notify] private WizardStepViewModel _screen;
|
||||||
|
|
||||||
public StartupWizardViewModel(IContainer container, IWindowService windowService)
|
public StartupWizardViewModel(IContainer container, IWindowService windowService, ISettingsService settingsService)
|
||||||
{
|
{
|
||||||
_container = container;
|
_container = container;
|
||||||
|
_settingsService = settingsService;
|
||||||
_screen = _container.Resolve<WelcomeStepViewModel>();
|
_screen = _container.Resolve<WelcomeStepViewModel>();
|
||||||
_screen.Wizard = this;
|
_screen.Wizard = this;
|
||||||
|
|
||||||
@ -41,6 +44,10 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
|
|||||||
|
|
||||||
public void SkipOrFinishWizard()
|
public void SkipOrFinishWizard()
|
||||||
{
|
{
|
||||||
|
PluginSetting<bool> setting = _settingsService.GetSetting("UI.SetupWizardCompleted", false);
|
||||||
|
setting.Value = true;
|
||||||
|
setting.Save();
|
||||||
|
|
||||||
Close(true);
|
Close(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,6 +94,6 @@ public class RegistrationService : IRegistrationService
|
|||||||
|
|
||||||
public void RegisterControllers()
|
public void RegisterControllers()
|
||||||
{
|
{
|
||||||
_webServerService.AddController<RemoteController>(Constants.CorePlugin.Features.First().Instance!);
|
_webServerService.AddController<RemoteController>(Constants.CorePlugin.Features.First().Instance!, "remote");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,6 +38,14 @@
|
|||||||
<Setter Property="VerticalScrollBarVisibility" Value="Disabled"></Setter>
|
<Setter Property="VerticalScrollBarVisibility" Value="Disabled"></Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Workaround for https://github.com/amwx/FluentAvalonia/issues/648 -->
|
||||||
|
<Style Selector="controls|FontIcon">
|
||||||
|
<Setter Property="Width" Value="24" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="controls|SymbolIcon">
|
||||||
|
<Setter Property="Width" Value="24" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Styles.Resources>
|
<Styles.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<FontFamily x:Key="RobotoMono">avares://Artemis.UI/Assets/Fonts#Roboto Mono</FontFamily>
|
<FontFamily x:Key="RobotoMono">avares://Artemis.UI/Assets/Fonts#Roboto Mono</FontFamily>
|
||||||
|
|||||||
@ -14,6 +14,8 @@
|
|||||||
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.2.3" />
|
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.2.3" />
|
||||||
<PackageVersion Include="Avalonia.Skia.Lottie" Version="11.0.0" />
|
<PackageVersion Include="Avalonia.Skia.Lottie" Version="11.0.0" />
|
||||||
<PackageVersion Include="Avalonia.Win32" Version="11.2.3" />
|
<PackageVersion Include="Avalonia.Win32" Version="11.2.3" />
|
||||||
|
<PackageVersion Include="GenHTTP.Core" Version="9.6.2" />
|
||||||
|
<PackageVersion Include="GenHTTP.Modules.Webservices" Version="9.6.2" />
|
||||||
<PackageVersion Include="HPPH.SkiaSharp" Version="1.0.0" />
|
<PackageVersion Include="HPPH.SkiaSharp" Version="1.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.1" />
|
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.1" />
|
||||||
<PackageVersion Include="Avalonia.Xaml.Behaviors" Version="11.2.0.8" />
|
<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.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||||
<PackageVersion Include="DryIoc.dll" Version="5.4.3" />
|
<PackageVersion Include="DryIoc.dll" Version="5.4.3" />
|
||||||
<PackageVersion Include="DynamicData" Version="9.1.1" />
|
<PackageVersion Include="DynamicData" Version="9.1.1" />
|
||||||
<PackageVersion Include="EmbedIO" Version="3.5.2" />
|
|
||||||
<PackageVersion Include="FluentAvalonia.ProgressRing" Version="1.69.2" />
|
<PackageVersion Include="FluentAvalonia.ProgressRing" Version="1.69.2" />
|
||||||
<PackageVersion Include="FluentAvaloniaUI" Version="2.2.0" />
|
<PackageVersion Include="FluentAvaloniaUI" Version="2.2.0" />
|
||||||
<PackageVersion Include="HidSharp" Version="2.1.0" />
|
<PackageVersion Include="HidSharp" Version="2.1.0" />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user