From 862fbe15ed493dc82a5217c08986d2f44abe1f4f Mon Sep 17 00:00:00 2001 From: Harrison Deng Date: Tue, 25 May 2021 20:49:22 -0500 Subject: [PATCH] Reworked client assembly loading. --- src/MultiShop/Client/App.razor.cs | 106 ++++++++---------- .../{Models => }/ResultProfileExtensions.cs | 2 +- .../{Models => }/SearchProfileExtensions.cs | 2 +- .../Client/Module/ShopModuleLoadContext.cs | 39 +++++++ src/MultiShop/Client/Pages/Search.razor | 2 +- src/MultiShop/Client/Pages/Search.razor.cs | 2 +- .../Controllers/ShopModuleController.cs | 88 +++++++++++++++ .../Controllers/ShopModulesController.cs | 49 -------- ...{ShopsOptions.cs => ShopModulesOptions.cs} | 4 +- src/MultiShop/Server/appsettings.json | 2 +- .../{ => dependencies}/HtmlAgilityPack.dll | Bin .../{ => dependencies}/SimpleLogger.dll | Bin 12 files changed, 180 insertions(+), 116 deletions(-) rename src/MultiShop/Client/Extensions/{Models => }/ResultProfileExtensions.cs (97%) rename src/MultiShop/Client/Extensions/{Models => }/SearchProfileExtensions.cs (68%) create mode 100644 src/MultiShop/Client/Module/ShopModuleLoadContext.cs create mode 100644 src/MultiShop/Server/Controllers/ShopModuleController.cs delete mode 100644 src/MultiShop/Server/Controllers/ShopModulesController.cs rename src/MultiShop/Server/Options/{ShopsOptions.cs => ShopModulesOptions.cs} (78%) rename src/MultiShop/Server/modules/{ => dependencies}/HtmlAgilityPack.dll (100%) rename src/MultiShop/Server/modules/{ => dependencies}/SimpleLogger.dll (100%) diff --git a/src/MultiShop/Client/App.razor.cs b/src/MultiShop/Client/App.razor.cs index 0c66464..e0d1749 100644 --- a/src/MultiShop/Client/App.razor.cs +++ b/src/MultiShop/Client/App.razor.cs @@ -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 shops = new Dictionary(); - private Dictionary assemblyData = new Dictionary(); - private Dictionary assemblyCache = new Dictionary(); 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("ShopModules"); - Dictionary, string> downloadTasks = new Dictionary, string>(assemblyFileNames.Length); - foreach (string assemblyFileName in assemblyFileNames) + Dictionary assemblyData = new Dictionary(); + + string[] moduleNames = await http.GetFromJsonAsync("ShopModule/Modules"); + string[] dependencyNames = await http.GetFromJsonAsync("ShopModule/Dependencies"); + Dictionary, string> downloadTasks = new Dictionary, 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 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 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); } } } diff --git a/src/MultiShop/Client/Extensions/Models/ResultProfileExtensions.cs b/src/MultiShop/Client/Extensions/ResultProfileExtensions.cs similarity index 97% rename from src/MultiShop/Client/Extensions/Models/ResultProfileExtensions.cs rename to src/MultiShop/Client/Extensions/ResultProfileExtensions.cs index ae315c6..607c7d2 100644 --- a/src/MultiShop/Client/Extensions/Models/ResultProfileExtensions.cs +++ b/src/MultiShop/Client/Extensions/ResultProfileExtensions.cs @@ -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 { diff --git a/src/MultiShop/Client/Extensions/Models/SearchProfileExtensions.cs b/src/MultiShop/Client/Extensions/SearchProfileExtensions.cs similarity index 68% rename from src/MultiShop/Client/Extensions/Models/SearchProfileExtensions.cs rename to src/MultiShop/Client/Extensions/SearchProfileExtensions.cs index 10c9ae8..5f7e109 100644 --- a/src/MultiShop/Client/Extensions/Models/SearchProfileExtensions.cs +++ b/src/MultiShop/Client/Extensions/SearchProfileExtensions.cs @@ -1,6 +1,6 @@ using MultiShop.Shared.Models; -namespace MultiShop.Client.Extensions.Models +namespace MultiShop.Client.Extensions { public static class SearchProfileExtensions { diff --git a/src/MultiShop/Client/Module/ShopModuleLoadContext.cs b/src/MultiShop/Client/Module/ShopModuleLoadContext.cs new file mode 100644 index 0000000..221c407 --- /dev/null +++ b/src/MultiShop/Client/Module/ShopModuleLoadContext.cs @@ -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 rawAssemblies; + private Dictionary useCounter; + public IReadOnlyDictionary UseCounter {get => useCounter;} + public ShopModuleLoadContext(IEnumerable> assemblies) + { + this.rawAssemblies = new Dictionary(assemblies); + useCounter = new Dictionary(); + } + + 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}\"."); + } + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/Pages/Search.razor b/src/MultiShop/Client/Pages/Search.razor index 027bca2..5494b71 100644 --- a/src/MultiShop/Client/Pages/Search.razor +++ b/src/MultiShop/Client/Pages/Search.razor @@ -1,6 +1,6 @@ @page "/search/{Query?}" -@using MultiShop.Client.Extensions.Models +@using MultiShop.Client.Extensions
diff --git a/src/MultiShop/Client/Pages/Search.razor.cs b/src/MultiShop/Client/Pages/Search.razor.cs index 1674874..7fbc81a 100644 --- a/src/MultiShop/Client/Pages/Search.razor.cs +++ b/src/MultiShop/Client/Pages/Search.razor.cs @@ -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; diff --git a/src/MultiShop/Server/Controllers/ShopModuleController.cs b/src/MultiShop/Server/Controllers/ShopModuleController.cs new file mode 100644 index 0000000..5be2c2e --- /dev/null +++ b/src/MultiShop/Server/Controllers/ShopModuleController.cs @@ -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 shopModules; + private IDictionary shopModuleDependencies; + + + public ShopModuleController(IConfiguration configuration) + { + this.configuration = configuration; + this.shopModules = new Dictionary(); + this.shopModuleDependencies = new Dictionary(); + + ShopModulesOptions options = configuration.GetSection(ShopModulesOptions.ShopModules).Get(); + 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(); + 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(); + if (!shopModuleDependencies.ContainsKey(dependencyName)) return NotFound(); + return File(shopModuleDependencies[dependencyName], "application/module-dep-dll"); + } + } +} \ No newline at end of file diff --git a/src/MultiShop/Server/Controllers/ShopModulesController.cs b/src/MultiShop/Server/Controllers/ShopModulesController.cs deleted file mode 100644 index 30976c0..0000000 --- a/src/MultiShop/Server/Controllers/ShopModulesController.cs +++ /dev/null @@ -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 shopAssemblyData; - - - - public ShopModulesController(IConfiguration configuration) - { - this.configuration = configuration; - this.shopAssemblyData = new Dictionary(); - } - - public IActionResult GetShopModuleNames() { - List moduleNames = new List(); - ShopOptions options = configuration.GetSection(ShopOptions.Shop).Get(); - 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(); - 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"); - } - } -} \ No newline at end of file diff --git a/src/MultiShop/Server/Options/ShopsOptions.cs b/src/MultiShop/Server/Options/ShopModulesOptions.cs similarity index 78% rename from src/MultiShop/Server/Options/ShopsOptions.cs rename to src/MultiShop/Server/Options/ShopModulesOptions.cs index 894ee64..531f264 100644 --- a/src/MultiShop/Server/Options/ShopsOptions.cs +++ b/src/MultiShop/Server/Options/ShopModulesOptions.cs @@ -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 Disabled { get; set; } } diff --git a/src/MultiShop/Server/appsettings.json b/src/MultiShop/Server/appsettings.json index 7b1030b..2547378 100644 --- a/src/MultiShop/Server/appsettings.json +++ b/src/MultiShop/Server/appsettings.json @@ -1,5 +1,5 @@ { - "Shops": { + "ShopModules": { "Directory": "modules", "Disabled": [ ] diff --git a/src/MultiShop/Server/modules/HtmlAgilityPack.dll b/src/MultiShop/Server/modules/dependencies/HtmlAgilityPack.dll similarity index 100% rename from src/MultiShop/Server/modules/HtmlAgilityPack.dll rename to src/MultiShop/Server/modules/dependencies/HtmlAgilityPack.dll diff --git a/src/MultiShop/Server/modules/SimpleLogger.dll b/src/MultiShop/Server/modules/dependencies/SimpleLogger.dll similarity index 100% rename from src/MultiShop/Server/modules/SimpleLogger.dll rename to src/MultiShop/Server/modules/dependencies/SimpleLogger.dll