Finished compile time plugin loading. Refactored plugin loading. Extended build helper script

This commit is contained in:
2025-05-13 20:48:50 +02:00
parent 8126250d1a
commit a579dd4759
28 changed files with 1169 additions and 741 deletions

View File

@@ -1,4 +1,9 @@
# Prepare runtime docker image
#
# OUTDATED
# Use https://github.com/Moonlight-Panel/Deploy
#
# Prepare runtime docker image
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled AS base
WORKDIR /app

View File

@@ -12,31 +12,13 @@ namespace Moonlight.ApiServer.Http.Controllers;
public class FrontendController : Controller
{
private readonly FrontendService FrontendService;
private readonly PluginService PluginService;
public FrontendController(FrontendService frontendService, PluginService pluginService)
public FrontendController(FrontendService frontendService)
{
FrontendService = frontendService;
PluginService = pluginService;
}
[HttpGet("frontend.json")]
public async Task<FrontendConfiguration> GetConfiguration()
=> await FrontendService.GetConfiguration();
[HttpGet("plugins/{assemblyName}")]
public async Task GetPluginAssembly(string assemblyName)
{
var assembliesMap = PluginService.GetAssemblies("client");
if (!assembliesMap.TryGetValue(assemblyName, out var path))
throw new HttpApiException("The requested assembly could not be found", 404);
var absolutePath = Path.Combine(
Directory.GetCurrentDirectory(),
path
);
await Results.File(absolutePath).ExecuteAsync(HttpContext);
}
}

View File

@@ -1,24 +1,20 @@
using Microsoft.OpenApi.Models;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database;
using Moonlight.ApiServer.Interfaces.Startup;
using Moonlight.ApiServer.Plugins;
namespace Moonlight.ApiServer.Implementations.Startup;
[PluginStartup]
public class CoreStartup : IPluginStartup
{
private readonly AppConfiguration Configuration;
public CoreStartup(AppConfiguration configuration)
{
Configuration = configuration;
}
public Task BuildApplication(IHostApplicationBuilder builder)
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder)
{
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
#region Api Docs
if (Configuration.Development.EnableApiDocs)
if (configuration.Development.EnableApiDocs)
{
builder.Services.AddEndpointsApiExplorer();
@@ -53,14 +49,16 @@ public class CoreStartup : IPluginStartup
return Task.CompletedTask;
}
public Task ConfigureApplication(IApplicationBuilder app)
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app)
{
return Task.CompletedTask;
}
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder)
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder)
{
if(Configuration.Development.EnableApiDocs)
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
if(configuration.Development.EnableApiDocs)
routeBuilder.MapSwagger("/api/swagger/{documentName}");
return Task.CompletedTask;

View File

@@ -1,8 +0,0 @@
namespace Moonlight.ApiServer.Interfaces.Startup;
public interface IPluginStartup
{
public Task BuildApplication(IHostApplicationBuilder builder);
public Task ConfigureApplication(IApplicationBuilder app);
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder);
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.ApiServer.Models;
public class FrontendConfigurationOption
{
public string[] Scripts { get; set; } = [];
public string[] Styles { get; set; } = [];
}

View File

@@ -1,70 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Moonlight.Client\Moonlight.Client.csproj"/>
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Database\Migrations\"/>
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
<Pack>false</Pack>
</Content>
</ItemGroup>
<PropertyGroup>
<PackageId>Moonlight.ApiServer</PackageId>
<Version>2.1.0</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the api server for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
<DevelopmentDependency>true</DevelopmentDependency>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ExCSS" Version="4.3.0"/>
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18"/>
<PackageReference Include="Hangfire.Core" Version="1.8.18"/>
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.10"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"/>
<PackageReference Include="MoonCore" Version="1.8.5"/>
<PackageReference Include="MoonCore.Extended" Version="1.3.2"/>
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5"/>
<PackageReference Include="SharpZipLib" Version="1.4.2"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
<PackageReference Include="Ben.Demystifier" Version="0.4.1"/>
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Include="**\*.razor" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<Compile Remove="storage\**\*" />
<Content Remove="storage\**\*" />
<None Remove="storage\**\*" />
</ItemGroup>
</Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<PackageTags>apiserver</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Moonlight.Client\Moonlight.Client.csproj" />
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Database\Migrations\" />
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
<Pack>false</Pack>
</Content>
</ItemGroup>
<PropertyGroup>
<PackageId>Moonlight.ApiServer</PackageId>
<Version>2.1.0</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the api server for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
<DevelopmentDependency>true</DevelopmentDependency>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ExCSS" Version="4.3.0" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18" />
<PackageReference Include="Hangfire.Core" Version="1.8.18" />
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.15" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.15" />
<PackageReference Include="MoonCore" Version="1.8.5" />
<PackageReference Include="MoonCore.Extended" Version="1.3.2" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Include="**\*.razor" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<Compile Remove="storage\**\*" />
<Content Remove="storage\**\*" />
<None Remove="storage\**\*" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
namespace Moonlight.ApiServer.Plugins;
public interface IPluginStartup
{
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder);
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app);
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder);
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.ApiServer.Plugins;
[AttributeUsage(AttributeTargets.Class)]
public class PluginStartupAttribute : Attribute
{
}

