From e91646594feddae3efbe05950524090d5146ab0a Mon Sep 17 00:00:00 2001 From: Kwoth Date: Fri, 10 Mar 2023 01:11:43 +0100 Subject: [PATCH] Fully moved to Ninject, fixed issues with medusa (un)loadabaility --- src/NadekoBot/Bot.cs | 63 ++++----- .../Medusa/MedusaAssemblyLoadContext.cs | 27 ++-- .../Common/Medusa/MedusaIoCKernelModule.cs | 107 ++++++++++----- .../Common/Medusa/MedusaLoaderService.cs | 36 ++++-- .../Common/Medusa/Models/ResolvedMedusa.cs | 5 +- .../Common/Medusa/RemovablePlanner.cs | 122 ++++++++++++++++++ src/NadekoBot/Db/NadekoContext.cs | 12 +- src/NadekoBot/Directory.Build.props | 7 - src/NadekoBot/Services/CommandHandler.cs | 1 + .../ServiceCollectionExtensions.cs | 14 +- 10 files changed, 283 insertions(+), 111 deletions(-) create mode 100644 src/NadekoBot/Common/Medusa/RemovablePlanner.cs delete mode 100644 src/NadekoBot/Directory.Build.props diff --git a/src/NadekoBot/Bot.cs b/src/NadekoBot/Bot.cs index 4e1d07a15..05807fffc 100644 --- a/src/NadekoBot/Bot.cs +++ b/src/NadekoBot/Bot.cs @@ -8,7 +8,9 @@ using NadekoBot.Modules.Utility; using NadekoBot.Services.Database.Models; using Ninject; using Ninject.Extensions.Conventions; +using Ninject.Extensions.Conventions.Syntax; using Ninject.Infrastructure.Language; +using Ninject.Planning; using System.Collections.Immutable; using System.Diagnostics; using System.Reflection; @@ -102,7 +104,14 @@ public sealed class Bot AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray(); } - var kernel = new StandardKernel(); + var kernel = new StandardKernel(new NinjectSettings() + { + ThrowOnGetServiceNotFound = true, + ActivationCacheDisabled = true, + }); + + kernel.Components.Remove(); + kernel.Components.Add(); kernel.Bind().ToMethod(_ => _credsProvider.GetCreds()).InTransientScope(); @@ -122,7 +131,6 @@ public sealed class Bot .AddCache(_creds) .AddHttpClients(); - if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1") { kernel.Bind().To().InSingletonScope(); @@ -134,35 +142,25 @@ public sealed class Bot kernel.Bind(scan => { - var classes = scan.FromThisAssembly() - .SelectAllClasses() - .Where(c => (c.IsAssignableTo(typeof(INService)) - || c.IsAssignableTo(typeof(IExecOnMessage)) - || c.IsAssignableTo(typeof(IInputTransformer)) - || c.IsAssignableTo(typeof(IExecPreCommand)) - || c.IsAssignableTo(typeof(IExecPostCommand)) - || c.IsAssignableTo(typeof(IExecNoCommand))) - && !c.HasAttribute() -#if GLOBAL_NADEKO - && !c.HasAttribute() + scan.FromThisAssembly() + .SelectAllClasses() + .Where(c => (c.IsAssignableTo(typeof(INService)) + || c.IsAssignableTo(typeof(IExecOnMessage)) + || c.IsAssignableTo(typeof(IInputTransformer)) + || c.IsAssignableTo(typeof(IExecPreCommand)) + || c.IsAssignableTo(typeof(IExecPostCommand)) + || c.IsAssignableTo(typeof(IExecNoCommand))) + && !c.HasAttribute() +#if GLOBAL_NADEK + && !c.HasAttribute() #endif - ); - classes - .BindAllInterfaces() + ) + .BindToSelfWithInterfaces() .Configure(c => c.InSingletonScope()); - - classes.BindToSelf() - .Configure(c => c.InSingletonScope()); }); kernel.Bind().ToConstant(kernel).InSingletonScope(); - var services = kernel.GetServices(typeof(INService)); - foreach (var s in services) - { - Console.WriteLine(s.GetType().FullName); - } - //initialize Services Services = kernel; Services.GetRequiredService().Initialize(); @@ -187,16 +185,7 @@ public sealed class Bot private IEnumerable LoadTypeReaders(Assembly assembly) { - Type[] allTypes; - try - { - allTypes = assembly.GetTypes(); - } - catch (ReflectionTypeLoadException ex) - { - Log.Warning(ex.LoaderExceptions[0], "Error getting types"); - return Enumerable.Empty(); - } + var allTypes = assembly.GetTypes(); var filteredTypes = allTypes.Where(x => x.IsSubclassOf(typeof(TypeReader)) && x.BaseType?.GetGenericArguments().Length > 0 @@ -205,10 +194,12 @@ public sealed class Bot var toReturn = new List(); foreach (var ft in filteredTypes) { - var x = (TypeReader)ActivatorUtilities.CreateInstance(Services, ft); var baseType = ft.BaseType; if (baseType is null) continue; + + var x = (TypeReader)ActivatorUtilities.CreateInstance(Services, ft); + var typeArgs = baseType.GetGenericArguments(); _commandService.AddTypeReader(typeArgs[0], x); toReturn.Add(x); diff --git a/src/NadekoBot/Common/Medusa/MedusaAssemblyLoadContext.cs b/src/NadekoBot/Common/Medusa/MedusaAssemblyLoadContext.cs index 51aaa4915..a6930f04e 100644 --- a/src/NadekoBot/Common/Medusa/MedusaAssemblyLoadContext.cs +++ b/src/NadekoBot/Common/Medusa/MedusaAssemblyLoadContext.cs @@ -3,34 +3,33 @@ using System.Runtime.Loader; namespace Nadeko.Medusa; -public sealed class MedusaAssemblyLoadContext : AssemblyLoadContext +public class MedusaAssemblyLoadContext : AssemblyLoadContext { - private readonly AssemblyDependencyResolver _depResolver; + private readonly AssemblyDependencyResolver _resolver; + + public MedusaAssemblyLoadContext(string folderPath) : base(isCollectible: true) + => _resolver = new(folderPath); - public MedusaAssemblyLoadContext(string pluginPath) : base(isCollectible: true) - { - _depResolver = new(pluginPath); - } + // public Assembly MainAssembly { get; private set; } protected override Assembly? Load(AssemblyName assemblyName) { - var assemblyPath = _depResolver.ResolveAssemblyToPath(assemblyName); + var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) { - return LoadFromAssemblyPath(assemblyPath); + Assembly assembly = LoadFromAssemblyPath(assemblyPath); + LoadDependencies(assembly); + return assembly; } return null; } - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + public void LoadDependencies(Assembly assembly) { - var libraryPath = _depResolver.ResolveUnmanagedDllToPath(unmanagedDllName); - if (libraryPath != null) + foreach (var reference in assembly.GetReferencedAssemblies()) { - return LoadUnmanagedDllFromPath(libraryPath); + Load(reference); } - - return IntPtr.Zero; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Medusa/MedusaIoCKernelModule.cs b/src/NadekoBot/Common/Medusa/MedusaIoCKernelModule.cs index c4d969824..d9de7e786 100644 --- a/src/NadekoBot/Common/Medusa/MedusaIoCKernelModule.cs +++ b/src/NadekoBot/Common/Medusa/MedusaIoCKernelModule.cs @@ -1,50 +1,95 @@ -using Ninject.Modules; -using Ninject.Extensions.Conventions; -using System.Reflection; +using System.Reflection; +using Ninject; +using Ninject.Activation; +using Ninject.Activation.Caching; +using Ninject.Modules; +using Ninject.Planning; +using System.Text.Json; -namespace Nadeko.Medusa; - -public sealed class MedusaIoCKernelModule : NinjectModule +public sealed class MedusaNinjectModule : NinjectModule { - private Assembly _a; public override string Name { get; } + private volatile bool _isLoaded = false; + private readonly Dictionary _types; - public MedusaIoCKernelModule(string name, Assembly a) + public MedusaNinjectModule(Assembly assembly, string name) { Name = name; - _a = a; + _types = assembly.GetExportedTypes() + .Where(t => t.IsClass) + .Where(t => t.GetCustomAttribute() is not null) + .ToDictionary(x => x, + type => type.GetInterfaces().ToArray()); } public override void Load() { - // todo cehck for duplicate registrations with ninject.extensions.convention - Kernel.Bind(conf => - { - var transient = conf.From(_a) - .SelectAllClasses() - .WithAttribute(x => x.Lifetime == Lifetime.Transient); - - transient.BindAllInterfaces().Configure(x => x.InTransientScope()); - transient.BindToSelf().Configure(x => x.InTransientScope()); + if (_isLoaded) + return; - var singleton = conf.From(_a) - .SelectAllClasses() - .WithAttribute(x => x.Lifetime == Lifetime.Singleton); + foreach (var (type, data) in _types) + { + var attribute = type.GetCustomAttribute()!; + var scope = GetScope(attribute.Lifetime); + + Bind(type) + .ToSelf() + .InScope(scope); - singleton.BindAllInterfaces().Configure(x => x.InSingletonScope()); - singleton.BindToSelf().Configure(x => x.InSingletonScope()); - }); + foreach (var inter in data) + { + Bind(inter) + .ToMethod(x => x.Kernel.Get(type)) + .InScope(scope); + } + } + + _isLoaded = true; } + private Func GetScope(Lifetime lt) + => _ => lt switch + { + Lifetime.Singleton => this, + Lifetime.Transient => null, + }; + public override void Unload() { - // todo implement unload - // Kernel.Unbind(); - } + if (!_isLoaded) + return; - public override void Dispose(bool disposing) - { - _a = null!; - base.Dispose(disposing); + var planner = (RemovablePlanner)Kernel.Components.Get(); + var cache = Kernel.Components.Get(); + foreach (var binding in this.Bindings) + { + Kernel.RemoveBinding(binding); + } + + foreach (var type in _types.SelectMany(x => x.Value).Concat(_types.Keys)) + { + var binds = Kernel.GetBindings(type); + + if (!binds.Any()) + { + Unbind(type); + + planner.RemovePlan(type); + } + } + + + Bindings.Clear(); + + cache.Clear(this); + _types.Clear(); + + // in case the library uses System.Text.Json + var assembly = typeof(JsonSerializerOptions).Assembly; + var updateHandlerType = assembly.GetType("System.Text.Json.JsonSerializerOptionsUpdateHandler"); + var clearCacheMethod = updateHandlerType?.GetMethod("ClearCache", BindingFlags.Static | BindingFlags.Public); + clearCacheMethod?.Invoke(null, new object?[] { null }); + + _isLoaded = false; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Medusa/MedusaLoaderService.cs b/src/NadekoBot/Common/Medusa/MedusaLoaderService.cs index 62c5f6811..092867ab9 100644 --- a/src/NadekoBot/Common/Medusa/MedusaLoaderService.cs +++ b/src/NadekoBot/Common/Medusa/MedusaLoaderService.cs @@ -237,8 +237,10 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, SnekInfos: snekData.ToImmutableArray(), strings, typeReaders, - execs, - kernelModule); + execs) + { + KernelModule = kernelModule + }; _medusaConfig.AddLoadedMedusa(safeName); @@ -319,18 +321,28 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, ctxWr = null; snekData = null; - var path = $"{BASE_DIR}/{safeName}/{safeName}.dll"; - strings = MedusaStrings.CreateDefault($"{BASE_DIR}/{safeName}"); + var path = Path.GetFullPath($"{BASE_DIR}/{safeName}/{safeName}.dll"); + var dir = Path.GetFullPath($"{BASE_DIR}/{safeName}"); + + if (!Directory.Exists(dir)) + throw new DirectoryNotFoundException($"Medusa folder not found: {dir}"); + + if (!File.Exists(path)) + throw new FileNotFoundException($"Medusa dll not found: {path}"); + + strings = MedusaStrings.CreateDefault(dir); var ctx = new MedusaAssemblyLoadContext(Path.GetDirectoryName(path)!); var a = ctx.LoadFromAssemblyPath(Path.GetFullPath(path)); + ctx.LoadDependencies(a); // load services - ninjectModule = new MedusaIoCKernelModule(safeName, a); + ninjectModule = new MedusaNinjectModule(a, safeName); _kernel.Load(ninjectModule); var sis = LoadSneksFromAssembly(safeName, a); typeReaders = LoadTypeReadersFromAssembly(a, strings); - + + // todo allow this if (sis.Count == 0) { _kernel.Unload(safeName); @@ -590,8 +602,14 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, await DisposeSnekInstances(lsi); var lc = lsi.LoadContext; - - // lsi.KernelModule = null!; + var km = lsi.KernelModule; + lsi.KernelModule = null!; + + _kernel.Unload(km.Name); + + if (km is IDisposable d) + d.Dispose(); + lsi = null; _medusaConfig.RemoveLoadedMedusa(name); @@ -650,7 +668,9 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, private void UnloadContext(WeakReference lsiLoadContext) { if (lsiLoadContext.TryGetTarget(out var ctx)) + { ctx.Unload(); + } } private void GcCleanup() diff --git a/src/NadekoBot/Common/Medusa/Models/ResolvedMedusa.cs b/src/NadekoBot/Common/Medusa/Models/ResolvedMedusa.cs index 70f2fb978..a8f98c19d 100644 --- a/src/NadekoBot/Common/Medusa/Models/ResolvedMedusa.cs +++ b/src/NadekoBot/Common/Medusa/Models/ResolvedMedusa.cs @@ -9,7 +9,8 @@ public sealed record ResolvedMedusa( IImmutableList SnekInfos, IMedusaStrings Strings, Dictionary TypeReaders, - IReadOnlyCollection Execs, - INinjectModule KernelModule) + IReadOnlyCollection Execs +) { + public INinjectModule KernelModule { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Medusa/RemovablePlanner.cs b/src/NadekoBot/Common/Medusa/RemovablePlanner.cs new file mode 100644 index 000000000..03158a679 --- /dev/null +++ b/src/NadekoBot/Common/Medusa/RemovablePlanner.cs @@ -0,0 +1,122 @@ +//------------------------------------------------------------------------------- +// +// Copyright (c) 2007-2009, Enkari, Ltd. +// Copyright (c) 2009-2011 Ninject Project Contributors +// Authors: Nate Kohari (nate@enkari.com) +// Remo Gloor (remo.gloor@gmail.com) +// +// Dual-licensed under the Apache License, Version 2.0, and the Microsoft Public License (Ms-PL). +// you may not use this file except in compliance with one of the Licenses. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// or +// http://www.microsoft.com/opensource/licenses.mspx +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//------------------------------------------------------------------------------- + +// ReSharper disable all +#pragma warning disable + +namespace Ninject.Planning; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Ninject.Components; +using Ninject.Infrastructure.Language; +using Ninject.Planning.Strategies; + +/// +/// Generates plans for how to activate instances. +/// +public class RemovablePlanner : NinjectComponent, IPlanner +{ + private readonly ReaderWriterLock plannerLock = new ReaderWriterLock(); + private readonly Dictionary plans = new Dictionary(); + + /// + /// Initializes a new instance of the class. + /// + /// The strategies to execute during planning. + public RemovablePlanner(IEnumerable strategies) + { + this.Strategies = strategies.ToList(); + } + + /// + /// Gets the strategies that contribute to the planning process. + /// + public IList Strategies { get; private set; } + + /// + /// Gets or creates an activation plan for the specified type. + /// + /// The type for which a plan should be created. + /// The type's activation plan. + public IPlan GetPlan(Type type) + { + this.plannerLock.AcquireReaderLock(Timeout.Infinite); + try + { + IPlan plan; + return this.plans.TryGetValue(type, out plan) ? plan : this.CreateNewPlan(type); + } + finally + { + this.plannerLock.ReleaseReaderLock(); + } + } + + /// + /// Creates an empty plan for the specified type. + /// + /// The type for which a plan should be created. + /// The created plan. + protected virtual IPlan CreateEmptyPlan(Type type) + { + return new Plan(type); + } + + /// + /// Creates a new plan for the specified type. + /// This method requires an active reader lock! + /// + /// The type. + /// The newly created plan. + private IPlan CreateNewPlan(Type type) + { + var lockCooki = this.plannerLock.UpgradeToWriterLock(Timeout.Infinite); + try + { + IPlan plan; + if (this.plans.TryGetValue(type, out plan)) + { + return plan; + } + + plan = this.CreateEmptyPlan(type); + this.plans.Add(type, plan); + this.Strategies.Map(s => s.Execute(plan)); + + return plan; + } + finally + { + this.plannerLock.DowngradeFromWriterLock(ref lockCooki); + } + } + + public void RemovePlan(Type type) + { + plans.Remove(type); + plans.TrimExcess(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/NadekoContext.cs b/src/NadekoBot/Db/NadekoContext.cs index f78d30ab1..221d2bcfb 100644 --- a/src/NadekoBot/Db/NadekoContext.cs +++ b/src/NadekoBot/Db/NadekoContext.cs @@ -482,10 +482,10 @@ public abstract class NadekoContext : DbContext #endregion } -#if DEBUG - private static readonly ILoggerFactory _debugLoggerFactory = LoggerFactory.Create(x => x.AddConsole()); - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseLoggerFactory(_debugLoggerFactory); -#endif +// #if DEBUG +// private static readonly ILoggerFactory _debugLoggerFactory = LoggerFactory.Create(x => x.AddConsole()); +// +// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +// => optionsBuilder.UseLoggerFactory(_debugLoggerFactory); +// #endif } \ No newline at end of file diff --git a/src/NadekoBot/Directory.Build.props b/src/NadekoBot/Directory.Build.props deleted file mode 100644 index d157d2874..000000000 --- a/src/NadekoBot/Directory.Build.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - all - - - \ No newline at end of file diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index f7ac2a3d0..efaf53f34 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -55,6 +55,7 @@ public class CommandHandler : INService, IReadyExecutor _prefixes = bot.AllGuildConfigs.Where(x => x.Prefix is not null) .ToDictionary(x => x.GuildId, x => x.Prefix) .ToConcurrent(); + } public async Task OnReadyAsync() diff --git a/src/NadekoBot/_Extensions/ServiceCollectionExtensions.cs b/src/NadekoBot/_Extensions/ServiceCollectionExtensions.cs index 034911e71..f11b71fec 100644 --- a/src/NadekoBot/_Extensions/ServiceCollectionExtensions.cs +++ b/src/NadekoBot/_Extensions/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using NadekoBot.Modules.Music.Resolvers; using NadekoBot.Modules.Music.Services; using Ninject; using Ninject.Extensions.Conventions; +using Ninject.Extensions.Conventions.Syntax; using StackExchange.Redis; using System.Net; using System.Reflection; @@ -39,10 +40,7 @@ public static class ServiceCollectionExtensions .SelectAllClasses() .Where(f => f.IsAssignableToGenericType(typeof(ConfigServiceBase<>))); - // todo check for duplicates - configs.BindToSelf() - .Configure(c => c.InSingletonScope()); - configs.BindAllInterfaces() + configs.BindToSelfWithInterfaces() .Configure(c => c.InSingletonScope()); }); @@ -64,7 +62,7 @@ public static class ServiceCollectionExtensions kernel.Bind().To().InSingletonScope(); kernel.Bind().To().InSingletonScope(); kernel.Bind().To().InSingletonScope(); - kernel.Bind().ToSelf().InSingletonScope(); + // kernel.Bind().ToSelf().InSingletonScope(); return kernel; } @@ -77,8 +75,7 @@ public static class ServiceCollectionExtensions .SelectAllClasses() .Where(c => c.IsPublic && c.IsNested && baseType.IsAssignableFrom(baseType)); - classes.BindAllInterfaces().Configure(x => x.InSingletonScope()); - classes.BindToSelf().Configure(x => x.InSingletonScope()); + classes.BindToSelfWithInterfaces().Configure(x => x.InSingletonScope()); }); return kernel; @@ -125,4 +122,7 @@ public static class ServiceCollectionExtensions return kernel; } + + public static IConfigureSyntax BindToSelfWithInterfaces(this IJoinExcludeIncludeBindSyntax matcher) + => matcher.BindSelection((type, types) => types.Append(type)); } \ No newline at end of file