Reworked client assembly loading.

This commit is contained in:
Harrison Deng 2021-05-25 20:49:22 -05:00
parent 36ae3e5c99
commit 862fbe15ed
12 changed files with 180 additions and 116 deletions

View File

@ -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())
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))
{
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)
{
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;
foreach (string assemblyName in context.UseCounter.Keys)
{
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);
}
}
}

View File

@ -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
{

View File

@ -1,6 +1,6 @@
using MultiShop.Shared.Models;
namespace MultiShop.Client.Extensions.Models
namespace MultiShop.Client.Extensions
{
public static class SearchProfileExtensions
{

View 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}\".");
}
}
}

View File

@ -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">

View File

@ -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;

View 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");
}
}
}

View File

@ -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");
}
}
}

View File

@ -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; }
}

View File

@ -1,5 +1,5 @@
{
"Shops": {
"ShopModules": {
"Directory": "modules",
"Disabled": [
]