103 lines
6.7 KiB
C#
103 lines
6.7 KiB
C#
using MAX.Plugins;
|
|
using System.Reflection;
|
|
using MAX.Framework.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
|
|
|
if (args.ElementAtOrDefault(0) == "configuration") { ConfigurationChecks.Run(); return; }
|
|
if (args.ElementAtOrDefault(0) == "hangfire") { HangfireChecks.Run(); return; }
|
|
|
|
// Run each scenario in a fresh process: default ALC intentionally cannot unload plugins.
|
|
var finderType = typeof(ServiceCollectionExtensions).Assembly.GetType("MAX.Framework.DependencyInjection.StoreAppTypeFinder", true)!;
|
|
var finder = finderType.GetProperty("Current")!.GetValue(null);
|
|
var method = finderType.GetMethod("GetAssemblies", new[] { typeof(string) })!;
|
|
var mode = args.ElementAtOrDefault(1) ?? "plugins";
|
|
List<Assembly> assemblies;
|
|
try
|
|
{
|
|
assemblies = (List<Assembly>)method.Invoke(finder, new object[] { args[0] == "-" ? null : args[0] })!;
|
|
}
|
|
catch (TargetInvocationException ex) when (mode == "failure" && ex.InnerException is InvalidOperationException)
|
|
{
|
|
Console.WriteLine("PASS expected loading failure: " + ex.InnerException.Message);
|
|
return;
|
|
}
|
|
if (mode == "failure") throw new Exception("Expected loading failure");
|
|
var plugins = assemblies.Where(a => a.GetName().Name is "Store.CloudProduct" or "Store.ImageCache" or "Store.ProductInspection").ToArray();
|
|
if (mode == "empty")
|
|
{
|
|
if (plugins.Length != 0) throw new Exception("Unexpected plugins");
|
|
Console.WriteLine("PASS empty/missing directory");
|
|
return;
|
|
}
|
|
if (plugins.Length != (mode == "image" ? 1 : 2)) throw new Exception("Unexpected plugin count");
|
|
if (assemblies.Select(a => a.FullName).Distinct().Count() != assemblies.Count) throw new Exception("Duplicate assemblies");
|
|
var mvc = new ServiceCollection().AddControllers();
|
|
foreach (var plugin in plugins) mvc.AddApplicationPart(plugin);
|
|
var feature = new ControllerFeature();
|
|
mvc.PartManager.PopulateFeature(feature);
|
|
if (!feature.Controllers.Any(t => t.Name == "ImageCacheController")) throw new Exception("ImageCache controller missing");
|
|
if (mode != "image" && !feature.Controllers.Any(t => t.Name == "GoodsController")) throw new Exception("Goods controller missing");
|
|
if (mode is "image" or "plugins")
|
|
{
|
|
var host = Host.CreateApplicationBuilder();
|
|
host.Services.AddHttpContextAccessor();
|
|
host.Services.AddScoped<MAX.Framework.Security.ICurrentUser, MAX.Framework.Security.CurrentUser>();
|
|
var core = typeof(ServiceCollectionExtensions).GetMethod("AddStoreCore", BindingFlags.Static | BindingFlags.NonPublic)!;
|
|
host.Configuration["Search:Url"] = "https://host.invalid";
|
|
host.Configuration["ImageCache:ConnectionString"] = "host-only";
|
|
core.Invoke(null, new object[] { host, plugins, new Dictionary<Type, object>() });
|
|
if (host.Services.Any(d => d.ServiceType == typeof(Hangfire.JobStorage)))
|
|
throw new Exception("Plugins must not register shared Hangfire storage");
|
|
using var built = host.Build();
|
|
foreach (var pluginType in plugins.SelectMany(a => a.GetTypes()).Where(t => !t.IsAbstract && typeof(IPlugin).IsAssignableFrom(t)))
|
|
{
|
|
var contextType = typeof(PluginContext<>).MakeGenericType(pluginType);
|
|
// 先解析泛型入口,确认无需提前解析键名上下文。
|
|
var typed = built.Services.GetRequiredService(contextType);
|
|
var id = (string)contextType.GetProperty("Id")!.GetValue(typed)!;
|
|
var keyed = built.Services.GetRequiredKeyedService<PluginContext>(id);
|
|
if (!ReferenceEquals(contextType.GetProperty("Configuration")!.GetValue(typed), keyed.Configuration)
|
|
|| !Equals(contextType.GetProperty("DirectoryPath")!.GetValue(typed), keyed.DirectoryPath)
|
|
|| !ReferenceEquals(contextType.GetProperty("Environment")!.GetValue(typed), keyed.Environment))
|
|
throw new Exception("Typed context must share the plugin configuration and environment");
|
|
using var scope = built.Services.CreateScope();
|
|
if (!ReferenceEquals(typed, scope.ServiceProvider.GetRequiredService(contextType)))
|
|
throw new Exception("Typed plugin context must be singleton");
|
|
}
|
|
var image = built.Services.GetRequiredKeyedService<PluginContext>("ImageCache");
|
|
if (image.Configuration["ImageCache:ConnectionString"] is null or "host-only") throw new Exception("Image config isolation failed");
|
|
if (image.Configuration["Search:Url"] != null) throw new Exception("Host settings leaked into ImageCache");
|
|
if (host.Configuration["ImageCache:ConnectionString"] != "host-only") throw new Exception("Plugin modified host configuration");
|
|
if (mode == "plugins")
|
|
{
|
|
var inspection = plugins.Single(p => p.GetName().Name == "Store.CloudProduct");
|
|
using (var scope = built.Services.CreateScope())
|
|
{
|
|
scope.ServiceProvider.GetRequiredService(inspection.GetTypes().Single(t => t.Name == "ProductInspectionService"));
|
|
scope.ServiceProvider.GetRequiredService(inspection.GetTypes().Single(t => t.Name == "QwenInspectionIntegration"));
|
|
}
|
|
if (!feature.Controllers.Any(t => t.Name == "ProductInspectionController"))
|
|
throw new Exception("Product inspection controller missing");
|
|
var cloud = built.Services.GetRequiredKeyedService<PluginContext>("CloudProduct");
|
|
if (cloud.Configuration["Search:Url"] is null or "https://host.invalid") throw new Exception("Cloud config isolation failed");
|
|
if (host.Configuration["Search:Url"] != "https://host.invalid") throw new Exception("Cloud modified host configuration");
|
|
foreach (var name in new[] { "OpenSearchService", "UserFavoritesIndexService", "RawDataStorage", "GoodsIndexJob", "GoodsViolationIndexJob", "GoodsService" })
|
|
{
|
|
var type = plugins.SelectMany(p => p.GetTypes()).Single(t => t.Name == name);
|
|
using var scope = built.Services.CreateScope();
|
|
scope.ServiceProvider.GetRequiredService(type);
|
|
}
|
|
}
|
|
|
|
if (!host.Services.Any(d => d.ServiceType.Name == "ImageCacheService")) throw new Exception("Plugin DI registration missing");
|
|
if (!host.Services.Any(d => d.ServiceKey is string key && key == "ImageCacheConnection")) throw new Exception("Plugin database registration missing");
|
|
var registeredMvc = built.Services.GetRequiredService<ApplicationPartManager>();
|
|
var registered = new ControllerFeature();
|
|
registeredMvc.PopulateFeature(registered);
|
|
if (!registered.Controllers.Any(t => t.Name == "ImageCacheController")) throw new Exception("Core MVC registration missing");
|
|
}
|
|
Console.WriteLine($"PASS plugins={plugins.Length}, controllers={feature.Controllers.Count}; dependencies/types loaded" + (mode is "image" or "plugins" ? "; isolated configuration/DI/plugin lifecycle/MVC registered" : ""));
|