2020-12-06 18:48:54 -05:00
using System ;
using System.Collections.Generic ;
2021-04-06 15:59:47 -04:00
using System.Globalization ;
2020-12-06 18:48:54 -05:00
using System.IO ;
using System.Linq ;
2021-02-23 09:11:17 -05:00
using System.Net.Http ;
2020-12-06 18:48:54 -05:00
using System.Reflection ;
2023-01-15 15:39:57 -05:00
using System.Runtime.Loader ;
2020-12-06 18:48:54 -05:00
using System.Text ;
using System.Text.Json ;
2021-02-23 09:11:17 -05:00
using System.Threading.Tasks ;
2023-03-30 10:59:21 -04:00
using Emby.Server.Implementations.Library ;
2021-06-19 12:02:33 -04:00
using Jellyfin.Extensions.Json ;
using Jellyfin.Extensions.Json.Converters ;
2021-09-25 14:32:53 -04:00
using MediaBrowser.Common.Extensions ;
2021-02-23 09:11:17 -05:00
using MediaBrowser.Common.Net ;
2020-12-06 18:48:54 -05:00
using MediaBrowser.Common.Plugins ;
2023-11-09 15:09:51 -05:00
using MediaBrowser.Controller ;
using MediaBrowser.Controller.Plugins ;
2020-12-06 18:48:54 -05:00
using MediaBrowser.Model.Configuration ;
2021-06-12 16:20:35 -04:00
using MediaBrowser.Model.IO ;
2020-12-06 18:48:54 -05:00
using MediaBrowser.Model.Plugins ;
2021-02-12 08:33:10 -05:00
using MediaBrowser.Model.Updates ;
2020-12-06 18:48:54 -05:00
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Logging ;
2020-12-18 03:25:04 -05:00
namespace Emby.Server.Implementations.Plugins
2020-12-06 18:48:54 -05:00
{
/// <summary>
/// Defines the <see cref="PluginManager" />.
/// </summary>
2023-09-22 21:10:49 -04:00
public sealed class PluginManager : IPluginManager , IDisposable
2020-12-06 18:48:54 -05:00
{
2023-04-09 12:53:09 -04:00
private const string MetafileName = "meta.json" ;
2020-12-06 18:48:54 -05:00
private readonly string _pluginsPath ;
private readonly Version _appVersion ;
2023-01-15 17:00:38 -05:00
private readonly List < AssemblyLoadContext > _assemblyLoadContexts ;
2020-12-06 18:48:54 -05:00
private readonly JsonSerializerOptions _jsonOptions ;
private readonly ILogger < PluginManager > _logger ;
2023-11-09 15:09:51 -05:00
private readonly IServerApplicationHost _appHost ;
2020-12-06 18:48:54 -05:00
private readonly ServerConfiguration _config ;
2021-03-05 05:15:14 -05:00
private readonly List < LocalPlugin > _plugins ;
2020-12-06 18:48:54 -05:00
private readonly Version _minimumVersion ;
2021-02-23 09:11:17 -05:00
private IHttpClientFactory ? _httpClientFactory ;
2020-12-06 18:48:54 -05:00
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
2023-03-30 10:59:21 -04:00
/// <param name="logger">The <see cref="ILogger{PluginManager}"/>.</param>
2023-11-09 15:09:51 -05:00
/// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
2020-12-06 18:48:54 -05:00
/// <param name="config">The <see cref="ServerConfiguration"/>.</param>
/// <param name="pluginsPath">The plugin path.</param>
/// <param name="appVersion">The application version.</param>
public PluginManager (
2020-12-18 03:25:39 -05:00
ILogger < PluginManager > logger ,
2023-11-09 15:09:51 -05:00
IServerApplicationHost appHost ,
2020-12-06 18:48:54 -05:00
ServerConfiguration config ,
string pluginsPath ,
Version appVersion )
{
2020-12-18 04:44:57 -05:00
_logger = logger ? ? throw new ArgumentNullException ( nameof ( logger ) ) ;
2020-12-06 18:48:54 -05:00
_pluginsPath = pluginsPath ;
_appVersion = appVersion ? ? throw new ArgumentNullException ( nameof ( appVersion ) ) ;
2021-03-08 23:57:38 -05:00
_jsonOptions = new JsonSerializerOptions ( JsonDefaults . Options )
2020-12-23 12:43:29 -05:00
{
WriteIndented = true
} ;
2020-12-22 09:07:01 -05:00
// We need to use the default GUID converter, so we need to remove any custom ones.
for ( int a = _jsonOptions . Converters . Count - 1 ; a > = 0 ; a - - )
{
if ( _jsonOptions . Converters [ a ] is JsonGuidConverter convertor )
{
_jsonOptions . Converters . Remove ( convertor ) ;
break ;
}
}
2020-12-06 18:48:54 -05:00
_config = config ;
_appHost = appHost ;
_minimumVersion = new Version ( 0 , 0 , 0 , 1 ) ;
_plugins = Directory . Exists ( _pluginsPath ) ? DiscoverPlugins ( ) . ToList ( ) : new List < LocalPlugin > ( ) ;
2023-01-15 15:39:57 -05:00
2023-01-15 17:00:38 -05:00
_assemblyLoadContexts = new List < AssemblyLoadContext > ( ) ;
2020-12-06 18:48:54 -05:00
}
2021-09-25 14:32:53 -04:00
private IHttpClientFactory HttpClientFactory
{
get
{
return _httpClientFactory ? ? = _appHost . Resolve < IHttpClientFactory > ( ) ;
}
}
2020-12-06 18:48:54 -05:00
/// <summary>
/// Gets the Plugins.
/// </summary>
2021-03-05 05:15:14 -05:00
public IReadOnlyList < LocalPlugin > Plugins = > _plugins ;
2020-12-06 18:48:54 -05:00
/// <summary>
/// Returns all the assemblies.
/// </summary>
/// <returns>An IEnumerable{Assembly}.</returns>
public IEnumerable < Assembly > LoadAssemblies ( )
{
2020-12-14 18:08:04 -05:00
// Attempt to remove any deleted plugins and change any successors to be active.
2020-12-23 05:31:11 -05:00
for ( int i = _plugins . Count - 1 ; i > = 0 ; i - - )
2020-12-14 18:08:04 -05:00
{
2020-12-23 05:31:11 -05:00
var plugin = _plugins [ i ] ;
2020-12-17 08:44:38 -05:00
if ( plugin . Manifest . Status = = PluginStatus . Deleted & & DeletePlugin ( plugin ) )
2020-12-14 18:08:04 -05:00
{
2020-12-23 11:28:50 -05:00
// See if there is another version, and if so make that active.
ProcessAlternative ( plugin ) ;
2020-12-14 18:08:04 -05:00
}
}
// Now load the assemblies..
2020-12-06 18:48:54 -05:00
foreach ( var plugin in _plugins )
{
2020-12-18 17:17:46 -05:00
UpdatePluginSuperceedStatus ( plugin ) ;
2020-12-14 18:08:04 -05:00
if ( plugin . IsEnabledAndSupported = = false )
{
_logger . LogInformation ( "Skipping disabled plugin {Version} of {Name} " , plugin . Version , plugin . Name ) ;
continue ;
}
2023-01-17 18:49:00 -05:00
var assemblyLoadContext = new PluginLoadContext ( plugin . Path ) ;
_assemblyLoadContexts . Add ( assemblyLoadContext ) ;
2023-01-18 10:26:39 -05:00
var assemblies = new List < Assembly > ( plugin . DllFiles . Count ) ;
var loadedAll = true ;
2020-12-06 18:48:54 -05:00
foreach ( var file in plugin . DllFiles )
{
try
{
2023-01-18 10:26:39 -05:00
assemblies . Add ( assemblyLoadContext . LoadFromAssemblyPath ( file ) ) ;
2020-12-06 18:48:54 -05:00
}
catch ( FileLoadException ex )
{
2023-01-18 10:26:39 -05:00
_logger . LogError ( ex , "Failed to load assembly {Path}. Disabling plugin" , file ) ;
2020-12-17 08:44:38 -05:00
ChangePluginState ( plugin , PluginStatus . Malfunctioned ) ;
2023-01-18 10:26:39 -05:00
loadedAll = false ;
break ;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch ( Exception ex )
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger . LogError ( ex , "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin" , file ) ;
ChangePluginState ( plugin , PluginStatus . Malfunctioned ) ;
loadedAll = false ;
break ;
}
}
if ( ! loadedAll )
{
continue ;
}
foreach ( var assembly in assemblies )
{
try
{
// Load all required types to verify that the plugin will load
assembly . GetTypes ( ) ;
2020-12-06 18:48:54 -05:00
}
2021-11-06 17:44:05 -04:00
catch ( SystemException ex ) when ( ex is TypeLoadException or ReflectionTypeLoadException ) // Undocumented exception
2021-01-24 07:34:22 -05:00
{
2023-01-18 10:26:39 -05:00
_logger . LogError ( ex , "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin" , assembly . Location ) ;
2021-01-24 07:34:22 -05:00
ChangePluginState ( plugin , PluginStatus . NotSupported ) ;
2023-01-18 10:26:39 -05:00
break ;
2021-01-24 07:34:22 -05:00
}
2021-01-25 03:44:06 -05:00
#pragma warning disable CA1031 // Do not catch general exception types
catch ( Exception ex )
#pragma warning restore CA1031 // Do not catch general exception types
{
2023-01-18 10:26:39 -05:00
_logger . LogError ( ex , "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin" , assembly . Location ) ;
2021-01-25 03:44:06 -05:00
ChangePluginState ( plugin , PluginStatus . Malfunctioned ) ;
2023-01-18 10:26:39 -05:00
break ;
2021-01-25 03:44:06 -05:00
}
2020-12-06 18:48:54 -05:00
2023-01-18 10:26:39 -05:00
_logger . LogInformation ( "Loaded assembly {Assembly} from {Path}" , assembly . FullName , assembly . Location ) ;
2020-12-15 11:37:11 -05:00
yield return assembly ;
2020-12-06 18:48:54 -05:00
}
}
}
/// <summary>
/// Creates all the plugin instances.
/// </summary>
public void CreatePlugins ( )
{
2021-05-02 14:25:04 -04:00
_ = _appHost . GetExports < IPlugin > ( CreatePluginInstance ) ;
2020-12-06 18:48:54 -05:00
}
/// <summary>
/// Registers the plugin's services with the DI.
/// Note: DI is not yet instantiated yet.
/// </summary>
/// <param name="serviceCollection">A <see cref="ServiceCollection"/> instance.</param>
public void RegisterServices ( IServiceCollection serviceCollection )
{
foreach ( var pluginServiceRegistrator in _appHost . GetExportTypes < IPluginServiceRegistrator > ( ) )
{
2020-12-22 10:01:26 -05:00
var plugin = GetPluginByAssembly ( pluginServiceRegistrator . Assembly ) ;
2022-12-05 09:00:20 -05:00
if ( plugin is null )
2020-12-06 18:48:54 -05:00
{
2020-12-15 05:05:04 -05:00
_logger . LogError ( "Unable to find plugin in assembly {Assembly}" , pluginServiceRegistrator . Assembly . FullName ) ;
continue ;
2020-12-06 18:48:54 -05:00
}
2020-12-18 17:17:46 -05:00
UpdatePluginSuperceedStatus ( plugin ) ;
2020-12-06 18:48:54 -05:00
if ( ! plugin . IsEnabledAndSupported )
{
continue ;
}
try
{
var instance = ( IPluginServiceRegistrator ? ) Activator . CreateInstance ( pluginServiceRegistrator ) ;
2023-11-09 15:09:51 -05:00
instance ? . RegisterServices ( serviceCollection , _appHost ) ;
2020-12-06 18:48:54 -05:00
}
#pragma warning disable CA1031 // Do not catch general exception types
catch ( Exception ex )
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger . LogError ( ex , "Error registering plugin services from {Assembly}." , pluginServiceRegistrator . Assembly . FullName ) ;
2020-12-17 08:44:38 -05:00
if ( ChangePluginState ( plugin , PluginStatus . Malfunctioned ) )
2020-12-06 18:48:54 -05:00
{
_logger . LogInformation ( "Disabling plugin {Path}" , plugin . Path ) ;
}
}
}
}
/// <summary>
/// Imports a plugin manifest from <paramref name="folder"/>.
/// </summary>
/// <param name="folder">Folder of the plugin.</param>
public void ImportPluginFrom ( string folder )
{
2022-10-13 13:08:00 -04:00
ArgumentException . ThrowIfNullOrEmpty ( folder ) ;
2020-12-06 18:48:54 -05:00
// Load the plugin.
var plugin = LoadManifest ( folder ) ;
// Make sure we haven't already loaded this.
2020-12-18 17:17:46 -05:00
if ( _plugins . Any ( p = > p . Manifest . Equals ( plugin . Manifest ) ) )
2020-12-06 18:48:54 -05:00
{
return ;
}
_plugins . Add ( plugin ) ;
EnablePlugin ( plugin ) ;
}
/// <summary>
/// Removes the plugin reference '<paramref name="plugin"/>.
/// </summary>
/// <param name="plugin">The plugin.</param>
/// <returns>Outcome of the operation.</returns>
public bool RemovePlugin ( LocalPlugin plugin )
{
2022-10-06 14:21:23 -04:00
ArgumentNullException . ThrowIfNull ( plugin ) ;
2020-12-06 18:48:54 -05:00
if ( DeletePlugin ( plugin ) )
{
2020-12-23 11:28:50 -05:00
ProcessAlternative ( plugin ) ;
2020-12-06 18:48:54 -05:00
return true ;
}
2020-12-15 11:37:11 -05:00
_logger . LogWarning ( "Unable to delete {Path}, so marking as deleteOnStartup." , plugin . Path ) ;
2020-12-06 18:48:54 -05:00
// Unable to delete, so disable.
2020-12-23 11:28:50 -05:00
if ( ChangePluginState ( plugin , PluginStatus . Deleted ) )
{
ProcessAlternative ( plugin ) ;
return true ;
}
return false ;
2020-12-06 18:48:54 -05:00
}
/// <summary>
/// Attempts to find the plugin with and id of <paramref name="id"/>.
/// </summary>
2020-12-15 15:27:42 -05:00
/// <param name="id">The <see cref="Guid"/> of plugin.</param>
/// <param name="version">Optional <see cref="Version"/> of the plugin to locate.</param>
/// <returns>A <see cref="LocalPlugin"/> if located, or null if not.</returns>
public LocalPlugin ? GetPlugin ( Guid id , Version ? version = null )
2020-12-06 18:48:54 -05:00
{
2020-12-15 15:27:42 -05:00
LocalPlugin ? plugin ;
2022-12-05 09:00:20 -05:00
if ( version is null )
2020-12-06 18:48:54 -05:00
{
2020-12-14 18:08:04 -05:00
// If no version is given, return the current instance.
2020-12-23 05:29:21 -05:00
var plugins = _plugins . Where ( p = > p . Id . Equals ( id ) ) . ToList ( ) ;
2020-12-14 18:08:04 -05:00
2023-04-01 17:00:51 -04:00
plugin = plugins . FirstOrDefault ( p = > p . Instance is not null ) ? ? plugins . MaxBy ( p = > p . Version ) ;
2020-12-06 18:48:54 -05:00
}
else
{
2020-12-14 18:08:04 -05:00
// Match id and version number.
2020-12-06 18:48:54 -05:00
plugin = _plugins . FirstOrDefault ( p = > p . Id . Equals ( id ) & & p . Version . Equals ( version ) ) ;
}
2020-12-15 15:27:42 -05:00
return plugin ;
2020-12-06 18:48:54 -05:00
}
/// <summary>
/// Enables the plugin, disabling all other versions.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
public void EnablePlugin ( LocalPlugin plugin )
{
2022-10-06 14:21:23 -04:00
ArgumentNullException . ThrowIfNull ( plugin ) ;
2020-12-06 18:48:54 -05:00
if ( ChangePluginState ( plugin , PluginStatus . Active ) )
{
2020-12-23 11:28:50 -05:00
// See if there is another version, and if so, supercede it.
ProcessAlternative ( plugin ) ;
2020-12-06 18:48:54 -05:00
}
}
/// <summary>
/// Disable the plugin.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
public void DisablePlugin ( LocalPlugin plugin )
{
2022-10-06 14:21:23 -04:00
ArgumentNullException . ThrowIfNull ( plugin ) ;
2020-12-06 18:48:54 -05:00
// Update the manifest on disk
if ( ChangePluginState ( plugin , PluginStatus . Disabled ) )
{
2020-12-23 11:28:50 -05:00
// If there is another version, activate it.
ProcessAlternative ( plugin ) ;
2020-12-06 18:48:54 -05:00
}
}
/// <summary>
/// Disable the plugin.
/// </summary>
/// <param name="assembly">The <see cref="Assembly"/> of the plug to disable.</param>
public void FailPlugin ( Assembly assembly )
{
// Only save if disabled.
2022-10-06 14:21:23 -04:00
ArgumentNullException . ThrowIfNull ( assembly ) ;
2020-12-06 18:48:54 -05:00
2020-12-18 16:56:54 -05:00
var plugin = _plugins . FirstOrDefault ( p = > p . DllFiles . Contains ( assembly . Location ) ) ;
2022-12-05 09:00:20 -05:00
if ( plugin is null )
2020-12-06 18:48:54 -05:00
{
// A plugin's assembly didn't cause this issue, so ignore it.
return ;
}
2020-12-17 08:44:38 -05:00
ChangePluginState ( plugin , PluginStatus . Malfunctioned ) ;
2020-12-06 18:48:54 -05:00
}
2021-02-12 08:33:10 -05:00
/// <inheritdoc/>
2020-12-06 18:48:54 -05:00
public bool SaveManifest ( PluginManifest manifest , string path )
{
try
{
var data = JsonSerializer . Serialize ( manifest , _jsonOptions ) ;
2023-04-09 12:53:09 -04:00
File . WriteAllText ( Path . Combine ( path , MetafileName ) , data ) ;
2020-12-06 18:48:54 -05:00
return true ;
}
2021-02-12 08:33:10 -05:00
catch ( ArgumentException e )
2020-12-06 18:48:54 -05:00
{
2021-02-12 08:33:10 -05:00
_logger . LogWarning ( e , "Unable to save plugin manifest due to invalid value. {Path}" , path ) ;
2020-12-06 18:48:54 -05:00
return false ;
}
}
2021-02-12 08:33:10 -05:00
/// <inheritdoc/>
2023-04-09 12:53:09 -04:00
public async Task < bool > PopulateManifest ( PackageInfo packageInfo , Version version , string path , PluginStatus status )
2021-02-12 08:33:10 -05:00
{
var versionInfo = packageInfo . Versions . First ( v = > v . Version = = version . ToString ( ) ) ;
2021-02-23 09:11:17 -05:00
var imagePath = string . Empty ;
2021-02-12 08:33:10 -05:00
2021-02-23 09:11:17 -05:00
if ( ! string . IsNullOrEmpty ( packageInfo . ImageUrl ) )
2021-02-12 08:33:10 -05:00
{
2021-02-23 09:11:17 -05:00
var url = new Uri ( packageInfo . ImageUrl ) ;
2021-02-23 09:36:49 -05:00
imagePath = Path . Join ( path , url . Segments [ ^ 1 ] ) ;
2021-02-23 09:11:17 -05:00
2023-10-11 12:32:57 -04:00
var fileStream = AsyncFile . OpenWrite ( imagePath ) ;
Stream ? downloadStream = null ;
2021-02-23 10:03:26 -05:00
try
{
2023-10-11 12:32:57 -04:00
downloadStream = await HttpClientFactory
2021-02-23 10:03:26 -05:00
. CreateClient ( NamedClient . Default )
. GetStreamAsync ( url )
. ConfigureAwait ( false ) ;
await downloadStream . CopyToAsync ( fileStream ) . ConfigureAwait ( false ) ;
}
catch ( HttpRequestException ex )
{
_logger . LogError ( ex , "Failed to download image to path {Path} on disk." , imagePath ) ;
imagePath = string . Empty ;
}
2023-10-11 12:32:57 -04:00
finally
{
await fileStream . DisposeAsync ( ) . ConfigureAwait ( false ) ;
if ( downloadStream is not null )
{
await downloadStream . DisposeAsync ( ) . ConfigureAwait ( false ) ;
}
}
2021-02-12 08:33:10 -05:00
}
var manifest = new PluginManifest
{
Category = packageInfo . Category ,
Changelog = versionInfo . Changelog ? ? string . Empty ,
Description = packageInfo . Description ,
2021-06-06 12:11:51 -04:00
Id = packageInfo . Id ,
2021-02-12 08:33:10 -05:00
Name = packageInfo . Name ,
Overview = packageInfo . Overview ,
Owner = packageInfo . Owner ,
TargetAbi = versionInfo . TargetAbi ? ? string . Empty ,
2021-04-06 15:59:47 -04:00
Timestamp = string . IsNullOrEmpty ( versionInfo . Timestamp ) ? DateTime . MinValue : DateTime . Parse ( versionInfo . Timestamp , CultureInfo . InvariantCulture ) ,
2021-02-12 08:33:10 -05:00
Version = versionInfo . Version ,
2021-04-06 15:59:47 -04:00
Status = status = = PluginStatus . Disabled ? PluginStatus . Disabled : PluginStatus . Active , // Keep disabled state.
2021-02-12 08:33:10 -05:00
AutoUpdate = true ,
2023-04-09 12:53:09 -04:00
ImagePath = imagePath
2021-02-12 08:33:10 -05:00
} ;
2023-10-11 12:32:57 -04:00
if ( ! await ReconcileManifest ( manifest , path ) . ConfigureAwait ( false ) )
2023-04-09 12:53:09 -04:00
{
2023-04-16 09:46:12 -04:00
// An error occurred during reconciliation and saving could be undesirable.
return false ;
2023-04-09 12:53:09 -04:00
}
2021-02-12 08:33:10 -05:00
return SaveManifest ( manifest , path ) ;
}
2023-09-22 21:10:49 -04:00
/// <inheritdoc />
public void Dispose ( )
{
foreach ( var assemblyLoadContext in _assemblyLoadContexts )
{
assemblyLoadContext . Unload ( ) ;
}
}
2023-04-09 12:53:09 -04:00
/// <summary>
2023-04-16 09:46:12 -04:00
/// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
/// If no file is found, no reconciliation occurs.
2023-04-09 12:53:09 -04:00
/// </summary>
2023-04-16 09:46:12 -04:00
/// <param name="manifest">The <see cref="PluginManifest"/> to reconcile against.</param>
/// <param name="path">The plugin path.</param>
/// <returns>The reconciled <see cref="PluginManifest"/>.</returns>
2023-04-16 20:47:57 -04:00
private async Task < bool > ReconcileManifest ( PluginManifest manifest , string path )
2023-04-09 12:53:09 -04:00
{
2023-04-16 09:46:12 -04:00
try
2023-04-09 12:53:09 -04:00
{
2023-04-16 09:46:12 -04:00
var metafile = Path . Combine ( path , MetafileName ) ;
if ( ! File . Exists ( metafile ) )
{
_logger . LogInformation ( "No local manifest exists for plugin {Plugin}. Skipping manifest reconciliation." , manifest . Name ) ;
return true ;
}
2023-04-09 12:53:09 -04:00
2023-04-16 20:47:57 -04:00
using var metaStream = File . OpenRead ( metafile ) ;
2023-10-11 12:32:57 -04:00
var localManifest = await JsonSerializer . DeserializeAsync < PluginManifest > ( metaStream , _jsonOptions ) . ConfigureAwait ( false ) ;
2023-04-16 20:47:57 -04:00
localManifest ? ? = new PluginManifest ( ) ;
2023-04-09 12:53:09 -04:00
2023-04-16 09:46:12 -04:00
if ( ! Equals ( localManifest . Id , manifest . Id ) )
2023-04-09 12:53:09 -04:00
{
2023-04-16 09:46:12 -04:00
_logger . LogError ( "The manifest ID {LocalUUID} did not match the package info ID {PackageUUID}." , localManifest . Id , manifest . Id ) ;
manifest . Status = PluginStatus . Malfunctioned ;
2023-04-09 12:53:09 -04:00
}
2023-04-16 09:46:12 -04:00
if ( localManifest . Version ! = manifest . Version )
2023-04-09 12:53:09 -04:00
{
2023-04-16 09:46:12 -04:00
// Package information provides the version and is the source of truth. Pre-packages meta.json is assumed to be a mistake in this regard.
_logger . LogWarning ( "The version of the local manifest was {LocalVersion}, but {PackageVersion} was expected. The value will be replaced." , localManifest . Version , manifest . Version ) ;
2023-04-09 12:53:09 -04:00
}
2023-04-16 09:46:12 -04:00
// Explicitly mapping properties instead of using reflection is preferred here.
manifest . Category = string . IsNullOrEmpty ( localManifest . Category ) ? manifest . Category : localManifest . Category ;
manifest . AutoUpdate = localManifest . AutoUpdate ; // Preserve whatever is local. Package info does not have this property.
manifest . Changelog = string . IsNullOrEmpty ( localManifest . Changelog ) ? manifest . Changelog : localManifest . Changelog ;
manifest . Description = string . IsNullOrEmpty ( localManifest . Description ) ? manifest . Description : localManifest . Description ;
manifest . Name = string . IsNullOrEmpty ( localManifest . Name ) ? manifest . Name : localManifest . Name ;
manifest . Overview = string . IsNullOrEmpty ( localManifest . Overview ) ? manifest . Overview : localManifest . Overview ;
manifest . Owner = string . IsNullOrEmpty ( localManifest . Owner ) ? manifest . Owner : localManifest . Owner ;
manifest . TargetAbi = string . IsNullOrEmpty ( localManifest . TargetAbi ) ? manifest . TargetAbi : localManifest . TargetAbi ;
2023-04-16 20:47:57 -04:00
manifest . Timestamp = localManifest . Timestamp . Equals ( default ) ? manifest . Timestamp : localManifest . Timestamp ;
2023-04-16 09:46:12 -04:00
manifest . ImagePath = string . IsNullOrEmpty ( localManifest . ImagePath ) ? manifest . ImagePath : localManifest . ImagePath ;
manifest . Assemblies = localManifest . Assemblies ;
return true ;
}
catch ( Exception e )
{
_logger . LogWarning ( e , "Unable to reconcile plugin manifest due to an error. {Path}" , path ) ;
return false ;
2023-04-09 12:53:09 -04:00
}
}
2020-12-06 18:48:54 -05:00
/// <summary>
/// Changes a plugin's load status.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> instance.</param>
/// <param name="state">The <see cref="PluginStatus"/> of the plugin.</param>
/// <returns>Success of the task.</returns>
private bool ChangePluginState ( LocalPlugin plugin , PluginStatus state )
{
if ( plugin . Manifest . Status = = state | | string . IsNullOrEmpty ( plugin . Path ) )
{
// No need to save as the state hasn't changed.
return true ;
}
plugin . Manifest . Status = state ;
2020-12-15 11:37:11 -05:00
return SaveManifest ( plugin . Manifest , plugin . Path ) ;
2020-12-06 18:48:54 -05:00
}
/// <summary>
2020-12-22 10:01:26 -05:00
/// Finds the plugin record using the assembly.
2020-12-06 18:48:54 -05:00
/// </summary>
2020-12-22 10:01:26 -05:00
/// <param name="assembly">The <see cref="Assembly"/> being sought.</param>
2020-12-06 18:48:54 -05:00
/// <returns>The matching record, or null if not found.</returns>
2020-12-22 10:01:26 -05:00
private LocalPlugin ? GetPluginByAssembly ( Assembly assembly )
2020-12-06 18:48:54 -05:00
{
// Find which plugin it is by the path.
2021-01-19 16:15:40 -05:00
return _plugins . FirstOrDefault ( p = > p . DllFiles . Contains ( assembly . Location , StringComparer . Ordinal ) ) ;
2020-12-06 18:48:54 -05:00
}
/// <summary>
/// Creates the instance safe.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>System.Object.</returns>
2020-12-18 03:26:11 -05:00
private IPlugin ? CreatePluginInstance ( Type type )
2020-12-06 18:48:54 -05:00
{
// Find the record for this plugin.
2020-12-22 10:01:26 -05:00
var plugin = GetPluginByAssembly ( type . Assembly ) ;
2020-12-14 19:42:59 -05:00
if ( plugin ? . Manifest . Status < PluginStatus . Active )
{
return null ;
}
2020-12-06 18:48:54 -05:00
try
{
_logger . LogDebug ( "Creating instance of {Type}" , type ) ;
2021-05-28 08:33:54 -04:00
// _appHost.ServiceProvider is already assigned when we create the plugins
var instance = ( IPlugin ) ActivatorUtilities . CreateInstance ( _appHost . ServiceProvider ! , type ) ;
2022-12-05 09:00:20 -05:00
if ( plugin is null )
2020-12-06 18:48:54 -05:00
{
// Create a dummy record for the providers.
2021-02-12 08:33:10 -05:00
// TODO: remove this code once all provided have been released as separate plugins.
2020-12-06 18:48:54 -05:00
plugin = new LocalPlugin (
2020-12-18 04:04:40 -05:00
instance . AssemblyFilePath ,
2020-12-06 18:48:54 -05:00
true ,
new PluginManifest
{
2020-12-18 15:37:35 -05:00
Id = instance . Id ,
2020-12-06 18:48:54 -05:00
Status = PluginStatus . Active ,
2020-12-18 04:04:40 -05:00
Name = instance . Name ,
Version = instance . Version . ToString ( )
2020-12-06 18:48:54 -05:00
} )
{
2020-12-18 04:04:40 -05:00
Instance = instance
2020-12-06 18:48:54 -05:00
} ;
_plugins . Add ( plugin ) ;
plugin . Manifest . Status = PluginStatus . Active ;
}
else
{
2020-12-18 03:24:26 -05:00
plugin . Instance = instance ;
2020-12-06 18:48:54 -05:00
var manifest = plugin . Manifest ;
2021-01-19 16:15:40 -05:00
var pluginStr = instance . Version . ToString ( ) ;
2020-12-15 05:05:04 -05:00
bool changed = false ;
2021-01-19 16:15:40 -05:00
if ( string . Equals ( manifest . Version , pluginStr , StringComparison . Ordinal )
2022-02-21 08:15:09 -05:00
| | ! manifest . Id . Equals ( instance . Id ) )
2020-12-06 18:48:54 -05:00
{
// If a plugin without a manifest failed to load due to an external issue (eg config),
// this updates the manifest to the actual plugin values.
manifest . Version = pluginStr ;
manifest . Name = plugin . Instance . Name ;
manifest . Description = plugin . Instance . Description ;
2021-01-19 16:15:40 -05:00
manifest . Id = plugin . Instance . Id ;
2020-12-15 05:05:04 -05:00
changed = true ;
2020-12-06 18:48:54 -05:00
}
2020-12-15 05:05:04 -05:00
changed = changed | | manifest . Status ! = PluginStatus . Active ;
2020-12-06 18:48:54 -05:00
manifest . Status = PluginStatus . Active ;
2020-12-15 05:05:04 -05:00
if ( changed )
{
SaveManifest ( manifest , plugin . Path ) ;
}
2020-12-06 18:48:54 -05:00
}
_logger . LogInformation ( "Loaded plugin: {PluginName} {PluginVersion}" , plugin . Name , plugin . Version ) ;
return instance ;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch ( Exception ex )
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger . LogError ( ex , "Error creating {Type}" , type . FullName ) ;
2022-12-05 09:01:13 -05:00
if ( plugin is not null )
2020-12-06 18:48:54 -05:00
{
2020-12-17 08:44:38 -05:00
if ( ChangePluginState ( plugin , PluginStatus . Malfunctioned ) )
2020-12-06 18:48:54 -05:00
{
_logger . LogInformation ( "Plugin {Path} has been disabled." , plugin . Path ) ;
return null ;
}
}
_logger . LogDebug ( "Unable to auto-disable." ) ;
return null ;
}
}
2020-12-18 17:17:46 -05:00
private void UpdatePluginSuperceedStatus ( LocalPlugin plugin )
2020-12-06 18:48:54 -05:00
{
if ( plugin . Manifest . Status ! = PluginStatus . Superceded )
{
return ;
}
var predecessor = _plugins . OrderByDescending ( p = > p . Version )
. FirstOrDefault ( p = > p . Id . Equals ( plugin . Id ) & & p . IsEnabledAndSupported & & p . Version ! = plugin . Version ) ;
2022-12-05 09:01:13 -05:00
if ( predecessor is not null )
2020-12-06 18:48:54 -05:00
{
return ;
}
plugin . Manifest . Status = PluginStatus . Active ;
}
/// <summary>
/// Attempts to delete a plugin.
/// </summary>
/// <param name="plugin">A <see cref="LocalPlugin"/> instance to delete.</param>
/// <returns>True if successful.</returns>
private bool DeletePlugin ( LocalPlugin plugin )
{
// Attempt a cleanup of old folders.
try
{
Directory . Delete ( plugin . Path , true ) ;
2020-12-15 11:37:11 -05:00
_logger . LogDebug ( "Deleted {Path}" , plugin . Path ) ;
2020-12-06 18:48:54 -05:00
}
#pragma warning disable CA1031 // Do not catch general exception types
2020-12-15 11:37:11 -05:00
catch
2020-12-06 18:48:54 -05:00
#pragma warning restore CA1031 // Do not catch general exception types
{
return false ;
}
return _plugins . Remove ( plugin ) ;
}
2021-02-08 11:10:20 -05:00
internal LocalPlugin LoadManifest ( string dir )
2020-12-06 18:48:54 -05:00
{
2020-12-18 17:17:46 -05:00
Version ? version ;
PluginManifest ? manifest = null ;
2023-04-09 12:53:09 -04:00
var metafile = Path . Combine ( dir , MetafileName ) ;
2020-12-18 17:17:46 -05:00
if ( File . Exists ( metafile ) )
2020-12-06 18:48:54 -05:00
{
2021-01-12 10:03:13 -05:00
// Only path where this stays null is when File.ReadAllBytes throws an IOException
byte [ ] data = null ! ;
2020-12-18 17:17:46 -05:00
try
2020-12-06 18:48:54 -05:00
{
2021-01-12 10:03:13 -05:00
data = File . ReadAllBytes ( metafile ) ;
2020-12-18 17:17:46 -05:00
manifest = JsonSerializer . Deserialize < PluginManifest > ( data , _jsonOptions ) ;
}
2021-01-12 10:03:13 -05:00
catch ( IOException ex )
2020-12-06 18:48:54 -05:00
{
2021-01-12 10:03:13 -05:00
_logger . LogError ( ex , "Error reading file {Path}." , dir ) ;
2020-12-06 18:48:54 -05:00
}
2021-01-12 10:03:13 -05:00
catch ( JsonException ex )
2020-12-06 18:48:54 -05:00
{
2023-07-29 15:35:38 -04:00
_logger . LogError ( ex , "Error deserializing {Json}." , Encoding . UTF8 . GetString ( data ) ) ;
2020-12-06 18:48:54 -05:00
}
2020-12-18 17:17:46 -05:00
2022-12-05 09:01:13 -05:00
if ( manifest is not null )
2020-12-06 18:48:54 -05:00
{
2021-01-12 10:03:13 -05:00
if ( ! Version . TryParse ( manifest . TargetAbi , out var targetAbi ) )
{
targetAbi = _minimumVersion ;
}
if ( ! Version . TryParse ( manifest . Version , out version ) )
{
manifest . Version = _minimumVersion . ToString ( ) ;
}
2020-12-06 18:48:54 -05:00
2021-01-12 10:03:13 -05:00
return new LocalPlugin ( dir , _appVersion > = targetAbi , manifest ) ;
}
2020-12-18 17:17:46 -05:00
}
2020-12-06 18:48:54 -05:00
2020-12-18 17:17:46 -05:00
// No metafile, so lets see if the folder is versioned.
// TODO: Phase this support out in future versions.
metafile = dir . Split ( Path . DirectorySeparatorChar , StringSplitOptions . RemoveEmptyEntries ) [ ^ 1 ] ;
int versionIndex = dir . LastIndexOf ( '_' ) ;
if ( versionIndex ! = - 1 )
{
// Get the version number from the filename if possible.
2021-12-15 12:25:36 -05:00
metafile = Path . GetFileName ( dir [ . . versionIndex ] ) ;
2020-12-18 17:17:46 -05:00
version = Version . TryParse ( dir . AsSpan ( ) [ ( versionIndex + 1 ) . . ] , out Version ? parsedVersion ) ? parsedVersion : _appVersion ;
2020-12-06 18:48:54 -05:00
}
2020-12-18 17:17:46 -05:00
else
2020-12-06 18:48:54 -05:00
{
2020-12-18 17:17:46 -05:00
// Un-versioned folder - Add it under the path name and version it suitable for this instance.
version = _appVersion ;
2020-12-06 18:48:54 -05:00
}
2020-12-18 17:17:46 -05:00
// Auto-create a plugin manifest, so we can disable it, if it fails to load.
manifest = new PluginManifest
{
2021-01-19 16:15:40 -05:00
Status = PluginStatus . Active ,
2020-12-18 17:17:46 -05:00
Name = metafile ,
AutoUpdate = false ,
Id = metafile . GetMD5 ( ) ,
TargetAbi = _appVersion . ToString ( ) ,
Version = version . ToString ( )
} ;
return new LocalPlugin ( dir , true , manifest ) ;
2020-12-06 18:48:54 -05:00
}
/// <summary>
/// Gets the list of local plugins.
/// </summary>
/// <returns>Enumerable of local plugins.</returns>
private IEnumerable < LocalPlugin > DiscoverPlugins ( )
{
var versions = new List < LocalPlugin > ( ) ;
if ( ! Directory . Exists ( _pluginsPath ) )
{
// Plugin path doesn't exist, don't try to enumerate sub-folders.
return Enumerable . Empty < LocalPlugin > ( ) ;
}
var directories = Directory . EnumerateDirectories ( _pluginsPath , "*.*" , SearchOption . TopDirectoryOnly ) ;
foreach ( var dir in directories )
{
2020-12-18 17:17:46 -05:00
versions . Add ( LoadManifest ( dir ) ) ;
2020-12-06 18:48:54 -05:00
}
string lastName = string . Empty ;
versions . Sort ( LocalPlugin . Compare ) ;
// Traverse backwards through the list.
// The first item will be the latest version.
for ( int x = versions . Count - 1 ; x > = 0 ; x - - )
{
2020-12-18 17:17:46 -05:00
var entry = versions [ x ] ;
2020-12-06 18:48:54 -05:00
if ( ! string . Equals ( lastName , entry . Name , StringComparison . OrdinalIgnoreCase ) )
{
2023-03-30 10:59:21 -04:00
if ( ! TryGetPluginDlls ( entry , out var allowedDlls ) )
{
_logger . LogError ( "One or more assembly paths was invalid. Marking plugin {Plugin} as \"Malfunctioned\"." , entry . Name ) ;
ChangePluginState ( entry , PluginStatus . Malfunctioned ) ;
continue ;
}
entry . DllFiles = allowedDlls ;
2020-12-06 18:48:54 -05:00
if ( entry . IsEnabledAndSupported )
{
lastName = entry . Name ;
continue ;
}
}
if ( string . IsNullOrEmpty ( lastName ) )
{
continue ;
}
var cleaned = false ;
var path = entry . Path ;
if ( _config . RemoveOldPlugins )
{
// Attempt a cleanup of old folders.
try
{
_logger . LogDebug ( "Deleting {Path}" , path ) ;
Directory . Delete ( path , true ) ;
cleaned = true ;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch ( Exception e )
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger . LogWarning ( e , "Unable to delete {Path}" , path ) ;
}
2020-12-15 11:37:11 -05:00
if ( cleaned )
2020-12-06 18:48:54 -05:00
{
2020-12-15 11:37:11 -05:00
versions . RemoveAt ( x ) ;
2020-12-06 18:48:54 -05:00
}
2020-12-15 11:37:11 -05:00
else
2020-12-15 05:05:04 -05:00
{
2020-12-18 16:59:14 -05:00
ChangePluginState ( entry , PluginStatus . Deleted ) ;
2020-12-15 05:05:04 -05:00
}
2020-12-06 18:48:54 -05:00
}
}
// Only want plugin folders which have files.
return versions . Where ( p = > p . DllFiles . Count ! = 0 ) ;
}
2020-12-18 04:04:40 -05:00
2023-03-30 10:59:21 -04:00
/// <summary>
/// Attempts to retrieve valid DLLs from the plugin path. This method will consider the assembly whitelist
/// from the manifest.
/// </summary>
/// <remarks>
/// Loading DLLs from externally supplied paths introduces a path traversal risk. This method
/// uses a safelisting tactic of considering DLLs from the plugin directory and only using
/// the plugin's canonicalized assembly whitelist for comparison. See
/// <see href="https://owasp.org/www-community/attacks/Path_Traversal"/> for more details.
/// </remarks>
/// <param name="plugin">The plugin.</param>
/// <param name="whitelistedDlls">The whitelisted DLLs. If the method returns <see langword="false"/>, this will be empty.</param>
/// <returns>
/// <see langword="true"/> if all assemblies listed in the manifest were available in the plugin directory.
/// <see langword="false"/> if any assemblies were invalid or missing from the plugin directory.
/// </returns>
/// <exception cref="ArgumentNullException">If the <see cref="LocalPlugin"/> is null.</exception>
private bool TryGetPluginDlls ( LocalPlugin plugin , out IReadOnlyList < string > whitelistedDlls )
{
2023-04-01 06:59:07 -04:00
ArgumentNullException . ThrowIfNull ( nameof ( plugin ) ) ;
2023-03-30 10:59:21 -04:00
IReadOnlyList < string > pluginDlls = Directory . GetFiles ( plugin . Path , "*.dll" , SearchOption . AllDirectories ) ;
whitelistedDlls = Array . Empty < string > ( ) ;
if ( pluginDlls . Count > 0 & & plugin . Manifest . Assemblies . Count > 0 )
{
_logger . LogInformation ( "Registering whitelisted assemblies for plugin \"{Plugin}\"..." , plugin . Name ) ;
var canonicalizedPaths = new List < string > ( ) ;
foreach ( var path in plugin . Manifest . Assemblies )
{
var canonicalized = Path . Combine ( plugin . Path , path ) . Canonicalize ( ) ;
// Ensure we stay in the plugin directory.
2023-04-16 20:47:57 -04:00
if ( ! canonicalized . StartsWith ( plugin . Path . NormalizePath ( ) , StringComparison . Ordinal ) )
2023-03-30 10:59:21 -04:00
{
_logger . LogError ( "Assembly path {Path} is not inside the plugin directory." , path ) ;
return false ;
}
canonicalizedPaths . Add ( canonicalized ) ;
}
var intersected = pluginDlls . Intersect ( canonicalizedPaths ) . ToList ( ) ;
if ( intersected . Count ! = canonicalizedPaths . Count )
{
_logger . LogError ( "Plugin {Plugin} contained assembly paths that were not found in the directory." , plugin . Name ) ;
return false ;
}
whitelistedDlls = intersected ;
}
else
{
// No whitelist, default to loading all DLLs in plugin directory.
whitelistedDlls = pluginDlls ;
}
return true ;
}
2020-12-18 04:04:40 -05:00
/// <summary>
/// Changes the status of the other versions of the plugin to "Superceded".
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> that's master.</param>
2020-12-23 11:28:50 -05:00
private void ProcessAlternative ( LocalPlugin plugin )
2020-12-18 04:04:40 -05:00
{
// Detect whether there is another version of this plugin that needs disabling.
2020-12-23 11:28:50 -05:00
var previousVersion = _plugins . OrderByDescending ( p = > p . Version )
2020-12-18 04:04:40 -05:00
. FirstOrDefault (
p = > p . Id . Equals ( plugin . Id )
& & p . IsEnabledAndSupported
& & p . Version ! = plugin . Version ) ;
2022-12-05 09:00:20 -05:00
if ( previousVersion is null )
2020-12-18 04:04:40 -05:00
{
2020-12-23 11:28:50 -05:00
// This value is memory only - so that the web will show restart required.
plugin . Manifest . Status = PluginStatus . Restart ;
2022-11-11 10:32:29 -05:00
plugin . Manifest . AutoUpdate = false ;
2020-12-18 04:04:40 -05:00
return ;
}
2020-12-23 11:28:50 -05:00
if ( plugin . Manifest . Status = = PluginStatus . Active & & ! ChangePluginState ( previousVersion , PluginStatus . Superceded ) )
{
_logger . LogError ( "Unable to enable version {Version} of {Name}" , previousVersion . Version , previousVersion . Name ) ;
}
else if ( plugin . Manifest . Status = = PluginStatus . Superceded & & ! ChangePluginState ( previousVersion , PluginStatus . Active ) )
2020-12-18 04:04:40 -05:00
{
2020-12-23 11:28:50 -05:00
_logger . LogError ( "Unable to supercede version {Version} of {Name}" , previousVersion . Version , previousVersion . Name ) ;
2020-12-18 04:04:40 -05:00
}
2020-12-23 11:28:50 -05:00
// This value is memory only - so that the web will show restart required.
plugin . Manifest . Status = PluginStatus . Restart ;
2022-11-11 10:32:29 -05:00
plugin . Manifest . AutoUpdate = false ;
2020-12-18 04:04:40 -05:00
}
2020-12-06 18:48:54 -05:00
}
}