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.Net.Http.Json;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using MultiShop.Client.Module;
|
||||||
using MultiShop.Shop.Framework;
|
using MultiShop.Shop.Framework;
|
||||||
using SimpleLogger;
|
using SimpleLogger;
|
||||||
|
|
||||||
@ -15,99 +16,84 @@ namespace MultiShop.Client
|
|||||||
private bool modulesLoaded = false;
|
private bool modulesLoaded = false;
|
||||||
|
|
||||||
private Dictionary<string, IShop> shops = new Dictionary<string, IShop>();
|
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()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await DownloadShopModules();
|
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
|
await DownloadShopModules();
|
||||||
}
|
}
|
||||||
private async Task DownloadShopModules()
|
private async Task DownloadShopModules()
|
||||||
{
|
{
|
||||||
HttpClient http = HttpFactory.CreateClient("Public-MultiShop.ServerAPI");
|
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);
|
Logger.Log($"Downloading shop: {moduleName}", LogLevel.Debug);
|
||||||
downloadTasks.Add(http.GetByteArrayAsync(Path.Join("ShopModules", assemblyFileName)), assemblyFileName);
|
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);
|
Task<byte[]> downloadTask = await Task.WhenAny(downloadTasks.Keys);
|
||||||
string assemblyFileName = downloadTasks[data];
|
assemblyData.Add(downloadTasks[downloadTask], await downloadTask);
|
||||||
Logger.Log($"\"{assemblyFileName}\" completed downloading.", LogLevel.Debug);
|
Logger.Log($"Shop module \"{downloadTasks[downloadTask]}\" completed downloading.", LogLevel.Debug);
|
||||||
assemblyData.Add(assemblyFileName, data.Result);
|
downloadTasks.Remove(downloadTask);
|
||||||
downloadTasks.Remove(data);
|
|
||||||
}
|
}
|
||||||
|
Logger.Log($"Downloaded {assemblyData.Count} assemblies in total.");
|
||||||
|
|
||||||
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyDependencyRequest;
|
ShopModuleLoadContext context = new ShopModuleLoadContext(assemblyData);
|
||||||
|
Logger.Log("Beginning to load shop modules.");
|
||||||
foreach (string assemblyFileName in assemblyData.Keys)
|
foreach (string moduleName in moduleNames)
|
||||||
{
|
{
|
||||||
Assembly assembly = AppDomain.CurrentDomain.Load(assemblyData[assemblyFileName]);
|
Logger.Log($"Attempting to load shop module: \"{moduleName}\"", LogLevel.Debug);
|
||||||
bool used = false;
|
Assembly moduleAssembly = context.LoadFromAssemblyName(new AssemblyName(moduleName));
|
||||||
foreach (Type type in assembly.GetTypes())
|
bool shopLoaded = false;
|
||||||
{
|
foreach (Type type in moduleAssembly.GetTypes())
|
||||||
if (typeof(IShop).IsAssignableFrom(type))
|
|
||||||
{
|
{
|
||||||
|
if (typeof(IShop).IsAssignableFrom(type)) {
|
||||||
IShop shop = Activator.CreateInstance(type) as IShop;
|
IShop shop = Activator.CreateInstance(type) as IShop;
|
||||||
if (shop != null)
|
if (shop != null) {
|
||||||
{
|
shopLoaded = true;
|
||||||
shop.Initialize();
|
shop.Initialize();
|
||||||
shops.Add(shop.ShopName, shop);
|
shops.Add(shop.ShopName, shop);
|
||||||
Logger.Log($"Registered and started lifetime of module for \"{shop.ShopName}\".", LogLevel.Debug);
|
Logger.Log($"Added shop: {shop.ShopName}", LogLevel.Debug);
|
||||||
used = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!used) {
|
if (!shopLoaded) {
|
||||||
Logger.Log($"Since unused, caching \"{assemblyFileName}\".", LogLevel.Debug);
|
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);
|
||||||
assemblyCache.Add(assemblyFileName, assembly);
|
|
||||||
}
|
}
|
||||||
assemblyData.Remove(assemblyFileName);
|
|
||||||
}
|
}
|
||||||
foreach (string assembly in assemblyData.Keys)
|
Logger.Log($"Shop module loading complete. Loaded a total of {shops.Count} shops.");
|
||||||
{
|
|
||||||
Logger.Log($"\"{assembly}\" was unused.", LogLevel.Warning);
|
|
||||||
}
|
|
||||||
foreach (string assembly in assemblyCache.Keys)
|
|
||||||
{
|
|
||||||
Logger.Log($"\"{assembly}\" was unused.", LogLevel.Warning);
|
|
||||||
}
|
|
||||||
assemblyData.Clear();
|
|
||||||
assemblyCache.Clear();
|
|
||||||
modulesLoaded = true;
|
modulesLoaded = true;
|
||||||
}
|
foreach (string assemblyName in context.UseCounter.Keys)
|
||||||
|
|
||||||
|
|
||||||
private Assembly OnAssemblyDependencyRequest(object sender, ResolveEventArgs args)
|
|
||||||
{
|
{
|
||||||
string dependencyName = args.Name.Substring(0, args.Name.IndexOf(','));
|
int usage = context.UseCounter[assemblyName];
|
||||||
Logger.Log($"Assembly \"{args.RequestingAssembly.GetName().Name}\" is requesting dependency assembly \"{dependencyName}\".", LogLevel.Debug);
|
Logger.Log($"\"{assemblyName}\" was used {usage} times.", LogLevel.Debug);
|
||||||
if (assemblyCache.ContainsKey(dependencyName)) {
|
if (usage <= 0) {
|
||||||
Logger.Log($"Found \"{dependencyName}\" in cache.", LogLevel.Debug);
|
Logger.Log($"\"{assemblyName}\" was not used at all.", LogLevel.Warning);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
foreach (string name in shops.Keys)
|
foreach (string name in shops.Keys)
|
||||||
{
|
{
|
||||||
shops[name].Dispose();
|
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;
|
||||||
using MultiShop.Shared.Models;
|
using MultiShop.Shared.Models;
|
||||||
|
|
||||||
namespace MultiShop.Client.Extensions.Models
|
namespace MultiShop.Client.Extensions
|
||||||
{
|
{
|
||||||
public static class ResultProfileExtensions
|
public static class ResultProfileExtensions
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using MultiShop.Shared.Models;
|
using MultiShop.Shared.Models;
|
||||||
|
|
||||||
namespace MultiShop.Client.Extensions.Models
|
namespace MultiShop.Client.Extensions
|
||||||
{
|
{
|
||||||
public static class SearchProfileExtensions
|
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?}"
|
@page "/search/{Query?}"
|
||||||
|
|
||||||
@using MultiShop.Client.Extensions.Models
|
@using MultiShop.Client.Extensions
|
||||||
|
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
<div class="input-group my-2">
|
<div class="input-group my-2">
|
||||||
|
@ -6,7 +6,7 @@ using System.Text.Json;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using MultiShop.Client.Extensions.Models;
|
using MultiShop.Client.Extensions;
|
||||||
using MultiShop.Client.Listing;
|
using MultiShop.Client.Listing;
|
||||||
using MultiShop.Shared.Models;
|
using MultiShop.Shared.Models;
|
||||||
using MultiShop.Shop.Framework;
|
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
|
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
|
//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 string Directory { get; set; }
|
||||||
public IList<string> Disabled { get; set; }
|
public IList<string> Disabled { get; set; }
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Shops": {
|
"ShopModules": {
|
||||||
"Directory": "modules",
|
"Directory": "modules",
|
||||||
"Disabled": [
|
"Disabled": [
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user