From 97511d59884fed3049f7b6656df89dd939c7fbe9 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 1 May 2021 17:55:29 +0200 Subject: [PATCH] Suporting optional dependencies --- Kyoo.Authentication/Class1.cs | 6 + .../Kyoo.Authentication.csproj | 23 +++ Kyoo.Common/Controllers/IPlugin.cs | 168 ++++++++++++++++-- Kyoo.Postgresql/PostgresModule.cs | 11 +- Kyoo.sln | 6 + Kyoo/Controllers/PluginManager.cs | 46 ++++- Kyoo/CoreModule.cs | 67 +++---- Kyoo/Program.cs | 2 +- Kyoo/Startup.cs | 9 +- 9 files changed, 276 insertions(+), 62 deletions(-) create mode 100644 Kyoo.Authentication/Class1.cs create mode 100644 Kyoo.Authentication/Kyoo.Authentication.csproj diff --git a/Kyoo.Authentication/Class1.cs b/Kyoo.Authentication/Class1.cs new file mode 100644 index 00000000..18aefd85 --- /dev/null +++ b/Kyoo.Authentication/Class1.cs @@ -0,0 +1,6 @@ +using System; + +namespace Kyoo.Authentication +{ + public class Class1 { } +} \ No newline at end of file diff --git a/Kyoo.Authentication/Kyoo.Authentication.csproj b/Kyoo.Authentication/Kyoo.Authentication.csproj new file mode 100644 index 00000000..75bd3077 --- /dev/null +++ b/Kyoo.Authentication/Kyoo.Authentication.csproj @@ -0,0 +1,23 @@ + + + + net5.0 + ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql + false + false + false + false + + SDG + Zoe Roux + https://github.com/AnonymusRaccoon/Kyoo + default + + + + + all + false + + + diff --git a/Kyoo.Common/Controllers/IPlugin.cs b/Kyoo.Common/Controllers/IPlugin.cs index a2f9d1b0..9f0e68e0 100644 --- a/Kyoo.Common/Controllers/IPlugin.cs +++ b/Kyoo.Common/Controllers/IPlugin.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using Microsoft.AspNetCore.Builder; using Unity; @@ -29,34 +31,38 @@ namespace Kyoo.Controllers string Description { get; } /// - /// A list of services that are provided by this service. This allow other plugins to declare dependencies. + /// A list of services that are provided by this service. This allow other plugins to declare dependencies /// /// /// You should put the type's interface that will be register in configure. /// - Type[] Provides { get; } + ICollection Provides { get; } /// - /// A list of services that are required by this service. - /// The Core will warn the user that this plugin can't be loaded if a required service is not found. + /// A list of types that will be provided only if a condition is met. The condition can be an arbitrary method or + /// a condition based on other type availability. For more information, see . + /// + ICollection ConditionalProvides { get; } + + /// + /// A list of services that are required by this plugin. + /// You can put services that you provide conditionally here if you want. + /// Kyoo will warn the user that this plugin can't be loaded if a required service is not found. /// /// /// Put here the most complete type that are needed for your plugin to work. If you need a LibraryManager, /// put typeof(ILibraryManager). /// - Type[] Requires { get; } - - /// - /// True if this plugin is needed to start Kyoo. If this is true and a dependency could not be met, app startup - /// will be canceled. If this is false, Kyoo's startup will continue without enabling this plugin. - /// - bool IsRequired { get; } + ICollection Requires { get; } /// /// A configure method that will be run on plugin's startup. /// /// A unity container to register new services. - void Configure(IUnityContainer container); + /// The list of types that are available for this instance. This can be used + /// for conditional type. See + /// or > + void Configure(IUnityContainer container, ICollection availableTypes); /// /// An optional configuration step to allow a plugin to change asp net configurations. @@ -64,6 +70,144 @@ namespace Kyoo.Controllers /// /// The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities. void ConfigureAspNet(IApplicationBuilder app) {} + } + + /// + /// A type that will only be provided if a special condition is met. To check that your condition is met, + /// you can check the class. + /// + public class ConditionalProvide : Tuple + { + /// + /// Get the type that may be provided + /// + public Type Type => Item1; + + /// + /// Get the condition. + /// + public ProviderCondition Condition => Item2; + /// + /// Create a from a type and a condition. + /// + /// The type to provide + /// The condition + public ConditionalProvide(Type type, ProviderCondition condition) + : base(type, condition) + { } + + /// + /// Create a from a tuple of (Type, ProviderCondition). + /// + /// The tuple to convert + public ConditionalProvide((Type type, ProviderCondition condition) tuple) + : base(tuple.type, tuple.condition) + { } + + /// + /// Implicitly convert a tuple to a . + /// + /// The tuple to convert + /// A new based on the given tuple. + public static implicit operator ConditionalProvide((Type, Type) tuple) => new (tuple); + } + + /// + /// A condition for a conditional type. + /// + public class ProviderCondition + { + /// + /// The condition as a method. If true is returned, the type will be provided. + /// + public Func Condition { get; } = () => true; + /// + /// The list of types that this method needs. + /// + public ICollection Needed { get; } = ArraySegment.Empty; + + + /// + /// Create a new from a raw function. + /// + /// The predicate that will be used as condition + public ProviderCondition(Func condition) + { + Condition = condition; + } + + /// + /// Create a new from a type. This allow you to inform that a type will + /// only be available if a dependency is met. + /// + /// The type that you need + public ProviderCondition(Type needed) + { + Needed = new[] {needed}; + } + + /// + /// Create a new from a list of type. This allow you to inform that a type will + /// only be available if a list of dependencies are met. + /// + /// The types that you need + public ProviderCondition(ICollection needed) + { + Needed = needed; + } + + /// + /// Create a new with a list of types as dependencies and a predicate + /// for arbitrary conditions. + /// + /// The list of dependencies + /// An arbitrary condition + public ProviderCondition(ICollection needed, Func condition) + { + Needed = needed; + Condition = condition; + } + + + /// + /// Implicitly convert a type to a . + /// + /// The type dependency + /// A that will return true if the given type is available. + public static implicit operator ProviderCondition(Type type) => new(type); + + /// + /// Implicitly convert a list of type to a . + /// + /// The list of type dependencies + /// A that will return true if the given types are available. + public static implicit operator ProviderCondition(Type[] types) => new(types); + + /// + public static implicit operator ProviderCondition(List types) => new(types); + + + /// + /// Check if a type is available. + /// + /// The type to check + /// The list of types + /// True if the dependency is met, false otherwise + public static bool Has(Type needed, ICollection available) + { + return available.Contains(needed); + } + + /// + /// Check if a list of type are available. + /// + /// The list of types to check + /// The list of types + /// True if the dependencies are met, false otherwise + public static bool Has(ICollection needed, ICollection available) + { + return needed.All(x => Has(x, available)); + } } } \ No newline at end of file diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index e54f0a45..f3eecff0 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Kyoo.Controllers; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -22,16 +23,16 @@ namespace Kyoo.Postgresql public string Description => "A database context for postgresql."; /// - public Type[] Provides => new[] + public ICollection Provides => new[] { typeof(PostgresContext) }; /// - public Type[] Requires => Array.Empty(); - + public ICollection ConditionalProvides => ArraySegment.Empty; + /// - public bool IsRequired => true; + public ICollection Requires => ArraySegment.Empty; /// @@ -56,7 +57,7 @@ namespace Kyoo.Postgresql } /// - public void Configure(IUnityContainer container) + public void Configure(IUnityContainer container, ICollection availableTypes) { container.RegisterFactory(_ => new PostgresContext( _configuration.GetDatabaseConnection("postgres"), diff --git a/Kyoo.sln b/Kyoo.sln index 79ee6e7d..3f814bd3 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Tests", "Kyoo.Tests\Ky EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "Kyoo.Postgresql\Kyoo.Postgresql.csproj", "{3213C96D-0BF3-460B-A8B5-B9977229408A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,5 +37,9 @@ Global {3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.Build.0 = Debug|Any CPU {3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.ActiveCfg = Release|Any CPU {3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.Build.0 = Release|Any CPU + {7A841335-6523-47DB-9717-80AA7BD943FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A841335-6523-47DB-9717-80AA7BD943FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs index d2945b0c..d2df582e 100644 --- a/Kyoo/Controllers/PluginManager.cs +++ b/Kyoo/Controllers/PluginManager.cs @@ -101,7 +101,7 @@ namespace Kyoo.Controllers newPlugins.Add(new CoreModule()); _plugins.AddRange(newPlugins); - ICollection available = _plugins.SelectMany(x => x.Provides).ToArray(); + ICollection available = GetProvidedTypes(); foreach (IPlugin plugin in newPlugins) { Type missing = plugin.Requires.FirstOrDefault(x => available.All(y => !y.IsAssignableTo(x))); @@ -109,11 +109,9 @@ namespace Kyoo.Controllers { Exception error = new MissingDependencyException(plugin.Name, missing.Name); _logger.LogCritical(error, "A plugin's dependency could not be met"); - if (plugin.IsRequired) - Environment.Exit(1); } else - plugin.Configure(_container); + plugin.Configure(_container, available); } if (!_plugins.Any()) @@ -122,6 +120,46 @@ namespace Kyoo.Controllers _logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name)); } + /// + /// Get the list of types provided by the currently loaded plugins. + /// + /// The list of types available. + private ICollection GetProvidedTypes() + { + List available = _plugins.SelectMany(x => x.Provides).ToList(); + List conditionals =_plugins + .SelectMany(x => x.ConditionalProvides) + .Where(x => x.Condition.Condition()) + .ToList(); + + bool IsAvailable(ConditionalProvide conditional, bool log = false) + { + if (!conditional.Condition.Condition()) + return false; + + ICollection needed = conditional.Condition.Needed + .Where(y => !available.Contains(y)) + .ToList(); + needed = needed.Where(x => !conditionals + .Where(y => y.Type == x) + .Any(y => IsAvailable(y))) + .ToList(); + if (!needed.Any()) + return true; + _logger.LogWarning("The type {Type} is not available, {Dependencies} could not be met", + conditional.Type.Name, + needed.Select(x => x.Name)); + return false; + } + + foreach (ConditionalProvide conditional in conditionals) + { + if (IsAvailable(conditional, true)) + available.Add(conditional.Type); + } + return available; + } + /// /// A custom to load plugin's dependency if they are on the same folder. diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index a32689a5..9f9810cf 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Kyoo.Controllers; using Kyoo.Tasks; using Unity; @@ -21,38 +22,37 @@ namespace Kyoo public string Description => "The core module containing default implementations."; /// - public Type[] Provides => new[] + public ICollection Provides => new[] { typeof(IFileManager), typeof(ITranscoder), typeof(IThumbnailsManager), typeof(IProviderManager), typeof(ITaskManager), - typeof(ILibraryManager), - typeof(ILibraryRepository), - typeof(ILibraryItemRepository), - typeof(ICollectionRepository), - typeof(IShowRepository), - typeof(ISeasonRepository), - typeof(IEpisodeRepository), - typeof(ITrackRepository), - typeof(IPeopleRepository), - typeof(IStudioRepository), - typeof(IGenreRepository), - typeof(IProviderRepository) + typeof(ILibraryManager) }; /// - public Type[] Requires => new[] + public ICollection ConditionalProvides => new ConditionalProvide[] { - typeof(DatabaseContext) + (typeof(ILibraryRepository), typeof(DatabaseContext)), + (typeof(ILibraryItemRepository), typeof(DatabaseContext)), + (typeof(ICollectionRepository), typeof(DatabaseContext)), + (typeof(IShowRepository), typeof(DatabaseContext)), + (typeof(ISeasonRepository), typeof(DatabaseContext)), + (typeof(IEpisodeRepository), typeof(DatabaseContext)), + (typeof(ITrackRepository), typeof(DatabaseContext)), + (typeof(IPeopleRepository), typeof(DatabaseContext)), + (typeof(IStudioRepository), typeof(DatabaseContext)), + (typeof(IGenreRepository), typeof(DatabaseContext)), + (typeof(IProviderRepository), typeof(DatabaseContext)) }; /// - public bool IsRequired => true; - - /// - public void Configure(IUnityContainer container) + public ICollection Requires => ArraySegment.Empty; + + /// + public void Configure(IUnityContainer container, ICollection availableTypes) { container.RegisterType(new SingletonLifetimeManager()); container.RegisterType(new SingletonLifetimeManager()); @@ -61,19 +61,22 @@ namespace Kyoo container.RegisterType(new SingletonLifetimeManager()); container.RegisterType(new HierarchicalLifetimeManager()); - - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - container.RegisterRepository(); - + + if (ProviderCondition.Has(typeof(DatabaseContext), availableTypes)) + { + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + container.RegisterRepository(); + } + container.RegisterTask(); } } diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index 4d749a0f..beba3b0d 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -80,7 +80,7 @@ namespace Kyoo private static IWebHostBuilder CreateWebHostBuilder(string[] args) { UnityContainer container = new(); - // container.EnableDebugDiagnostic(); + container.EnableDebugDiagnostic(); return new WebHostBuilder() .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 1bdf23b2..d747c960 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -147,14 +147,7 @@ namespace Kyoo services.AddHostedService(x => x.GetService() as TaskManager); } - public void ConfigureContainer(UnityContainer container) - { - // TODO move this to the configure section and figure out a way to reload ControllerActivators with the updated unity container - - // TODO the reload should re inject components from the constructor. - // TODO fin a way to inject tasks without a IUnityContainer. - // container.RegisterFactory(c => c.Resolve(), new SingletonLifetimeManager()); - } + public void ConfigureContainer(UnityContainer container) { } public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env) {