diff --git a/Moonlight.ApiServer/Http/Controllers/ClientPlugins/ClientPluginsController.cs b/Moonlight.ApiServer/Http/Controllers/ClientPlugins/ClientPluginsController.cs new file mode 100644 index 00000000..76223904 --- /dev/null +++ b/Moonlight.ApiServer/Http/Controllers/ClientPlugins/ClientPluginsController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc; +using Moonlight.ApiServer.Services; +using Moonlight.Shared.Http.Responses.ClientPlugins; + +namespace Moonlight.ApiServer.Http.Controllers.ClientPlugins; + +[ApiController] +[Route("api/clientPlugins")] +public class ClientPluginsController : Controller +{ + private readonly ModuleService ModuleService; + + public ClientPluginsController(ModuleService moduleService) + { + ModuleService = moduleService; + } + + [HttpGet] + public async Task Get() + { + var dlls = ModuleService.Modules + .Where(x => x.Modules.ContainsKey("client")) + .SelectMany(x => + x.Modules + .FirstOrDefault(c => c.Key == "client") + .Value + .Select(y => x.Name + "." + y) + ) + .ToArray(); + + return new() + { + CacheKey = "unset", + Dlls = dlls + }; + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Models/ModuleModel.cs b/Moonlight.ApiServer/Models/ModuleModel.cs new file mode 100644 index 00000000..c4e3fde2 --- /dev/null +++ b/Moonlight.ApiServer/Models/ModuleModel.cs @@ -0,0 +1,12 @@ +namespace Moonlight.ApiServer.Models; + +public class ModuleModel +{ + public string Name { get; set; } + public string Author { get; set; } + public string Version { get; set; } + public string? DonateUrl { get; set; } + public string? UpdateUrl { get; set; } + + public Dictionary> Modules { get; set; } = new(); +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/ModuleService.cs b/Moonlight.ApiServer/Services/ModuleService.cs new file mode 100644 index 00000000..4a95a0fa --- /dev/null +++ b/Moonlight.ApiServer/Services/ModuleService.cs @@ -0,0 +1,74 @@ +using System.Text.Json; +using MoonCore.Helpers; +using Moonlight.ApiServer.Models; + +namespace Moonlight.ApiServer.Services; + +public class ModuleService +{ + private readonly ILogger Logger; + private readonly Dictionary ModuleMeta = new(); + private static string ModulePath = PathBuilder.Dir("storage", "modules"); + + public ModuleModel[] Modules => ModuleMeta.Values.ToArray(); + + public ModuleService(ILogger logger) + { + Logger = logger; + } + + public void Load() + { + Logger.LogInformation("Loading modules"); + + Directory.CreateDirectory(ModulePath); + + foreach (var moduleDirectory in Directory.EnumerateDirectories(ModulePath)) + { + var metaPath = PathBuilder.File(moduleDirectory, "meta.json"); + + if (!File.Exists(metaPath)) + { + Logger.LogWarning("No meta.json found in {folder}", moduleDirectory); + continue; + } + + ModuleModel moduleModel; + + try + { + var json = File.ReadAllText(metaPath); + moduleModel = JsonSerializer.Deserialize(json)!; + } + catch (Exception e) + { + Logger.LogError("An error occured while loading meta.json in {folder}: {e}", moduleDirectory, e); + continue; + } + + ModuleMeta.Add(moduleDirectory, moduleModel); + } + + Logger.LogInformation("Loaded {count} modules", ModuleMeta.Count); + } + + public string[] GetModuleDlls(string moduleName, string section) + { + if (ModuleMeta.All(x => x.Value.Name != moduleName)) + throw new ArgumentException($"No module with the name '{moduleName}' found"); + + var moduleKvp = ModuleMeta + .FirstOrDefault(x => x.Value.Name == moduleName); + + var module = moduleKvp.Value; + + if (!module.Modules.ContainsKey(section)) + return []; + + var modulePaths = module.Modules[section].Select( + dllName => PathBuilder.File(ModulePath, moduleKvp.Key, "bin", section, dllName) + ).Where(File.Exists).ToArray(); + + return modulePaths; + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Startup.cs b/Moonlight.ApiServer/Startup.cs index f9188934..751157a6 100644 --- a/Moonlight.ApiServer/Startup.cs +++ b/Moonlight.ApiServer/Startup.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.Loader; using System.Text.Json; using MoonCore.Authentication; using MoonCore.Exceptions; @@ -16,6 +17,7 @@ using Moonlight.ApiServer.Http.Middleware; using Moonlight.ApiServer.Interfaces.Auth; using Moonlight.ApiServer.Interfaces.OAuth2; using Moonlight.ApiServer.Interfaces.Startup; +using Moonlight.ApiServer.Services; using Moonlight.Shared.Http.Responses.OAuth2; namespace Moonlight.ApiServer; @@ -25,7 +27,7 @@ public static class Startup public static async Task Main(string[] args) => await Run(args, []); - public static async Task Run(string[] args, Assembly[]? pluginAssemblies = null) + public static async Task Run(string[] args, Assembly[]? additionalAssemblies = null) { // Cry about it #pragma warning disable ASP0000 @@ -47,12 +49,10 @@ public static class Startup // Storage i guess Directory.CreateDirectory(PathBuilder.Dir("storage")); -// TODO: Load plugin/module assemblies - -// Configure startup logger + // Configure startup logger var startupLoggerFactory = new LoggerFactory(); -// TODO: Add direct extension method + // TODO: Add direct extension method var providers = LoggerBuildHelper.BuildFromConfiguration(configuration => { configuration.Console.Enable = true; @@ -64,7 +64,35 @@ public static class Startup var startupLogger = startupLoggerFactory.CreateLogger("Startup"); -// Configure startup interfaces + // Load plugin/modules + var moduleService = new ModuleService(startupLoggerFactory.CreateLogger()); + moduleService.Load(); + + // Load api server module assemblies + var apiServerDlls = moduleService.Modules.SelectMany( + x => moduleService.GetModuleDlls(x.Name, "apiServer") + ); + + var apiServerModuleContext = new AssemblyLoadContext(null); + + foreach (var apiServerDll in apiServerDlls) + { + try + { + apiServerModuleContext.LoadFromStream(File.OpenRead( + apiServerDll + )); + } + catch (Exception e) + { + startupLogger.LogCritical("Unable to load dll {name} into context: {e}", apiServerDll, e); + throw; + } + } + + var moduleAssemblies = apiServerModuleContext.Assemblies.ToArray(); + + // Configure startup interfaces var startupServiceCollection = new ServiceCollection(); startupServiceCollection.AddConfiguration(options => @@ -87,10 +115,10 @@ public static class Startup // Configure assemblies to scan configuration.AddAssembly(typeof(Startup).Assembly); - if(pluginAssemblies != null) - configuration.AddAssemblies(pluginAssemblies); + if(additionalAssemblies != null) + configuration.AddAssemblies(additionalAssemblies); - //TODO: Load plugins from file + configuration.AddAssemblies(moduleAssemblies); }); @@ -128,7 +156,13 @@ public static class Startup } } - builder.Services.AddControllers(); + var controllerBuilder = builder.Services.AddControllers(); + + // Add current assemblies to the application part + foreach (var moduleAssembly in moduleAssemblies) + controllerBuilder.AddApplicationPart(moduleAssembly); + + builder.Services.AddSingleton(moduleService); builder.Services.AddSingleton(config); builder.Services.AutoAddServices(typeof(Startup).Assembly); builder.Services.AddHttpClient(); @@ -144,10 +178,10 @@ public static class Startup configuration.AddAssembly(typeof(Startup).Assembly); - if(pluginAssemblies != null) - configuration.AddAssemblies(pluginAssemblies); + if(additionalAssemblies != null) + configuration.AddAssemblies(additionalAssemblies); - //TODO: Load plugins from file + configuration.AddAssemblies(moduleAssemblies); }); var app = builder.Build(); diff --git a/Moonlight.Shared/Http/Responses/ClientPlugins/ClientPluginsResponse.cs b/Moonlight.Shared/Http/Responses/ClientPlugins/ClientPluginsResponse.cs new file mode 100644 index 00000000..767df61c --- /dev/null +++ b/Moonlight.Shared/Http/Responses/ClientPlugins/ClientPluginsResponse.cs @@ -0,0 +1,7 @@ +namespace Moonlight.Shared.Http.Responses.ClientPlugins; + +public class ClientPluginsResponse +{ + public string[] Dlls { get; set; } + public string CacheKey { get; set; } +} \ No newline at end of file