Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public sealed class MockedFlowExecutorApiFixture : WebApplicationFactory<CtrlApi
{
public MockedFlowExecutorApiFixture()
{
WebApplicationWrapper<IApiService>.ModifyJsonSerializerOptions(SystemTextJsonSerializerConfig.Options);
WebApplicationWrapper<IApiService>.ModifyJsonSerializerOptions(
SystemTextJsonSerializerConfig.Options
);
}

public ValueTask InitializeAsync() => ValueTask.CompletedTask;
Expand Down
2 changes: 1 addition & 1 deletion Oma.WndwCtrl.Abstractions/Errors/FlowError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public FlowError(string message, bool isExceptional) : this(message, isException

[System.Diagnostics.Contracts.Pure]
public static FlowError NoCommandExecutorFound(ICommand command) => new(
$"No transformation executor found that handles transformation type {command.GetType().FullName}.",
$"No command executor found that handles transformation type {command.GetType().FullName}.",
isExceptional: false
);

Expand Down
6 changes: 5 additions & 1 deletion Oma.WndwCtrl.Api/CtrlApiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ public async static Task Main(string[] args)
MessageBus = new NoOpMessageBus(),
};

IConfiguration configuration = new ConfigurationBuilder()
.Build();

CtrlApiService apiService = new(
await ComponentConfigurationAccessor.FromFileAsync(),
messageBusAccessor
messageBusAccessor,
configuration
);

await apiService.StartAsync(CancellationToken.None, args);
Expand Down
5 changes: 3 additions & 2 deletions Oma.WndwCtrl.Api/CtrlApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ namespace Oma.WndwCtrl.Api;

public class CtrlApiService(
ComponentConfigurationAccessor configurationAccessor,
MessageBusAccessor messageBusAccessor
MessageBusAccessor messageBusAccessor,
IConfiguration rootConfiguration
)
: WebApplicationWrapper<CtrlApiService>(messageBusAccessor)
: WebApplicationWrapper<CtrlApiService>(messageBusAccessor, rootConfiguration)
{
private readonly MessageBusAccessor _messageBusAccessor = messageBusAccessor;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"oma-service-status": {
"type": "sensor",
"active": false,
"active": true,
"queryCommand": {
"type": "cli",
"fileName": "sc.exe",
Expand Down Expand Up @@ -65,6 +65,7 @@
},
"ping-google": {
"type": "sensor",
"active": false,
"queryCommand": {
"type": "cli",
"fileName": "ping",
Expand Down
40 changes: 40 additions & 0 deletions Oma.WndwCtrl.Core/Extensions/IServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Oma.WndwCtrl.Abstractions;
Expand All @@ -13,6 +15,7 @@
using Oma.WndwCtrl.Core.Logger;
using Oma.WndwCtrl.Core.Metrics;
using Oma.WndwCtrl.Core.Model;
using Oma.WndwCtrl.Core.Model.Settings;

namespace Oma.WndwCtrl.Core.Extensions;

Expand All @@ -24,6 +27,8 @@ public static IServiceCollection AddCommandExecutors(
IConfiguration configuration
)
{
services.AddOptions();

IConfigurationSection coreConfig = configuration.GetSection("Core");

// TODO: Will cause problems when called multiple times.
Expand All @@ -35,6 +40,13 @@ IConfiguration configuration
coreConfig.GetSection(CliParserLoggerOptions.SectionName)
);

ExtensionSettings extensions = [];
coreConfig.GetSection(ExtensionSettings.SectionName).Bind(extensions);

List<Assembly> extensionAssemblies = extensions.GetAssemblies();
JsonExtensions.AddAssembliesToLoad(extensionAssemblies.ToList());
AddExtensionExecutors(services, extensionAssemblies);

services
.AddSingleton<IAcaadCoreMetrics, AcaadCoreMetrics>()
.AddSingleton<IExpressionCache, ExpressionCache>()
Expand All @@ -47,4 +59,32 @@ IConfiguration configuration

return services;
}

private static void AddExtensionExecutors(
IServiceCollection services,
List<Assembly> extensionAssemblies
)
{
foreach (Assembly assembly in extensionAssemblies)
{
AddAllFromAssembly<ICommandExecutor>(services, assembly);
AddAllFromAssembly<IOutcomeTransformer>(services, assembly);
}
}

[PublicAPI]
public static IServiceCollection AddAllFromAssembly<T>(
IServiceCollection services,
Assembly assembly,
ServiceLifetime serviceLifetime = ServiceLifetime.Scoped
)
{
Type baseType = typeof(T);

foreach (Type implType in assembly.GetTypes()
.Where(t => t is { IsAbstract: false, } && t.IsAssignableTo(baseType)))
services.Add(new ServiceDescriptor(baseType, implType, serviceLifetime));

return services;
}
}
16 changes: 14 additions & 2 deletions Oma.WndwCtrl.Core/Extensions/JsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,24 @@ namespace Oma.WndwCtrl.Core.Extensions;

public static class JsonExtensions
{
private static readonly Assembly AssembliesToSearch = typeof(BaseTransformation).Assembly;
private static IEnumerable<Assembly> _assembliesToInspect = [];

internal static void AddAssembliesToLoad(List<Assembly> assembliesToLoad)
{
if (assembliesToLoad.Count == 0)
{
return;
}

_assembliesToInspect = _assembliesToInspect.Union(assembliesToLoad).Distinct();
}

public static Action<JsonTypeInfo> GetPolymorphismModifierFor<T>(
Func<Type, string> typeToDiscriminatorTransform
)
{
List<Assembly> assembliesToSearch = [.._assembliesToInspect, typeof(BaseTransformation).Assembly,];

return jsonTypeInfo =>
{
Type baseType = typeof(T);
Expand All @@ -29,7 +41,7 @@ Func<Type, string> typeToDiscriminatorTransform
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
};

IEnumerable<Type> types = AssembliesToSearch.GetTypes()
IEnumerable<Type> types = assembliesToSearch.SelectMany(a => a.GetTypes())
.Where(t => t is { IsClass: true, IsAbstract: false, } && t.IsAssignableTo(baseType));

foreach (Type t in types)
Expand Down
10 changes: 10 additions & 0 deletions Oma.WndwCtrl.Core/Model/Settings/AssemblyInfo2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using JetBrains.Annotations;

namespace Oma.WndwCtrl.Core.Model.Settings;

[UsedImplicitly]
[PublicAPI]
public record AssemblyInfo2
{
public string AssemblyName { get; set; } = string.Empty;
}
14 changes: 14 additions & 0 deletions Oma.WndwCtrl.Core/Model/Settings/ExtensionSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Reflection;

namespace Oma.WndwCtrl.Core.Model.Settings;

public class ExtensionSettings : List<AssemblyInfo2>
{
public const string SectionName = "Extensions";

public List<Assembly> GetAssemblies()
{
// TODO: [Required for MVP] Obvious security concerns..
return this.Select(assemblyInfo => Assembly.LoadFrom($"{assemblyInfo.AssemblyName}")).ToList();
}
}
10 changes: 10 additions & 0 deletions Oma.WndwCtrl.CoreAsp/BackgroundServiceWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@ public class BackgroundServiceWrapper<TAssemblyDescriptor>(IConfiguration config

protected readonly string RunningInOs = configuration.GetValue<string>("ACaaD:OS") ?? "windows";

private IConfiguration? _configuration;

protected static IServiceProvider ServiceProvider => _serviceProvider
?? throw new InvalidOperationException(
"The WebApplicationWrapper has not been initialized properly."
);

protected IConfiguration Configuration
{
get => _configuration ??
throw new InvalidOperationException($"{nameof(Configuration)} is not populated.");
private set => _configuration = value;
}

[PublicAPI]
protected IHost? Host { get; private set; }

Expand All @@ -37,6 +46,7 @@ public async Task StartAsync(CancellationToken cancelToken = default, params str
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
#endif

Configuration = configuration;
HostBuilder hostBuilder = new();

hostBuilder.ConfigureHostConfiguration(builder => builder.AddConfiguration(configuration));
Expand Down
33 changes: 28 additions & 5 deletions Oma.WndwCtrl.CoreAsp/WebApplicationWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Extensions.FileProviders;
using Oma.WndwCtrl.Abstractions;
using Oma.WndwCtrl.Core.Extensions;
using Oma.WndwCtrl.Core.Model.Settings;
using Oma.WndwCtrl.CoreAsp.Api.Filters;
using Oma.WndwCtrl.CoreAsp.Conventions;
using Oma.WndwCtrl.Messaging.Bus;
Expand All @@ -14,7 +15,10 @@

namespace Oma.WndwCtrl.CoreAsp;

public class WebApplicationWrapper<TAssemblyDescriptor>(MessageBusAccessor? messageBusAccessor) : IApiService
public class WebApplicationWrapper<TAssemblyDescriptor>(
MessageBusAccessor? messageBusAccessor,
IConfiguration? rootConfiguration
) : IApiService
where TAssemblyDescriptor : class, IApiService
{
[SuppressMessage(
Expand All @@ -24,6 +28,8 @@ public class WebApplicationWrapper<TAssemblyDescriptor>(MessageBusAccessor? mess
)]
private static IServiceProvider? _serviceProvider;

private static readonly string _serviceName = typeof(TAssemblyDescriptor).Name;

private IConfiguration? _configuration;

private IWebHostEnvironment? _environment;
Expand Down Expand Up @@ -74,8 +80,20 @@ public async Task StartAsync(CancellationToken cancelToken = default, params str
Environment = builder.Environment;
Configuration = builder.Configuration;

IConfiguration coreConfig = Configuration.GetSection("Core");
ExtensionSettings extensions = [];
coreConfig.GetSection(ExtensionSettings.SectionName).Bind(extensions);

ConfigurationConfiguration(builder.Configuration);

builder.WebHost.ConfigureKestrel(
(context, options) =>
{
IConfiguration configToUse = context.Configuration.GetSection(_serviceName).GetSection("Kestrel");
options.Configure(configToUse);
}
);

IMvcCoreBuilder mvcBuilder = builder.Services
.AddMvcCore(
opts =>
Expand Down Expand Up @@ -154,7 +172,9 @@ public Task ForceStopAsync(CancellationToken cancelToken) => Application is not
? Application.StopAsync(cancelToken)
: Task.CompletedTask;

public static void ModifyJsonSerializerOptions(JsonSerializerOptions jsonSerializerOptions)
public static void ModifyJsonSerializerOptions(
JsonSerializerOptions jsonSerializerOptions
)
{
jsonSerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver()
.WithAddedModifier(
Expand All @@ -179,7 +199,10 @@ protected virtual IConfigurationBuilder ConfigurationConfiguration(
IConfigurationBuilder configurationBuilder
)
{
string serviceName = typeof(TAssemblyDescriptor).Name;
if (rootConfiguration is not null)
{
configurationBuilder.AddConfiguration(rootConfiguration);
}

IFileProvider standardFileProvider = configurationBuilder.GetFileProvider();

Expand All @@ -190,12 +213,12 @@ IConfigurationBuilder configurationBuilder

configurationBuilder.SetFileProvider(compositeFileProvider);

configurationBuilder.AddJsonFile($"{serviceName}.config.json", optional: true, reloadOnChange: false);
configurationBuilder.AddJsonFile($"{_serviceName}.config.json", optional: true, reloadOnChange: false);

if (Environment.IsDevelopment())
{
configurationBuilder.AddJsonFile(
$"{serviceName}.config.development.json",
$"{_serviceName}.config.development.json",
optional: true,
reloadOnChange: false
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
using LanguageExt;
using Oma.WndwCtrl.Abstractions;
using Oma.WndwCtrl.Abstractions.Errors;
using Oma.WndwCtrl.Abstractions.Model;
using Oma.WndwCtrl.Ext.Windows.Media.Model.Commands;
using static LanguageExt.Prelude;

namespace Oma.WndwCtrl.Ext.Windows.Media.Executors.Commands;

[UsedImplicitly]
public partial class MediaCommandExecutor : ICommandExecutor<MediaCommand>
{
private const int KEYEVENTF_EXTENTEDKEY = 1;

private const int VK_MEDIA_NEXT_TRACK = 0xB0;
private const int VK_MEDIA_PREV_TRACK = 0xB1;
private const int VK_MEDIA_STOP = 0xB2;
private const int VK_MEDIA_PLAY_PAUSE = 0xB3;

[SuppressMessage(
"Reliability",
"CA2000:Dispose objects before losing scope",
Justification = "Must be disposed by caller."
)]
[MustDisposeResource]
[SuppressMessage("ReSharper", "NotDisposedResource", Justification = "Method flagged as must-dispose.")]
public Task<Either<FlowError, CommandOutcome>> ExecuteAsync(
MediaCommand command,
CancellationToken cancelToken = default
)
{
byte keyToSend = command.Action switch
{
MediaAction.Play or MediaAction.Pause => VK_MEDIA_PLAY_PAUSE,
MediaAction.Next => VK_MEDIA_NEXT_TRACK,
MediaAction.Previous => VK_MEDIA_PREV_TRACK,
MediaAction.Stop => VK_MEDIA_STOP,
var err => throw new InvalidOperationException($"Unhandled media action {err.ToString()}."),
};

keybd_event(keyToSend, scanCode: 0, KEYEVENTF_EXTENTEDKEY, IntPtr.Zero);

return Task.FromResult<Either<FlowError, CommandOutcome>>(
Right(
new CommandOutcome
{
Success = true,
}
)
);
}

[LibraryImport("user32.dll")]
private static partial void keybd_event(byte virtualKey, byte scanCode, uint flags, IntPtr extraInfo);
}
Loading