View File

@@ -6,6 +6,7 @@ using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Models;
using Moonlight.Shared.Misc;
namespace Moonlight.ApiServer.Services;
@@ -14,18 +15,18 @@ namespace Moonlight.ApiServer.Services;
public class FrontendService
{
private readonly AppConfiguration Configuration;
private readonly PluginService PluginService;
private readonly IWebHostEnvironment WebHostEnvironment;
private readonly IEnumerable<FrontendConfigurationOption> ConfigurationOptions;
public FrontendService(
AppConfiguration configuration,
PluginService pluginService,
IWebHostEnvironment webHostEnvironment
IWebHostEnvironment webHostEnvironment,
IEnumerable<FrontendConfigurationOption> configurationOptions
)
{
Configuration = configuration;
PluginService = pluginService;
WebHostEnvironment = webHostEnvironment;
ConfigurationOptions = configurationOptions;
}
public async Task<FrontendConfiguration> GetConfiguration()
@@ -48,33 +49,15 @@ public class FrontendService
.Deserialize<Dictionary<string, string>>(variablesJson) ?? new();
}
// Collect assemblies for the 'client' section
configuration.Assemblies = PluginService
.GetAssemblies("client")
.Keys
.ToArray();
// Collect scripts to execute
configuration.Scripts = PluginService
.LoadedPlugins
.Keys
configuration.Scripts = ConfigurationOptions
.SelectMany(x => x.Scripts)
.ToArray();
// Collect styles
var styles = new List<string>();
styles.AddRange(
PluginService
.LoadedPlugins
.Keys
.SelectMany(x => x.Styles)
);
// Add bundle css
styles.Add("css/bundle.min.css");
configuration.Styles = styles.ToArray();
configuration.Styles = ConfigurationOptions
.SelectMany(x => x.Styles)
.ToArray();
return configuration;
}
@@ -111,42 +94,12 @@ public class FrontendService
// Add blazor files
await ArchiveFsItem(zipArchive, blazorPath, blazorPath, "_framework/");
// Add bundle.css
var bundleContent = await File.ReadAllBytesAsync(Path.Combine("storage", "tmp", "bundle.css"));
await ArchiveBytes(zipArchive, "css/bundle.css", bundleContent);
// Add frontend.json
var frontendConfig = await GetConfiguration();
frontendConfig.HostEnvironment = "Static";
var frontendJson = JsonSerializer.Serialize(frontendConfig);
await ArchiveText(zipArchive, "frontend.json", frontendJson);
// Add plugin wwwroot files
foreach (var pluginPath in PluginService.LoadedPlugins.Values)
{
var wwwRootPluginPath = Path.Combine(pluginPath, "wwwroot/");
if (!Directory.Exists(wwwRootPluginPath))
continue;
await ArchiveFsItem(zipArchive, wwwRootPluginPath, wwwRootPluginPath);
}
// Add plugin assemblies for client to the zip file
var assembliesMap = PluginService.GetAssemblies("client");
foreach (var assemblyName in assembliesMap.Keys)
{
var path = assembliesMap[assemblyName];
await ArchiveFsItem(
zipArchive,
path,
path,
$"plugins/{assemblyName}"
);
}
// Finish zip archive and reset stream so the code calling this function can process it
zipArchive.Dispose();
await memoryStream.FlushAsync();

View File

