Reworked client assembly loading.
This commit is contained in:
parent
36ae3e5c99
commit
862fbe15ed
@ -5,6 +5,7 @@ using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using MultiShop.Client.Module;
|
||||
using MultiShop.Shop.Framework;
|
||||
using SimpleLogger;
|
||||
|
||||
@ -15,99 +16,84 @@ namespace MultiShop.Client
|
||||
private bool modulesLoaded = false;
|
||||
|
||||
private Dictionary<string, IShop> shops = new Dictionary<string, IShop>();
|
||||
private Dictionary<string, byte[]> assemblyData = new Dictionary<string, byte[]>();
|
||||
private Dictionary<string, Assembly> assemblyCache = new Dictionary<string, Assembly>();
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await DownloadShopModules();
|
||||
await base.OnInitializedAsync();
|
||||
await DownloadShopModules();
|
||||
}
|
||||
private async Task DownloadShopModules()
|
||||
{
|
||||
HttpClient http = HttpFactory.CreateClient("Public-MultiShop.ServerAPI");
|
||||
Logger.Log($"Fetching shop modules.", LogLevel.Debug);
|
||||
string[] assemblyFileNames = await http.GetFromJsonAsync<string[]>("ShopModules");
|
||||
Dictionary<Task<byte[]>, string> downloadTasks = new Dictionary<Task<byte[]>, string>(assemblyFileNames.Length);
|
||||
|
||||
foreach (string assemblyFileName in assemblyFileNames)
|
||||
Dictionary<string, byte[]> assemblyData = new Dictionary<string, byte[]>();
|
||||
|
||||
string[] moduleNames = await http.GetFromJsonAsync<string[]>("ShopModule/Modules");
|
||||
string[] dependencyNames = await http.GetFromJsonAsync<string[]>("ShopModule/Dependencies");
|
||||
Dictionary<Task<byte[]>, string> downloadTasks = new Dictionary<Task<byte[]>, string>();
|
||||
|
||||
Logger.Log("Beginning to download shop modules...");
|
||||
foreach (string moduleName in moduleNames)
|
||||
{
|
||||
Logger.Log($"Downloading \"{assemblyFileName}\"...", LogLevel.Debug);
|
||||
downloadTasks.Add(http.GetByteArrayAsync(Path.Join("ShopModules", assemblyFileName)), assemblyFileName);
|
||||
Logger.Log($"Downloading shop: {moduleName}", LogLevel.Debug);
|
||||
downloadTasks.Add(http.GetByteArrayAsync("shopModule/Modules/" + moduleName), moduleName);
|
||||
}
|
||||
Logger.Log("Beginning to download shop module dependencies...");
|
||||
foreach (string depName in dependencyNames)
|
||||
{
|
||||
Logger.Log($"Downloading shop module dependency: {depName}", LogLevel.Debug);
|
||||
downloadTasks.Add(http.GetByteArrayAsync("ShopModule/Dependencies/" + depName), depName);
|
||||
}
|
||||
|
||||
while (downloadTasks.Count != 0)
|
||||
while (downloadTasks.Count > 0)
|
||||
{
|
||||
Task<byte[]> data = await Task.WhenAny(downloadTasks.Keys);
|
||||
string assemblyFileName = downloadTasks[data];
|
||||
Logger.Log($"\"{assemblyFileName}\" completed downloading.", LogLevel.Debug);
|
||||
assemblyData.Add(assemblyFileName, data.Result);
|
||||
downloadTasks.Remove(data);
|
||||
Task<byte[]> downloadTask = await Task.WhenAny(downloadTasks.Keys);
|
||||
assemblyData.Add(downloadTasks[downloadTask], await downloadTask);
|
||||
Logger.Log($"Shop module \"{downloadTasks[downloadTask]}\" completed downloading.", LogLevel.Debug);
|
||||
downloadTasks.Remove(downloadTask);
|
||||
}
|
||||
Logger.Log($"Downloaded {assemblyData.Count} assemblies in total.");
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyDependencyRequest;
|
||||
|
||||
foreach (string assemblyFileName in assemblyData.Keys)
|
||||
ShopModuleLoadContext context = new ShopModuleLoadContext(assemblyData);
|
||||
Logger.Log("Beginning to load shop modules.");
|
||||
foreach (string moduleName in moduleNames)
|
||||
{
|
||||
Assembly assembly = AppDomain.CurrentDomain.Load(assemblyData[assemblyFileName]);
|
||||
bool used = false;
|
||||
foreach (Type type in assembly.GetTypes())
|
||||
{
|
||||
if (typeof(IShop).IsAssignableFrom(type))
|
||||
Logger.Log($"Attempting to load shop module: \"{moduleName}\"", LogLevel.Debug);
|
||||
Assembly moduleAssembly = context.LoadFromAssemblyName(new AssemblyName(moduleName));
|
||||
bool shopLoaded = false;
|
||||
foreach (Type type in moduleAssembly.GetTypes())
|
||||
{
|
||||
if (typeof(IShop).IsAssignableFrom(type)) {
|
||||
IShop shop = Activator.CreateInstance(type) as IShop;
|
||||
if (shop != null)
|
||||
{
|
||||
if (shop != null) {
|
||||
shopLoaded = true;
|
||||
shop.Initialize();
|
||||
shops.Add(shop.ShopName, shop);
|
||||
Logger.Log($"Registered and started lifetime of module for \"{shop.ShopName}\".", LogLevel.Debug);
|
||||
used = true;
|
||||
Logger.Log($"Added shop: {shop.ShopName}", LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!used) {
|
||||
Logger.Log($"Since unused, caching \"{assemblyFileName}\".", LogLevel.Debug);
|
||||
assemblyCache.Add(assemblyFileName, assembly);
|
||||
if (!shopLoaded) {
|
||||
Logger.Log($"Module \"{moduleName}\" was reported to be a shop module, but did not contain a shop interface. Please report this to the site administrator.", LogLevel.Warning);
|
||||
}
|
||||
assemblyData.Remove(assemblyFileName);
|
||||
}
|
||||
foreach (string assembly in assemblyData.Keys)
|
||||
{
|
||||
Logger.Log($"\"{assembly}\" was unused.", LogLevel.Warning);
|
||||
}
|
||||
foreach (string assembly in assemblyCache.Keys)
|
||||
{
|
||||
Logger.Log($"\"{assembly}\" was unused.", LogLevel.Warning);
|
||||
}
|
||||
assemblyData.Clear();
|
||||
assemblyCache.Clear();
|
||||
Logger.Log($"Shop module loading complete. Loaded a total of {shops.Count} shops.");
|
||||
modulesLoaded = true;
|
||||
}
|
||||
|
||||
|
||||
private Assembly OnAssemblyDependencyRequest(object sender, ResolveEventArgs args)
|
||||
foreach (string assemblyName in context.UseCounter.Keys)
|
||||
{
|
||||
string dependencyName = args.Name.Substring(0, args.Name.IndexOf(','));
|
||||
Logger.Log($"Assembly \"{args.RequestingAssembly.GetName().Name}\" is requesting dependency assembly \"{dependencyName}\".", LogLevel.Debug);
|
||||
if (assemblyCache.ContainsKey(dependencyName)) {
|
||||
Logger.Log($"Found \"{dependencyName}\" in cache.", LogLevel.Debug);
|
||||
Assembly dep = assemblyCache[dependencyName];
|
||||
assemblyCache.Remove(dependencyName);
|
||||
return dep;
|
||||
} else if (assemblyData.ContainsKey(dependencyName)) {
|
||||
return AppDomain.CurrentDomain.Load(assemblyData[dependencyName]);
|
||||
} else {
|
||||
Logger.Log($"No dependency under name \"{args.Name}\"", LogLevel.Warning);
|
||||
return null;
|
||||
int usage = context.UseCounter[assemblyName];
|
||||
Logger.Log($"\"{assemblyName}\" was used {usage} times.", LogLevel.Debug);
|
||||
if (usage <= 0) {
|
||||
Logger.Log($"\"{assemblyName}\" was not used at all.", LogLevel.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (string name in shops.Keys)
|
||||
{
|
||||
shops[name].Dispose();
|
||||
Logger.Log($"Ending lifetime of shop module for \"{name}\".");
|
||||
Logger.Log($"Ending lifetime of shop module for \"{name}\".", LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ using System;
|
||||
using MultiShop.Shared;
|
||||
using MultiShop.Shared.Models;
|
||||
|
||||
namespace MultiShop.Client.Extensions.Models
|
||||
namespace MultiShop.Client.Extensions
|
||||
{
|
||||
public static class ResultProfileExtensions
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using MultiShop.Shared.Models;
|
||||
|
||||
namespace MultiShop.Client.Extensions.Models
|
||||
namespace MultiShop.Client.Extensions
|
||||
{
|
||||
public static class SearchProfileExtensions
|
||||
{
|
39
src/MultiShop/Client/Module/ShopModuleLoadContext.cs
Normal file
39
src/MultiShop/Client/Module/ShopModuleLoadContext.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using SimpleLogger;
|
||||
|
||||
namespace MultiShop.Client.Module
|
||||
{
|
||||
public class ShopModuleLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private IDictionary<string, byte[]> rawAssemblies;
|
||||
private Dictionary<string, int> useCounter;
|
||||
public IReadOnlyDictionary<string, int> UseCounter {get => useCounter;}
|
||||
public ShopModuleLoadContext(IEnumerable<KeyValuePair<string, byte[]>> assemblies)
|
||||
{
|
||||
this.rawAssemblies = new Dictionary<string, byte[]>(assemblies);
|
||||
useCounter = new Dictionary<string, int>();
|
||||
}
|
||||
|
||||
protected override Assembly Load(AssemblyName assemblyName)
|
||||
{
|
||||
Logger.Log("ShopModuleLoadContext is attempting to load assembly: " + assemblyName.FullName, LogLevel.Debug);
|
||||
if (!rawAssemblies.ContainsKey(assemblyName.FullName)) return null;
|
||||
|
||||
useCounter[assemblyName.FullName] = useCounter.GetValueOrDefault(assemblyName.FullName) + 1;
|
||||
|
||||
using (MemoryStream stream = new MemoryStream(rawAssemblies[assemblyName.FullName]))
|
||||
{
|
||||
return LoadFromStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||
{
|
||||
throw new NotImplementedException($"Cannot load unmanaged dll \"{unmanagedDllName}\".");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
@page "/search/{Query?}"
|
||||
|
||||
@using MultiShop.Client.Extensions.Models
|
||||
@using MultiShop.Client.Extensions
|
||||
|
||||
<div class="my-2">
|
||||
<div class="input-group my-2">
|
||||
|
@ -6,7 +6,7 @@ using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using MultiShop.Client.Extensions.Models;
|
||||
using MultiShop.Client.Extensions;
|
||||
using MultiShop.Client.Listing;
|
||||
using MultiShop.Shared.Models;
|
||||
using MultiShop.Shop.Framework;
|
||||
|
88
src/MultiShop/Server/Controllers/ShopModuleController.cs
Normal file
88
src/MultiShop/Server/Controllers/ShopModuleController.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MultiShop.Server.Options;
|
||||
using SimpleLogger;
|
||||
|
||||
namespace MultiShop.Server.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class ShopModuleController : ControllerBase
|
||||
{
|
||||
private readonly IConfiguration configuration;
|
||||
private IDictionary<string, byte[]> shopModules;
|
||||
private IDictionary<string, byte[]> shopModuleDependencies;
|
||||
|
||||
|
||||
public ShopModuleController(IConfiguration configuration)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.shopModules = new Dictionary<string, byte[]>();
|
||||
this.shopModuleDependencies = new Dictionary<string, byte[]>();
|
||||
|
||||
ShopModulesOptions options = configuration.GetSection(ShopModulesOptions.ShopModules).Get<ShopModulesOptions>();
|
||||
foreach (string file in Directory.EnumerateFiles(options.Directory))
|
||||
{
|
||||
try
|
||||
{
|
||||
AssemblyName assemblyName = AssemblyName.GetAssemblyName(file);
|
||||
shopModules.Add(assemblyName.FullName, System.IO.File.ReadAllBytes(file));
|
||||
}
|
||||
catch (BadImageFormatException e) {
|
||||
Logger.Log($"\"{e.FileName}\" is not a valid assembly. Ignoring.", LogLevel.Warning);
|
||||
}
|
||||
catch (ArgumentException) {
|
||||
Logger.Log($"\"{Path.GetFileName(file)}\" has the same full name as another assembly. Ignoring this one.", LogLevel.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string file in Directory.EnumerateFiles(Path.Join(options.Directory, "dependencies")))
|
||||
{
|
||||
try
|
||||
{
|
||||
AssemblyName assemblyName = AssemblyName.GetAssemblyName(file);
|
||||
shopModuleDependencies.Add(assemblyName.FullName, System.IO.File.ReadAllBytes(file));
|
||||
}
|
||||
catch (BadImageFormatException e) {
|
||||
Logger.Log($"\"{e.FileName}\" is not a valid assembly. Ignoring.", LogLevel.Warning);
|
||||
}
|
||||
catch (ArgumentException) {
|
||||
Logger.Log($"\"{Path.GetFileName(file)}\" has the same full name as another assembly. Ignoring this one.", LogLevel.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Modules")]
|
||||
public IActionResult GetShopModuleNames() {
|
||||
return Ok(shopModules.Keys);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Modules/{shopModuleName}")]
|
||||
public IActionResult GetModule(string shopModuleName) {
|
||||
ShopModulesOptions options = configuration.GetSection(ShopModulesOptions.ShopModules).Get<ShopModulesOptions>();
|
||||
if (!shopModules.ContainsKey(shopModuleName)) return NotFound();
|
||||
if (options.Disabled != null && options.Disabled.Contains(shopModuleName)) return Forbid();
|
||||
return File(shopModules[shopModuleName], "application/module-dll");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Dependencies")]
|
||||
public IActionResult GetDependencyNames() {
|
||||
return Ok(shopModuleDependencies.Keys);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Dependencies/{dependencyName}")]
|
||||
public IActionResult GetDependency(string dependencyName) {
|
||||
ShopModulesOptions options = configuration.GetSection(ShopModulesOptions.ShopModules).Get<ShopModulesOptions>();
|
||||
if (!shopModuleDependencies.ContainsKey(dependencyName)) return NotFound();
|
||||
return File(shopModuleDependencies[dependencyName], "application/module-dep-dll");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MultiShop.Server.Options;
|
||||
|
||||
namespace MultiShop.Server.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class ShopModulesController : ControllerBase
|
||||
{
|
||||
private readonly IConfiguration configuration;
|
||||
private IDictionary<string, byte[]> shopAssemblyData;
|
||||
|
||||
|
||||
|
||||
public ShopModulesController(IConfiguration configuration)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.shopAssemblyData = new Dictionary<string, byte[]>();
|
||||
}
|
||||
|
||||
public IActionResult GetShopModuleNames() {
|
||||
List<string> moduleNames = new List<string>();
|
||||
ShopOptions options = configuration.GetSection(ShopOptions.Shop).Get<ShopOptions>();
|
||||
foreach (string file in Directory.EnumerateFiles(options.Directory))
|
||||
{
|
||||
if (Path.GetExtension(file).ToLower().Equals(".dll") && !(options.Disabled != null && options.Disabled.Contains(Path.GetFileNameWithoutExtension(file)))) {
|
||||
moduleNames.Add(Path.GetFileNameWithoutExtension(file));
|
||||
}
|
||||
}
|
||||
return Ok(moduleNames);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("{shopModuleName}")]
|
||||
public IActionResult GetModule(string shopModuleName) {
|
||||
ShopOptions options = configuration.GetSection(ShopOptions.Shop).Get<ShopOptions>();
|
||||
string shopPath = Path.Join(options.Directory, shopModuleName);
|
||||
shopPath += ".dll";
|
||||
if (!System.IO.File.Exists(shopPath)) return NotFound();
|
||||
if (options.Disabled != null && options.Disabled.Contains(shopModuleName)) return Forbid();
|
||||
return File(new FileStream(shopPath, FileMode.Open), "application/shop-dll");
|
||||
}
|
||||
}
|
||||
}
|
@ -3,9 +3,9 @@ using System.Collections.Generic;
|
||||
namespace MultiShop.Server.Options
|
||||
{
|
||||
//https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-5.0#bind-hierarchical-configuration-data-using-the-options-pattern
|
||||
public class ShopOptions
|
||||
public class ShopModulesOptions
|
||||
{
|
||||
public const string Shop = "Shops";
|
||||
public const string ShopModules = "ShopModules";
|
||||
public string Directory { get; set; }
|
||||
public IList<string> Disabled { get; set; }
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"Shops": {
|
||||
"ShopModules": {
|
||||
"Directory": "modules",
|
||||
"Disabled": [
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user