Suporting optional dependencies

This commit is contained in:
Zoe Roux 2021-05-01 17:55:29 +02:00
parent 36220613ab
commit 97511d5988
9 changed files with 276 additions and 62 deletions

View File

@ -0,0 +1,6 @@
using System;
namespace Kyoo.Authentication
{
public class Class1 { }
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<OutputPath>../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/postgresql</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<GenerateDependencyFile>false</GenerateDependencyFile>
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
<Company>SDG</Company>
<Authors>Zoe Roux</Authors>
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
<LangVersion>default</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Kyoo.Common/Kyoo.Common.csproj">
<PrivateAssets>all</PrivateAssets>
<Private>false</Private>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Unity; using Unity;
@ -29,34 +31,38 @@ namespace Kyoo.Controllers
string Description { get; } string Description { get; }
/// <summary> /// <summary>
/// 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
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// You should put the type's interface that will be register in configure. /// You should put the type's interface that will be register in configure.
/// </remarks> /// </remarks>
Type[] Provides { get; } ICollection<Type> Provides { get; }
/// <summary> /// <summary>
/// A list of services that are required by this service. /// A list of types that will be provided only if a condition is met. The condition can be an arbitrary method or
/// The Core will warn the user that this plugin can't be loaded if a required service is not found. /// a condition based on other type availability. For more information, see <see cref="ConditionalProvides"/>.
/// </summary>
ICollection<ConditionalProvide> ConditionalProvides { get; }
/// <summary>
/// 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.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Put here the most complete type that are needed for your plugin to work. If you need a LibraryManager, /// Put here the most complete type that are needed for your plugin to work. If you need a LibraryManager,
/// put typeof(ILibraryManager). /// put typeof(ILibraryManager).
/// </remarks> /// </remarks>
Type[] Requires { get; } ICollection<Type> Requires { get; }
/// <summary>
/// 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.
/// </summary>
bool IsRequired { get; }
/// <summary> /// <summary>
/// A configure method that will be run on plugin's startup. /// A configure method that will be run on plugin's startup.
/// </summary> /// </summary>
/// <param name="container">A unity container to register new services.</param> /// <param name="container">A unity container to register new services.</param>
void Configure(IUnityContainer container); /// <param name="availableTypes">The list of types that are available for this instance. This can be used
/// for conditional type. See <see cref="ProviderCondition.Has(System.Type,System.Collections.Generic.ICollection{System.Type})"/>
/// or <see cref="ProviderCondition.Has(System.Collections.Generic.ICollection{System.Type},System.Collections.Generic.ICollection{System.Type})"/>></param>
void Configure(IUnityContainer container, ICollection<Type> availableTypes);
/// <summary> /// <summary>
/// An optional configuration step to allow a plugin to change asp net configurations. /// An optional configuration step to allow a plugin to change asp net configurations.
@ -64,6 +70,144 @@ namespace Kyoo.Controllers
/// </summary> /// </summary>
/// <param name="app">The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities.</param> /// <param name="app">The Asp.Net application builder. On most case it is not needed but you can use it to add asp net functionalities.</param>
void ConfigureAspNet(IApplicationBuilder app) {} void ConfigureAspNet(IApplicationBuilder app) {}
}
/// <summary>
/// A type that will only be provided if a special condition is met. To check that your condition is met,
/// you can check the <see cref="ProviderCondition"/> class.
/// </summary>
public class ConditionalProvide : Tuple<Type, ProviderCondition>
{
/// <summary>
/// Get the type that may be provided
/// </summary>
public Type Type => Item1;
/// <summary>
/// Get the condition.
/// </summary>
public ProviderCondition Condition => Item2;
/// <summary>
/// Create a <see cref="ConditionalProvide"/> from a type and a condition.
/// </summary>
/// <param name="type">The type to provide</param>
/// <param name="condition">The condition</param>
public ConditionalProvide(Type type, ProviderCondition condition)
: base(type, condition)
{ }
/// <summary>
/// Create a <see cref="ConditionalProvide"/> from a tuple of (Type, ProviderCondition).
/// </summary>
/// <param name="tuple">The tuple to convert</param>
public ConditionalProvide((Type type, ProviderCondition condition) tuple)
: base(tuple.type, tuple.condition)
{ }
/// <summary>
/// Implicitly convert a tuple to a <see cref="ConditionalProvide"/>.
/// </summary>
/// <param name="tuple">The tuple to convert</param>
/// <returns>A new <see cref="ConditionalProvide"/> based on the given tuple.</returns>
public static implicit operator ConditionalProvide((Type, Type) tuple) => new (tuple);
}
/// <summary>
/// A condition for a conditional type.
/// </summary>
public class ProviderCondition
{
/// <summary>
/// The condition as a method. If true is returned, the type will be provided.
/// </summary>
public Func<bool> Condition { get; } = () => true;
/// <summary>
/// The list of types that this method needs.
/// </summary>
public ICollection<Type> Needed { get; } = ArraySegment<Type>.Empty;
/// <summary>
/// Create a new <see cref="ProviderCondition"/> from a raw function.
/// </summary>
/// <param name="condition">The predicate that will be used as condition</param>
public ProviderCondition(Func<bool> condition)
{
Condition = condition;
}
/// <summary>
/// Create a new <see cref="ProviderCondition"/> from a type. This allow you to inform that a type will
/// only be available if a dependency is met.
/// </summary>
/// <param name="needed">The type that you need</param>
public ProviderCondition(Type needed)
{
Needed = new[] {needed};
}
/// <summary>
/// Create a new <see cref="ProviderCondition"/> from a list of type. This allow you to inform that a type will
/// only be available if a list of dependencies are met.
/// </summary>
/// <param name="needed">The types that you need</param>
public ProviderCondition(ICollection<Type> needed)
{
Needed = needed;
}
/// <summary>
/// Create a new <see cref="ProviderCondition"/> with a list of types as dependencies and a predicate
/// for arbitrary conditions.
/// </summary>
/// <param name="needed">The list of dependencies</param>
/// <param name="condition">An arbitrary condition</param>
public ProviderCondition(ICollection<Type> needed, Func<bool> condition)
{
Needed = needed;
Condition = condition;
}
/// <summary>
/// Implicitly convert a type to a <see cref="ProviderCondition"/>.
/// </summary>
/// <param name="type">The type dependency</param>
/// <returns>A <see cref="ProviderCondition"/> that will return true if the given type is available.</returns>
public static implicit operator ProviderCondition(Type type) => new(type);
/// <summary>
/// Implicitly convert a list of type to a <see cref="ProviderCondition"/>.
/// </summary>
/// <param name="types">The list of type dependencies</param>
/// <returns>A <see cref="ProviderCondition"/> that will return true if the given types are available.</returns>
public static implicit operator ProviderCondition(Type[] types) => new(types);
/// <inheritdoc cref="op_Implicit(System.Type[])"/>
public static implicit operator ProviderCondition(List<Type> types) => new(types);
/// <summary>
/// Check if a type is available.
/// </summary>
/// <param name="needed">The type to check</param>
/// <param name="available">The list of types</param>
/// <returns>True if the dependency is met, false otherwise</returns>
public static bool Has(Type needed, ICollection<Type> available)
{
return available.Contains(needed);
}
/// <summary>
/// Check if a list of type are available.
/// </summary>
/// <param name="needed">The list of types to check</param>
/// <param name="available">The list of types</param>
/// <returns>True if the dependencies are met, false otherwise</returns>
public static bool Has(ICollection<Type> needed, ICollection<Type> available)
{
return needed.All(x => Has(x, available));
}
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Kyoo.Controllers; using Kyoo.Controllers;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -22,16 +23,16 @@ namespace Kyoo.Postgresql
public string Description => "A database context for postgresql."; public string Description => "A database context for postgresql.";
/// <inheritdoc /> /// <inheritdoc />
public Type[] Provides => new[] public ICollection<Type> Provides => new[]
{ {
typeof(PostgresContext) typeof(PostgresContext)
}; };
/// <inheritdoc /> /// <inheritdoc />
public Type[] Requires => Array.Empty<Type>(); public ICollection<ConditionalProvide> ConditionalProvides => ArraySegment<ConditionalProvide>.Empty;
/// <inheritdoc /> /// <inheritdoc />
public bool IsRequired => true; public ICollection<Type> Requires => ArraySegment<Type>.Empty;
/// <summary> /// <summary>
@ -56,7 +57,7 @@ namespace Kyoo.Postgresql
} }
/// <inheritdoc /> /// <inheritdoc />
public void Configure(IUnityContainer container) public void Configure(IUnityContainer container, ICollection<Type> availableTypes)
{ {
container.RegisterFactory<DatabaseContext>(_ => new PostgresContext( container.RegisterFactory<DatabaseContext>(_ => new PostgresContext(
_configuration.GetDatabaseConnection("postgres"), _configuration.GetDatabaseConnection("postgres"),

View File

@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Tests", "Kyoo.Tests\Ky
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "Kyoo.Postgresql\Kyoo.Postgresql.csproj", "{3213C96D-0BF3-460B-A8B5-B9977229408A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "Kyoo.Postgresql\Kyoo.Postgresql.csproj", "{3213C96D-0BF3-460B-A8B5-B9977229408A}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

View File

@ -101,7 +101,7 @@ namespace Kyoo.Controllers
newPlugins.Add(new CoreModule()); newPlugins.Add(new CoreModule());
_plugins.AddRange(newPlugins); _plugins.AddRange(newPlugins);
ICollection<Type> available = _plugins.SelectMany(x => x.Provides).ToArray(); ICollection<Type> available = GetProvidedTypes();
foreach (IPlugin plugin in newPlugins) foreach (IPlugin plugin in newPlugins)
{ {
Type missing = plugin.Requires.FirstOrDefault(x => available.All(y => !y.IsAssignableTo(x))); 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); Exception error = new MissingDependencyException(plugin.Name, missing.Name);
_logger.LogCritical(error, "A plugin's dependency could not be met"); _logger.LogCritical(error, "A plugin's dependency could not be met");
if (plugin.IsRequired)
Environment.Exit(1);
} }
else else
plugin.Configure(_container); plugin.Configure(_container, available);
} }
if (!_plugins.Any()) if (!_plugins.Any())
@ -122,6 +120,46 @@ namespace Kyoo.Controllers
_logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name)); _logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name));
} }
/// <summary>
/// Get the list of types provided by the currently loaded plugins.
/// </summary>
/// <returns>The list of types available.</returns>
private ICollection<Type> GetProvidedTypes()
{
List<Type> available = _plugins.SelectMany(x => x.Provides).ToList();
List<ConditionalProvide> 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<Type> 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;
}
/// <summary> /// <summary>
/// A custom <see cref="AssemblyLoadContext"/> to load plugin's dependency if they are on the same folder. /// A custom <see cref="AssemblyLoadContext"/> to load plugin's dependency if they are on the same folder.

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Tasks; using Kyoo.Tasks;
using Unity; using Unity;
@ -21,38 +22,37 @@ namespace Kyoo
public string Description => "The core module containing default implementations."; public string Description => "The core module containing default implementations.";
/// <inheritdoc /> /// <inheritdoc />
public Type[] Provides => new[] public ICollection<Type> Provides => new[]
{ {
typeof(IFileManager), typeof(IFileManager),
typeof(ITranscoder), typeof(ITranscoder),
typeof(IThumbnailsManager), typeof(IThumbnailsManager),
typeof(IProviderManager), typeof(IProviderManager),
typeof(ITaskManager), typeof(ITaskManager),
typeof(ILibraryManager), typeof(ILibraryManager)
typeof(ILibraryRepository),
typeof(ILibraryItemRepository),
typeof(ICollectionRepository),
typeof(IShowRepository),
typeof(ISeasonRepository),
typeof(IEpisodeRepository),
typeof(ITrackRepository),
typeof(IPeopleRepository),
typeof(IStudioRepository),
typeof(IGenreRepository),
typeof(IProviderRepository)
}; };
/// <inheritdoc /> /// <inheritdoc />
public Type[] Requires => new[] public ICollection<ConditionalProvide> 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))
}; };
/// <inheritdoc /> /// <inheritdoc />
public bool IsRequired => true; public ICollection<Type> Requires => ArraySegment<Type>.Empty;
/// <inheritdoc /> /// <inheritdoc />
public void Configure(IUnityContainer container) public void Configure(IUnityContainer container, ICollection<Type> availableTypes)
{ {
container.RegisterType<IFileManager, FileManager>(new SingletonLifetimeManager()); container.RegisterType<IFileManager, FileManager>(new SingletonLifetimeManager());
container.RegisterType<ITranscoder, Transcoder>(new SingletonLifetimeManager()); container.RegisterType<ITranscoder, Transcoder>(new SingletonLifetimeManager());
@ -62,6 +62,8 @@ namespace Kyoo
container.RegisterType<ILibraryManager, LibraryManager>(new HierarchicalLifetimeManager()); container.RegisterType<ILibraryManager, LibraryManager>(new HierarchicalLifetimeManager());
if (ProviderCondition.Has(typeof(DatabaseContext), availableTypes))
{
container.RegisterRepository<ILibraryRepository, LibraryRepository>(); container.RegisterRepository<ILibraryRepository, LibraryRepository>();
container.RegisterRepository<ILibraryItemRepository, LibraryItemRepository>(); container.RegisterRepository<ILibraryItemRepository, LibraryItemRepository>();
container.RegisterRepository<ICollectionRepository, CollectionRepository>(); container.RegisterRepository<ICollectionRepository, CollectionRepository>();
@ -73,6 +75,7 @@ namespace Kyoo
container.RegisterRepository<IStudioRepository, StudioRepository>(); container.RegisterRepository<IStudioRepository, StudioRepository>();
container.RegisterRepository<IGenreRepository, GenreRepository>(); container.RegisterRepository<IGenreRepository, GenreRepository>();
container.RegisterRepository<IProviderRepository, ProviderRepository>(); container.RegisterRepository<IProviderRepository, ProviderRepository>();
}
container.RegisterTask<Crawler>(); container.RegisterTask<Crawler>();
} }

View File

@ -80,7 +80,7 @@ namespace Kyoo
private static IWebHostBuilder CreateWebHostBuilder(string[] args) private static IWebHostBuilder CreateWebHostBuilder(string[] args)
{ {
UnityContainer container = new(); UnityContainer container = new();
// container.EnableDebugDiagnostic(); container.EnableDebugDiagnostic();
return new WebHostBuilder() return new WebHostBuilder()
.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)

View File

@ -147,14 +147,7 @@ namespace Kyoo
services.AddHostedService(x => x.GetService<ITaskManager>() as TaskManager); services.AddHostedService(x => x.GetService<ITaskManager>() as TaskManager);
} }
public void ConfigureContainer(UnityContainer container) 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<IHostedService>(c => c.Resolve<ITaskManager>(), new SingletonLifetimeManager());
}
public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env) public void Configure(IUnityContainer container, IApplicationBuilder app, IWebHostEnvironment env)
{ {