@@ -1,13 +1,10 @@
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Text.Json;
using Hangfire;
using Hangfire.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Configuration;
using MoonCore.EnvConfiguration;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Extensions;
@@ -15,15 +12,14 @@ using MoonCore.Extended.Helpers;
using MoonCore.Extended.JwtInvalidation;
using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.Services;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Helpers;
using Moonlight.ApiServer.Implementations;
using Moonlight.ApiServer.Implementations.Startup;
using Moonlight.ApiServer.Interfaces;
using Moonlight.ApiServer.Interfaces.Startup;
using Moonlight.ApiServer.Models;
using Moonlight.ApiServer.Plugins;
using Moonlight.ApiServer.Services;
namespace Moonlight.ApiServer;
@@ -34,8 +30,6 @@ namespace Moonlight.ApiServer;
public class Startup
{
private string[] Args;
private Assembly[] AdditionalAssemblies;
private PluginManifest[] AdditionalPluginManifests;
// Logging
private ILoggerProvider[] LoggerProviders;
@@ -51,24 +45,20 @@ public class Startup
private WebApplicationBuilder WebApplicationBuilder;
// Plugin Loading
private PluginService PluginService;
private AssemblyLoadContext PluginLoadContext;
private IPluginStartup[] PluginStartups;
private IPluginStartup[] AdditionalPlugins;
private IServiceProvider PluginLoadServiceProvider;
public async Task Run(string[] args, Assembly[]? additionalAssemblies = null,
PluginManifest[]? additionalManifests = null)
public async Task Run(string[] args, IPluginStartup[]? additionalPlugins = null)
{
Args = args;
AdditionalAssemblies = additionalAssemblies ?? [];
AdditionalPluginManifests = additionalManifests ?? [];
AdditionalPlugins = additionalPlugins ?? [];
await PrintVersion();
await CreateStorage();
await SetupAppConfiguration();
await SetupLogging();
await LoadPlugins();
await InitializePlugins();
await CreateWebApplicationBuilder();
@@ -139,17 +129,13 @@ public class Startup
// Add pre-existing services
WebApplicationBuilder.Services.AddSingleton(Configuration);
WebApplicationBuilder.Services.AddSingleton(PluginService);
// Configure controllers
var mvcBuilder = WebApplicationBuilder.Services.AddControllers();
// Add plugin and additional assemblies as application parts
foreach (var pluginAssembly in PluginLoadContext.Assemblies)
mvcBuilder.AddApplicationPart(pluginAssembly);
foreach (var additionalAssembly in AdditionalAssemblies)
mvcBuilder.AddApplicationPart(additionalAssembly);
// Add plugin assemblies as application parts
foreach (var pluginStartup in PluginStartups.Select(x => x.GetType().Assembly).Distinct())
mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly);
return Task.CompletedTask;
}
@@ -199,90 +185,34 @@ public class Startup
#region Plugin Loading
private async Task LoadPlugins()
{
// Load plugins
PluginService = new PluginService(
LoggerFactory.CreateLogger<PluginService>()
);
// Add plugins manually if specified in the startup
foreach (var manifest in AdditionalPluginManifests)
PluginService.LoadedPlugins.Add(manifest, Directory.GetCurrentDirectory());
// Search and load all plugins
await PluginService.Load();
// Search up assemblies for the apiServer
var assemblyFiles = PluginService.GetAssemblies("apiServer")
.Values
.ToArray();
// Create the load context and add assemblies
PluginLoadContext = new AssemblyLoadContext(null);
foreach (var assemblyFile in assemblyFiles)
{
try
{
PluginLoadContext.LoadFromAssemblyPath(
Path.Combine(Directory.GetCurrentDirectory(), assemblyFile)
);
}
catch (Exception e)
{
Logger.LogError("Unable to load plugin assembly '{assemblyFile}': {e}", assemblyFile, e);
}
}
}
private Task InitializePlugins()
{
// Define minimal service collection
var startupSc = new ServiceCollection();
// Create service provider for starting up
var serviceCollection = new ServiceCollection();
// Configure base services for initialisation
startupSc.AddSingleton(Configuration);
startupSc.AddLogging(builder =>
serviceCollection.AddSingleton(Configuration);
serviceCollection.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddProviders(LoggerProviders);
});
//
var startupSp = startupSc.BuildServiceProvider();
PluginLoadServiceProvider = serviceCollection.BuildServiceProvider();
// Collect startups
var pluginStartups = new List<IPluginStartup>();
pluginStartups.Add(new CoreStartup());
pluginStartups.AddRange(AdditionalPlugins); // Used by the development server
// Do NOT remove the following comment, as its used to place the plugin startup register calls
// MLBUILD_PLUGIN_STARTUP_HERE
// Initialize plugin startups
var startups = new List<IPluginStartup>();
var startupType = typeof(IPluginStartup);
var assembliesToScan = new List<Assembly>();
assembliesToScan.Add(typeof(Startup).Assembly);
assembliesToScan.AddRange(PluginLoadContext.Assemblies);
assembliesToScan.AddRange(AdditionalAssemblies);
foreach (var pluginAssembly in assembliesToScan)
{
var startupTypes = pluginAssembly
.ExportedTypes
.Where(x => !x.IsAbstract && !x.IsInterface && x.IsAssignableTo(startupType))
.ToArray();
foreach (var type in startupTypes)
{
var startup = ActivatorUtilities.CreateInstance(startupSp, type) as IPluginStartup;
if (startup == null)
continue;
startups.Add(startup);
}
}
PluginStartups = startups.ToArray();
PluginStartups = pluginStartups.ToArray();
return Task.CompletedTask;
}
@@ -298,11 +228,6 @@ public class Startup
FileProvider = new BundleAssetFileProvider()
});
WebApplication.UseStaticFiles(new StaticFileOptions()
{
FileProvider = PluginService.WwwRootFileProvider
});
return Task.CompletedTask;
}
@@ -314,7 +239,7 @@ public class Startup
{
try
{
await pluginAppStartup.BuildApplication(WebApplicationBuilder);
await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebApplicationBuilder);
}
catch (Exception e)
{
@@ -333,7 +258,7 @@ public class Startup
{
try
{
await pluginAppStartup.ConfigureApplication(WebApplication);
await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebApplication);
}
catch (Exception e)
{
@@ -352,7 +277,7 @@ public class Startup
{
try
{
await pluginEndpointStartup.ConfigureEndpoints(WebApplication);
await pluginEndpointStartup.ConfigureEndpoints(PluginLoadServiceProvider, WebApplication);
}
catch (Exception e)
{