Simplified plugin service and loading
This commit is contained in:
@@ -1,37 +0,0 @@
|
|||||||
using Microsoft.Extensions.FileProviders;
|
|
||||||
using Microsoft.Extensions.FileProviders.Physical;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using Moonlight.ApiServer.Services;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Helpers;
|
|
||||||
|
|
||||||
public class PluginAssetFileProvider : IFileProvider
|
|
||||||
{
|
|
||||||
private readonly PluginService PluginService;
|
|
||||||
|
|
||||||
public PluginAssetFileProvider(PluginService pluginService)
|
|
||||||
{
|
|
||||||
PluginService = pluginService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDirectoryContents GetDirectoryContents(string subpath)
|
|
||||||
{
|
|
||||||
return NotFoundDirectoryContents.Singleton;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IFileInfo GetFileInfo(string subpath)
|
|
||||||
{
|
|
||||||
if (!PluginService.AssetMap.TryGetValue(subpath, out var physicalPath))
|
|
||||||
return new NotFoundFileInfo(subpath);
|
|
||||||
|
|
||||||
if (!File.Exists(physicalPath))
|
|
||||||
return new NotFoundFileInfo(subpath);
|
|
||||||
|
|
||||||
return new PhysicalFileInfo(new FileInfo(physicalPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
public IChangeToken Watch(string filter)
|
|
||||||
{
|
|
||||||
return NullChangeToken.Singleton;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,17 +13,14 @@ public class FrontendController : Controller
|
|||||||
{
|
{
|
||||||
private readonly AppConfiguration Configuration;
|
private readonly AppConfiguration Configuration;
|
||||||
private readonly PluginService PluginService;
|
private readonly PluginService PluginService;
|
||||||
private readonly AssetService AssetService;
|
|
||||||
|
|
||||||
public FrontendController(
|
public FrontendController(
|
||||||
AppConfiguration configuration,
|
AppConfiguration configuration,
|
||||||
PluginService pluginService,
|
PluginService pluginService
|
||||||
AssetService assetService
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
PluginService = pluginService;
|
PluginService = pluginService;
|
||||||
AssetService = assetService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("frontend.json")]
|
[HttpGet("frontend.json")]
|
||||||
@@ -36,27 +33,54 @@ public class FrontendController : Controller
|
|||||||
HostEnvironment = "ApiServer"
|
HostEnvironment = "ApiServer"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load theme if it exists
|
#region Load theme.json if it exists
|
||||||
|
|
||||||
var themePath = PathBuilder.File("storage", "theme.json");
|
var themePath = PathBuilder.File("storage", "theme.json");
|
||||||
|
|
||||||
if (System.IO.File.Exists(themePath))
|
if (System.IO.File.Exists(themePath))
|
||||||
{
|
{
|
||||||
var variablesJson = await System.IO.File.ReadAllTextAsync(themePath);
|
var variablesJson = await System.IO.File.ReadAllTextAsync(themePath);
|
||||||
configuration.Theme.Variables = JsonSerializer.Deserialize<Dictionary<string, string>>(variablesJson) ?? new();
|
configuration.Theme.Variables =
|
||||||
|
JsonSerializer.Deserialize<Dictionary<string, string>>(variablesJson) ?? new();
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration.Plugins.Entrypoints = PluginService.HostedPluginsManifest.Entrypoints;
|
#endregion
|
||||||
configuration.Plugins.Assemblies = PluginService.HostedPluginsManifest.Assemblies;
|
|
||||||
|
|
||||||
configuration.Scripts = AssetService.GetJavascriptAssets();
|
// Collect assemblies for the 'client' section
|
||||||
|
configuration.Assemblies = PluginService
|
||||||
|
.GetAssemblies("client")
|
||||||
|
.Keys
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Collect scripts to execute
|
||||||
|
configuration.Scripts = PluginService
|
||||||
|
.LoadedPlugins
|
||||||
|
.Keys
|
||||||
|
.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();
|
||||||
|
|
||||||
return configuration;
|
return configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("plugins/{assemblyName}")] // TODO: Test this
|
[HttpGet("plugins/{assemblyName}")]
|
||||||
public async Task GetPluginAssembly(string assemblyName)
|
public async Task GetPluginAssembly(string assemblyName)
|
||||||
{
|
{
|
||||||
var assembliesMap = PluginService.ClientAssemblyMap;
|
var assembliesMap = PluginService.GetAssemblies("client");
|
||||||
|
|
||||||
if (assembliesMap.ContainsKey(assemblyName))
|
if (assembliesMap.ContainsKey(assemblyName))
|
||||||
throw new HttpApiException("The requested assembly could not be found", 404);
|
throw new HttpApiException("The requested assembly could not be found", 404);
|
||||||
|
|||||||
@@ -7,5 +7,9 @@ public class PluginManifest
|
|||||||
public string Author { get; set; }
|
public string Author { get; set; }
|
||||||
public string[] Dependencies { get; set; } = [];
|
public string[] Dependencies { get; set; } = [];
|
||||||
|
|
||||||
public Dictionary<string, string[]> Entrypoints { get; set; } = new();
|
public string[] Scripts { get; set; } = [];
|
||||||
|
public string[] Styles { get; set; } = [];
|
||||||
|
|
||||||
|
public string[] BundledStyles { get; set; } = [];
|
||||||
|
public Dictionary<string, string[]> Assemblies { get; set; } = new();
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Moonlight.ApiServer.Models;
|
|
||||||
|
|
||||||
public class PluginMeta
|
|
||||||
{
|
|
||||||
public PluginManifest Manifest { get; set; }
|
|
||||||
public string Path { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using MoonCore.Attributes;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Services;
|
|
||||||
|
|
||||||
[Singleton]
|
|
||||||
public class AssetService
|
|
||||||
{
|
|
||||||
public string[] JavascriptFiles { get; private set; }
|
|
||||||
|
|
||||||
private bool HasBeenCollected = false;
|
|
||||||
|
|
||||||
private readonly List<string> AdditionalCssAssets = new();
|
|
||||||
private readonly List<string> AdditionalJavascriptAssets = new();
|
|
||||||
|
|
||||||
private readonly PluginService PluginService;
|
|
||||||
|
|
||||||
public AssetService(PluginService pluginService)
|
|
||||||
{
|
|
||||||
PluginService = pluginService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CollectAssets()
|
|
||||||
{
|
|
||||||
// Javascript
|
|
||||||
var jsFiles = new List<string>();
|
|
||||||
|
|
||||||
jsFiles.AddRange(AdditionalJavascriptAssets);
|
|
||||||
jsFiles.AddRange(PluginService.AssetMap.Keys.Where(x => x.EndsWith(".js")));
|
|
||||||
|
|
||||||
JavascriptFiles = jsFiles.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddJavascriptAsset(string asset)
|
|
||||||
=> AdditionalJavascriptAssets.Add(asset);
|
|
||||||
|
|
||||||
public string[] GetJavascriptAssets()
|
|
||||||
{
|
|
||||||
if (HasBeenCollected)
|
|
||||||
return JavascriptFiles;
|
|
||||||
|
|
||||||
CollectAssets();
|
|
||||||
|
|
||||||
return JavascriptFiles;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,5 +7,8 @@ public class BundleService
|
|||||||
public void BundleCss(string path)
|
public void BundleCss(string path)
|
||||||
=> CssFiles.Add(path);
|
=> CssFiles.Add(path);
|
||||||
|
|
||||||
|
public void BundleCssRange(string[] paths)
|
||||||
|
=> CssFiles.AddRange(paths);
|
||||||
|
|
||||||
public IEnumerable<string> GetCssFiles() => CssFiles;
|
public IEnumerable<string> GetCssFiles() => CssFiles;
|
||||||
}
|
}
|
||||||
@@ -1,181 +1,136 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonCore.Models;
|
|
||||||
using Moonlight.ApiServer.Models;
|
using Moonlight.ApiServer.Models;
|
||||||
using Moonlight.Shared.Http.Responses.PluginsStream;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Services;
|
namespace Moonlight.ApiServer.Services;
|
||||||
|
|
||||||
public class PluginService
|
public class PluginService
|
||||||
{
|
{
|
||||||
public List<PluginMeta> Plugins { get; private set; } = new();
|
|
||||||
public Dictionary<string, string> AssetMap { get; private set; } = new();
|
|
||||||
public HostedPluginsManifest HostedPluginsManifest { get; private set; }
|
|
||||||
public Dictionary<string, string> ClientAssemblyMap { get; private set; }
|
|
||||||
|
|
||||||
private static string PluginsFolder = PathBuilder.Dir("storage", "plugins");
|
|
||||||
private readonly ILogger<PluginService> Logger;
|
private readonly ILogger<PluginService> Logger;
|
||||||
|
private readonly string PluginRoot;
|
||||||
|
|
||||||
private readonly JsonSerializerOptions SerializerOptions = new()
|
public readonly Dictionary<PluginManifest, string> LoadedPlugins = new();
|
||||||
{
|
public IFileProvider WwwRootFileProvider;
|
||||||
PropertyNameCaseInsensitive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
public PluginService(ILogger<PluginService> logger)
|
public PluginService(ILogger<PluginService> logger)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
|
|
||||||
|
PluginRoot = PathBuilder.Dir("storage", "plugins");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Load()
|
public async Task Load()
|
||||||
{
|
{
|
||||||
// Load all manifest files
|
var jsonOptions = new JsonSerializerOptions()
|
||||||
foreach (var pluginFolder in Directory.EnumerateDirectories(PluginsFolder))
|
|
||||||
{
|
{
|
||||||
var manifestPath = PathBuilder.File(pluginFolder, "plugin.json");
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
if (!File.Exists(manifestPath))
|
var pluginDirs = Directory.GetDirectories(PluginRoot);
|
||||||
|
var pluginMap = new Dictionary<PluginManifest, string>();
|
||||||
|
|
||||||
|
#region Scan plugins/ directory for plugin.json files
|
||||||
|
|
||||||
|
foreach (var dir in pluginDirs)
|
||||||
|
{
|
||||||
|
var metaPath = PathBuilder.File(dir, "plugin.json");
|
||||||
|
|
||||||
|
if (!File.Exists(metaPath))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("Ignoring '{folder}' because no manifest has been found", pluginFolder);
|
Logger.LogWarning("Skipped '{dir}' as it is missing a plugin.json", dir);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginManifest manifest;
|
var json = await File.ReadAllTextAsync(metaPath);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var manifestText = await File.ReadAllTextAsync(manifestPath);
|
var meta = JsonSerializer.Deserialize<PluginManifest>(json, jsonOptions);
|
||||||
manifest = JsonSerializer.Deserialize<PluginManifest>(manifestText, SerializerOptions)!;
|
|
||||||
|
if (meta == null)
|
||||||
|
throw new JsonException("Unable to parse. Return value was null");
|
||||||
|
|
||||||
|
pluginMap.Add(meta, dir);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (JsonException e)
|
||||||
{
|
{
|
||||||
Logger.LogError("An unhandled error occured while loading plugin manifest in '{folder}': {e}",
|
Logger.LogError("Unable to load plugin.json at '{path}': {e}", metaPath, e);
|
||||||
pluginFolder, e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogTrace("Loaded plugin manifest. Id: {id}", manifest.Id);
|
|
||||||
|
|
||||||
Plugins.Add(new()
|
|
||||||
{
|
|
||||||
Manifest = manifest,
|
|
||||||
Path = pluginFolder
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for missing dependencies
|
|
||||||
var pluginsToNotLoad = new List<string>();
|
|
||||||
|
|
||||||
foreach (var plugin in Plugins)
|
|
||||||
{
|
|
||||||
foreach (var dependency in plugin.Manifest.Dependencies)
|
|
||||||
{
|
|
||||||
// Check if dependency is found
|
|
||||||
if (Plugins.Any(x => x.Manifest.Id == dependency))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Logger.LogError(
|
|
||||||
"Unable to load plugin '{id}' ({path}) because the dependency '{dependency}' is missing",
|
|
||||||
plugin.Manifest.Id,
|
|
||||||
plugin.Path,
|
|
||||||
dependency
|
|
||||||
);
|
|
||||||
|
|
||||||
pluginsToNotLoad.Add(plugin.Manifest.Id);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove unloadable plugins from cache
|
#endregion
|
||||||
Plugins.RemoveAll(x => pluginsToNotLoad.Contains(x.Manifest.Id));
|
|
||||||
|
|
||||||
// Generate assembly map for client
|
#region Depdenency check
|
||||||
ClientAssemblyMap = GetAssemblies("client");
|
|
||||||
|
|
||||||
// Generate plugin stream manifest for client
|
foreach (var plugin in pluginMap.Keys)
|
||||||
HostedPluginsManifest = new()
|
|
||||||
{
|
{
|
||||||
Assemblies = ClientAssemblyMap.Keys.ToArray(),
|
var hasMissingDep = false;
|
||||||
Entrypoints = GetEntrypoints("client")
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate asset map
|
foreach (var dependency in plugin.Dependencies)
|
||||||
GenerateAssetMap();
|
{
|
||||||
|
if (pluginMap.Keys.All(x => x.Id != dependency))
|
||||||
|
{
|
||||||
|
hasMissingDep = true;
|
||||||
|
Logger.LogWarning("Plugin '{name}' has missing dependency: {dep}", plugin.Name, dependency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMissingDep)
|
||||||
|
Logger.LogWarning("Unable to load '{name}' due to missing dependencies", plugin.Name);
|
||||||
|
else
|
||||||
|
LoadedPlugins.Add(plugin, pluginMap[plugin]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Create wwwroot file provider
|
||||||
|
|
||||||
|
Logger.LogInformation("Creating wwwroot file provider");
|
||||||
|
WwwRootFileProvider = CreateWwwRootProvider();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
Logger.LogInformation("Loaded {count} plugins", LoadedPlugins.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, string> GetAssemblies(string section)
|
public Dictionary<string, string> GetAssemblies(string section)
|
||||||
{
|
{
|
||||||
var pathMappings = new Dictionary<string, string>();
|
var assemblyMap = new Dictionary<string, string>();
|
||||||
|
|
||||||
foreach (var plugin in Plugins)
|
foreach (var loadedPlugin in LoadedPlugins.Keys)
|
||||||
{
|
{
|
||||||
var binaryPath = PathBuilder.Dir(plugin.Path, "bin", section);
|
// Skip all plugins which haven't defined any assemblies in that section
|
||||||
|
if (!loadedPlugin.Assemblies.ContainsKey(section))
|
||||||
if(!Directory.Exists(binaryPath))
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
foreach (var file in Directory.EnumerateFiles(binaryPath))
|
var pluginPath = LoadedPlugins[loadedPlugin];
|
||||||
{
|
|
||||||
if (!file.EndsWith(".dll"))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var fileName = Path.GetFileName(file);
|
foreach (var assembly in loadedPlugin.Assemblies[section])
|
||||||
pathMappings[fileName] = file;
|
{
|
||||||
|
var assemblyFile = Path.GetFileName(assembly);
|
||||||
|
assemblyMap[assemblyFile] = PathBuilder.File(pluginPath, assembly);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathMappings;
|
return assemblyMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string[] GetEntrypoints(string section)
|
private IFileProvider CreateWwwRootProvider()
|
||||||
{
|
{
|
||||||
return Plugins
|
List<IFileProvider> wwwRootProviders = new();
|
||||||
.Where(x => x.Manifest.Entrypoints.ContainsKey(section))
|
|
||||||
.SelectMany(x => x.Manifest.Entrypoints[section])
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GenerateAssetMap()
|
foreach (var pluginFolder in LoadedPlugins.Values)
|
||||||
{
|
|
||||||
AssetMap.Clear();
|
|
||||||
|
|
||||||
foreach (var plugin in Plugins)
|
|
||||||
{
|
{
|
||||||
var assetPath = PathBuilder.Dir(plugin.Path, "wwwroot");
|
var wwwRootPath = Path.GetFullPath(
|
||||||
|
PathBuilder.Dir(pluginFolder, "wwwroot")
|
||||||
|
);
|
||||||
|
|
||||||
if (!Directory.Exists(assetPath))
|
wwwRootProviders.Add(
|
||||||
continue;
|
new PhysicalFileProvider(wwwRootPath)
|
||||||
|
);
|
||||||
var files = new List<string>();
|
|
||||||
GetFilesInDirectory(assetPath, files);
|
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
var mapPath = Formatter.ReplaceStart(file, assetPath, "");
|
|
||||||
|
|
||||||
mapPath = mapPath.Replace("\\", "/"); // To handle fucking windows
|
|
||||||
mapPath = mapPath.StartsWith("/") ? mapPath : "/" + mapPath; // Ensure starting /
|
|
||||||
|
|
||||||
if (AssetMap.ContainsKey(mapPath))
|
|
||||||
{
|
|
||||||
Logger.LogWarning(
|
|
||||||
"The plugin '{name}' tries to map an asset to the path '{path}' which is already used by another plugin. Ignoring asset mapping",
|
|
||||||
plugin.Manifest.Id,
|
|
||||||
mapPath
|
|
||||||
);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
AssetMap[mapPath] = file;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void GetFilesInDirectory(string directory, List<string> files)
|
return new CompositeFileProvider(wwwRootProviders);
|
||||||
{
|
|
||||||
files.AddRange(Directory.EnumerateFiles(directory));
|
|
||||||
|
|
||||||
foreach (var dir in Directory.EnumerateDirectories(directory))
|
|
||||||
GetFilesInDirectory(dir, files);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Runtime.Loader;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
@@ -19,7 +20,9 @@ using Moonlight.ApiServer.Database.Entities;
|
|||||||
using Moonlight.ApiServer.Helpers;
|
using Moonlight.ApiServer.Helpers;
|
||||||
using Moonlight.ApiServer.Interfaces.OAuth2;
|
using Moonlight.ApiServer.Interfaces.OAuth2;
|
||||||
using Moonlight.ApiServer.Interfaces.Startup;
|
using Moonlight.ApiServer.Interfaces.Startup;
|
||||||
|
using Moonlight.ApiServer.Models;
|
||||||
using Moonlight.ApiServer.Services;
|
using Moonlight.ApiServer.Services;
|
||||||
|
using Moonlight.Client.Services;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer;
|
namespace Moonlight.ApiServer;
|
||||||
|
|
||||||
@@ -30,6 +33,7 @@ public class Startup
|
|||||||
{
|
{
|
||||||
private string[] Args;
|
private string[] Args;
|
||||||
private Assembly[] AdditionalAssemblies;
|
private Assembly[] AdditionalAssemblies;
|
||||||
|
private PluginManifest[] AdditionalPluginManifests;
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
private ILoggerProvider[] LoggerProviders;
|
private ILoggerProvider[] LoggerProviders;
|
||||||
@@ -47,17 +51,18 @@ public class Startup
|
|||||||
|
|
||||||
// Plugin Loading
|
// Plugin Loading
|
||||||
private PluginService PluginService;
|
private PluginService PluginService;
|
||||||
private PluginLoaderService PluginLoaderService;
|
private AssemblyLoadContext PluginLoadContext;
|
||||||
|
|
||||||
// Asset bundling
|
// Asset bundling
|
||||||
private BundleService BundleService;
|
private BundleService BundleService = new();
|
||||||
|
|
||||||
private IPluginStartup[] PluginStartups;
|
private IPluginStartup[] PluginStartups;
|
||||||
|
|
||||||
public async Task Run(string[] args, Assembly[]? additionalAssemblies = null)
|
public async Task Run(string[] args, Assembly[]? additionalAssemblies = null, PluginManifest[]? additionalManifests = null)
|
||||||
{
|
{
|
||||||
Args = args;
|
Args = args;
|
||||||
AdditionalAssemblies = additionalAssemblies ?? [];
|
AdditionalAssemblies = additionalAssemblies ?? [];
|
||||||
|
AdditionalPluginManifests = additionalManifests ?? [];
|
||||||
|
|
||||||
await PrintVersion();
|
await PrintVersion();
|
||||||
|
|
||||||
@@ -76,18 +81,16 @@ public class Startup
|
|||||||
await RegisterAuth();
|
await RegisterAuth();
|
||||||
await RegisterCaching();
|
await RegisterCaching();
|
||||||
await HookPluginBuild();
|
await HookPluginBuild();
|
||||||
await HandleConfigureArguments();
|
|
||||||
await RegisterPluginAssets();
|
await RegisterPluginAssets();
|
||||||
|
|
||||||
await BuildWebApplication();
|
await BuildWebApplication();
|
||||||
|
|
||||||
await HandleServiceArguments();
|
|
||||||
await PrepareDatabase();
|
await PrepareDatabase();
|
||||||
|
|
||||||
|
await UsePluginAssets(); // We need to move the plugin assets to the top to allow plugins to override content
|
||||||
await UseBase();
|
await UseBase();
|
||||||
await UseAuth();
|
await UseAuth();
|
||||||
await HookPluginConfigure();
|
await HookPluginConfigure();
|
||||||
await UsePluginAssets();
|
|
||||||
|
|
||||||
await MapBase();
|
await MapBase();
|
||||||
await HookPluginEndpoints();
|
await HookPluginEndpoints();
|
||||||
@@ -123,73 +126,6 @@ public class Startup
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Command line arguments
|
|
||||||
|
|
||||||
private Task HandleConfigureArguments()
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task HandleServiceArguments()
|
|
||||||
{
|
|
||||||
// Handle manual asset loading arguments
|
|
||||||
if (Args.Any(x => x.StartsWith("--frontend-asset")))
|
|
||||||
{
|
|
||||||
if (!Configuration.Client.Enable)
|
|
||||||
{
|
|
||||||
Logger.LogWarning("The hosting of the moonlight frontend is disabled. Ignoring all --frontend-asset options");
|
|
||||||
return Task.CompletedTask; // TODO: Change this when adding more service argument handling functions
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!WebApplicationBuilder.Environment.IsDevelopment())
|
|
||||||
Logger.LogWarning("Using the --frontend-asset option is not meant to be used in production. Plugin assets will be loaded automaticly");
|
|
||||||
|
|
||||||
var assetService = WebApplication.Services.GetRequiredService<AssetService>();
|
|
||||||
|
|
||||||
for (var i = 0; i < Args.Length; i++)
|
|
||||||
{
|
|
||||||
var currentArg = Args[i];
|
|
||||||
|
|
||||||
// Ignore all args without relation to our frontend assets
|
|
||||||
if(!currentArg.Equals("--frontend-asset", StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (i + 1 >= Args.Length)
|
|
||||||
{
|
|
||||||
Logger.LogWarning("You need to specify an asset path after the --frontend-asset option");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextArg = Args[i + 1];
|
|
||||||
|
|
||||||
if (nextArg.StartsWith("--"))
|
|
||||||
{
|
|
||||||
Logger.LogWarning("You need to specify an asset path after the --frontend-asset option");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(nextArg);
|
|
||||||
|
|
||||||
switch (extension)
|
|
||||||
{
|
|
||||||
case ".css":
|
|
||||||
BundleService.BundleCss(nextArg);
|
|
||||||
break;
|
|
||||||
case ".js":
|
|
||||||
assetService.AddJavascriptAsset(nextArg);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Logger.LogWarning("Unknown asset extension {extension}. Ignoring it", extension);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Base
|
#region Base
|
||||||
|
|
||||||
private Task RegisterBase()
|
private Task RegisterBase()
|
||||||
@@ -207,7 +143,7 @@ public class Startup
|
|||||||
var mvcBuilder = WebApplicationBuilder.Services.AddControllers();
|
var mvcBuilder = WebApplicationBuilder.Services.AddControllers();
|
||||||
|
|
||||||
// Add plugin and additional assemblies as application parts
|
// Add plugin and additional assemblies as application parts
|
||||||
foreach (var pluginAssembly in PluginLoaderService.PluginAssemblies)
|
foreach (var pluginAssembly in PluginLoadContext.Assemblies)
|
||||||
mvcBuilder.AddApplicationPart(pluginAssembly);
|
mvcBuilder.AddApplicationPart(pluginAssembly);
|
||||||
|
|
||||||
foreach (var additionalAssembly in AdditionalAssemblies)
|
foreach (var additionalAssembly in AdditionalAssemblies)
|
||||||
@@ -238,9 +174,7 @@ public class Startup
|
|||||||
WebApplication.MapControllers();
|
WebApplication.MapControllers();
|
||||||
|
|
||||||
if (Configuration.Client.Enable)
|
if (Configuration.Client.Enable)
|
||||||
{
|
|
||||||
WebApplication.MapFallbackToFile("index.html");
|
WebApplication.MapFallbackToFile("index.html");
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@@ -256,25 +190,32 @@ public class Startup
|
|||||||
LoggerFactory.CreateLogger<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();
|
await PluginService.Load();
|
||||||
|
|
||||||
// Initialize api server plugin loader
|
// Search up assemblies for the apiServer
|
||||||
PluginLoaderService = new PluginLoaderService(
|
|
||||||
LoggerFactory.CreateLogger<PluginLoaderService>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Search up entrypoints and assemblies for the apiServer
|
|
||||||
var assemblyFiles = PluginService.GetAssemblies("apiServer")
|
var assemblyFiles = PluginService.GetAssemblies("apiServer")
|
||||||
.Values
|
.Values
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var entrypoints = PluginService.GetEntrypoints("apiServer");
|
// Create the load context and add assemblies
|
||||||
|
PluginLoadContext = new AssemblyLoadContext(null);
|
||||||
|
|
||||||
// Build source from the retrieved data
|
foreach (var assemblyFile in assemblyFiles)
|
||||||
PluginLoaderService.AddFilesSource(assemblyFiles, entrypoints);
|
{
|
||||||
|
try
|
||||||
// Perform assembly loading
|
{
|
||||||
await PluginLoaderService.Load();
|
PluginLoadContext.LoadFromAssemblyPath(assemblyFile);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError("Unable to load plugin assembly '{assemblyFile}': {e}", assemblyFile, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task InitializePlugins()
|
private Task InitializePlugins()
|
||||||
@@ -285,9 +226,13 @@ public class Startup
|
|||||||
// Configure base services for initialisation
|
// Configure base services for initialisation
|
||||||
startupSc.AddSingleton(Configuration);
|
startupSc.AddSingleton(Configuration);
|
||||||
|
|
||||||
BundleService = new BundleService();
|
// Add bundle service so plugins can do additional bundling if required
|
||||||
startupSc.AddSingleton(BundleService);
|
startupSc.AddSingleton(BundleService);
|
||||||
|
|
||||||
|
// Auto add all files specified in the bundledStyles section to the bundle job
|
||||||
|
foreach (var plugin in PluginService.LoadedPlugins.Keys)
|
||||||
|
BundleService.BundleCssRange(plugin.BundledStyles);
|
||||||
|
|
||||||
startupSc.AddLogging(builder =>
|
startupSc.AddLogging(builder =>
|
||||||
{
|
{
|
||||||
builder.ClearProviders();
|
builder.ClearProviders();
|
||||||
@@ -304,7 +249,7 @@ public class Startup
|
|||||||
var assembliesToScan = new List<Assembly>();
|
var assembliesToScan = new List<Assembly>();
|
||||||
|
|
||||||
assembliesToScan.Add(typeof(Startup).Assembly);
|
assembliesToScan.Add(typeof(Startup).Assembly);
|
||||||
assembliesToScan.AddRange(PluginLoaderService.PluginAssemblies);
|
assembliesToScan.AddRange(PluginLoadContext.Assemblies);
|
||||||
assembliesToScan.AddRange(AdditionalAssemblies);
|
assembliesToScan.AddRange(AdditionalAssemblies);
|
||||||
|
|
||||||
foreach (var pluginAssembly in assembliesToScan)
|
foreach (var pluginAssembly in assembliesToScan)
|
||||||
@@ -348,7 +293,7 @@ public class Startup
|
|||||||
|
|
||||||
WebApplication.UseStaticFiles(new StaticFileOptions()
|
WebApplication.UseStaticFiles(new StaticFileOptions()
|
||||||
{
|
{
|
||||||
FileProvider = new PluginAssetFileProvider(PluginService)
|
FileProvider = PluginService.WwwRootFileProvider
|
||||||
});
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
using System.Runtime.Loader;
|
|
||||||
using MoonCore.Plugins;
|
|
||||||
using Moonlight.Shared.Misc;
|
|
||||||
|
|
||||||
namespace Moonlight.Client.Implementations;
|
|
||||||
|
|
||||||
public class RemotePluginSource : IPluginSource
|
|
||||||
{
|
|
||||||
private readonly FrontendConfiguration Configuration;
|
|
||||||
private readonly ILogger<RemotePluginSource> Logger;
|
|
||||||
private readonly HttpClient HttpClient;
|
|
||||||
|
|
||||||
public RemotePluginSource(
|
|
||||||
FrontendConfiguration configuration,
|
|
||||||
ILogger<RemotePluginSource> logger,
|
|
||||||
HttpClient httpClient
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Configuration = configuration;
|
|
||||||
Logger = logger;
|
|
||||||
HttpClient = httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Load(AssemblyLoadContext loadContext, List<string> entrypoints)
|
|
||||||
{
|
|
||||||
entrypoints.AddRange(Configuration.Plugins.Entrypoints);
|
|
||||||
|
|
||||||
foreach (var assembly in Configuration.Plugins.Assemblies)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileStream = await HttpClient.GetStreamAsync($"plugins/{assembly}");
|
|
||||||
loadContext.LoadFromStream(fileStream);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogCritical("Unable to load plugin assembly '{assembly}': {e}", assembly, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,5 @@ namespace Moonlight.Client.Services;
|
|||||||
|
|
||||||
public class ApplicationAssemblyService
|
public class ApplicationAssemblyService
|
||||||
{
|
{
|
||||||
public Assembly[] AdditionalAssemblies { get; set; }
|
public List<Assembly> Assemblies { get; set; } = new();
|
||||||
public Assembly[] PluginAssemblies { get; set; }
|
|
||||||
public Assembly[] NavigationAssemblies => PluginAssemblies.Concat(AdditionalAssemblies).ToArray();
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Runtime.Loader;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
@@ -34,20 +35,15 @@ public class Startup
|
|||||||
private WebAssemblyHost WebAssemblyHost;
|
private WebAssemblyHost WebAssemblyHost;
|
||||||
|
|
||||||
// Plugin Loading
|
// Plugin Loading
|
||||||
private PluginLoaderService PluginLoaderService;
|
private AssemblyLoadContext PluginLoadContext;
|
||||||
private ApplicationAssemblyService ApplicationAssemblyService;
|
private Assembly[] AdditionalAssemblies;
|
||||||
|
|
||||||
private IPluginStartup[] PluginStartups;
|
private IPluginStartup[] PluginStartups;
|
||||||
|
|
||||||
public async Task Run(string[] args, Assembly[]? assemblies = null)
|
public async Task Run(string[] args, Assembly[]? additionalAssemblies = null)
|
||||||
{
|
{
|
||||||
Args = args;
|
Args = args;
|
||||||
|
AdditionalAssemblies = additionalAssemblies ?? [];
|
||||||
// Setup assembly storage
|
|
||||||
ApplicationAssemblyService = new()
|
|
||||||
{
|
|
||||||
AdditionalAssemblies = assemblies ?? []
|
|
||||||
};
|
|
||||||
|
|
||||||
await PrintVersion();
|
await PrintVersion();
|
||||||
await SetupLogging();
|
await SetupLogging();
|
||||||
@@ -174,11 +170,6 @@ public class Startup
|
|||||||
|
|
||||||
private async Task LoadPlugins()
|
private async Task LoadPlugins()
|
||||||
{
|
{
|
||||||
// Initialize api server plugin loader
|
|
||||||
PluginLoaderService = new PluginLoaderService(
|
|
||||||
LoggerFactory.CreateLogger<PluginLoaderService>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create everything required to stream plugins
|
// Create everything required to stream plugins
|
||||||
using var clientForStreaming = new HttpClient();
|
using var clientForStreaming = new HttpClient();
|
||||||
|
|
||||||
@@ -187,19 +178,21 @@ public class Startup
|
|||||||
: WebAssemblyHostBuilder.HostEnvironment.BaseAddress
|
: WebAssemblyHostBuilder.HostEnvironment.BaseAddress
|
||||||
);
|
);
|
||||||
|
|
||||||
PluginLoaderService.AddSource(new RemotePluginSource(
|
PluginLoadContext = new AssemblyLoadContext(null);
|
||||||
Configuration,
|
|
||||||
LoggerFactory.CreateLogger<RemotePluginSource>(),
|
|
||||||
clientForStreaming
|
|
||||||
));
|
|
||||||
|
|
||||||
// Perform assembly loading
|
foreach (var assembly in Configuration.Assemblies)
|
||||||
await PluginLoaderService.Load();
|
{
|
||||||
|
var assemblyStream = await clientForStreaming.GetStreamAsync($"plugins/{assembly}");
|
||||||
|
PluginLoadContext.LoadFromStream(assemblyStream);
|
||||||
|
}
|
||||||
|
|
||||||
// Add plugin loader service to di for the Router/App.razor
|
// Add application assembly service
|
||||||
ApplicationAssemblyService.PluginAssemblies = PluginLoaderService.PluginAssemblies;
|
var appAssemblyService = new ApplicationAssemblyService();
|
||||||
|
|
||||||
WebAssemblyHostBuilder.Services.AddSingleton(ApplicationAssemblyService);
|
appAssemblyService.Assemblies.AddRange(AdditionalAssemblies);
|
||||||
|
appAssemblyService.Assemblies.AddRange(PluginLoadContext.Assemblies);
|
||||||
|
|
||||||
|
WebAssemblyHostBuilder.Services.AddSingleton(appAssemblyService);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task InitializePlugins()
|
private Task InitializePlugins()
|
||||||
@@ -224,8 +217,8 @@ public class Startup
|
|||||||
var assembliesToScan = new List<Assembly>();
|
var assembliesToScan = new List<Assembly>();
|
||||||
|
|
||||||
assembliesToScan.Add(typeof(Startup).Assembly);
|
assembliesToScan.Add(typeof(Startup).Assembly);
|
||||||
assembliesToScan.AddRange(PluginLoaderService.PluginAssemblies);
|
assembliesToScan.AddRange(AdditionalAssemblies);
|
||||||
assembliesToScan.AddRange(ApplicationAssemblyService.AdditionalAssemblies);
|
assembliesToScan.AddRange(PluginLoadContext.Assemblies);
|
||||||
|
|
||||||
foreach (var pluginAssembly in assembliesToScan)
|
foreach (var pluginAssembly in assembliesToScan)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
|
|
||||||
<ApplicationRouter DefaultLayout="@typeof(MainLayout)"
|
<ApplicationRouter DefaultLayout="@typeof(MainLayout)"
|
||||||
AppAssembly="@typeof(App).Assembly"
|
AppAssembly="@typeof(App).Assembly"
|
||||||
AdditionalAssemblies="ApplicationAssemblyService.NavigationAssemblies" />
|
AdditionalAssemblies="ApplicationAssemblyService.Assemblies" />
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
|
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
|
||||||
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-800/60 px-6 pb-4">
|
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-800/60 px-6 pb-4">
|
||||||
<div class="flex h-16 shrink-0 items-center">
|
<div class="flex h-16 shrink-0 items-center">
|
||||||
<img class="h-8 w-auto" src="https://gamecp.masuowo.xyz/api/core/asset/Core/svg/logo.svg"
|
<img class="h-8 w-auto" src="/svg/logo.svg"
|
||||||
alt="Your Company">
|
alt="Your Company">
|
||||||
</div>
|
</div>
|
||||||
<nav class="flex flex-1 flex-col">
|
<nav class="flex flex-1 flex-col">
|
||||||
|
|||||||
@@ -7,16 +7,11 @@ public class FrontendConfiguration
|
|||||||
public string HostEnvironment { get; set; }
|
public string HostEnvironment { get; set; }
|
||||||
public ThemeData Theme { get; set; } = new();
|
public ThemeData Theme { get; set; } = new();
|
||||||
public string[] Scripts { get; set; }
|
public string[] Scripts { get; set; }
|
||||||
public PluginData Plugins { get; set; } = new();
|
public string[] Styles { get; set; }
|
||||||
|
public string[] Assemblies { get; set; }
|
||||||
|
|
||||||
public class ThemeData
|
public class ThemeData
|
||||||
{
|
{
|
||||||
public Dictionary<string, string> Variables { get; set; } = new();
|
public Dictionary<string, string> Variables { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PluginData
|
|
||||||
{
|
|
||||||
public string[] Assemblies { get; set; }
|
|
||||||
public string[] Entrypoints { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user