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 ;
using System.Text ;
using System.Text.Json ;
2021-02-23 09:11:17 -05:00
using System.Threading.Tasks ;
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 ;
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 ;
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>
public class PluginManager : IPluginManager
{
private readonly string _pluginsPath ;
private readonly Version _appVersion ;
private readonly JsonSerializerOptions _jsonOptions ;
private readonly ILogger < PluginManager > _logger ;
private readonly IApplicationHost _appHost ;
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>
2020-12-18 04:04:40 -05:00
/// <param name="logger">The <see cref="ILogger"/>.</param>
2020-12-06 18:48:54 -05:00
/// <param name="appHost">The <see cref="IApplicationHost"/>.</param>
/// <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 ,
2020-12-06 18:48:54 -05:00
IApplicationHost appHost ,
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 > ( ) ;
}
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 ;
}
2020-12-06 18:48:54 -05:00
foreach ( var file in plugin . DllFiles )
{
2020-12-15 11:37:11 -05:00
Assembly assembly ;
2020-12-06 18:48:54 -05:00
try
{
2020-12-15 11:37:11 -05:00
assembly = Assembly . LoadFrom ( file ) ;
2020-12-31 06:39:34 -05:00
2021-11-06 17:44:05 -04:00
// Load all required types to verify that the plugin will load
assembly . GetTypes ( ) ;
2020-12-06 18:48:54 -05:00
}
catch ( FileLoadException ex )
{
_logger . LogError ( ex , "Failed to load assembly {Path}. Disabling plugin." , file ) ;
2020-12-17 08:44:38 -05:00
ChangePluginState ( plugin , PluginStatus . Malfunctioned ) ;
2020-12-06 18:48:54 -05:00
continue ;
}
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
{
_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." , file ) ;
ChangePluginState ( plugin , PluginStatus . NotSupported ) ;
continue ;
}
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
{
_logger . LogError ( ex , "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin." , file ) ;
ChangePluginState ( plugin , PluginStatus . Malfunctioned ) ;
continue ;
}
2020-12-06 18:48:54 -05:00
2020-12-15 11:37:11 -05:00
_logger . LogInformation ( "Loaded assembly {Assembly} from {Path}" , assembly . FullName , file ) ;
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 ) ;
2020-12-06 18:48:54 -05:00
if ( plugin = = null )
{
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 ) ;
instance ? . RegisterServices ( serviceCollection ) ;
}
#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 )
{
if ( string . IsNullOrEmpty ( folder ) )
{
throw new ArgumentNullException ( nameof ( folder ) ) ;
}
// 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 )
{
if ( plugin = = null )
{
throw new ArgumentNullException ( nameof ( plugin ) ) ;
}
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 ;
2020-12-06 18:48:54 -05:00
if ( version = = null )
{
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
2021-05-05 07:51:14 -04:00
plugin = plugins . FirstOrDefault ( p = > p . Instance ! = null ) ? ? plugins . OrderByDescending ( p = > p . Version ) . FirstOrDefault ( ) ;
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 )
{
if ( plugin = = null )
{
throw new ArgumentNullException ( nameof ( plugin ) ) ;
}
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 )
{
if ( plugin = = null )
{
throw new ArgumentNullException ( nameof ( plugin ) ) ;
}
// 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.
if ( assembly = = null )
{
throw new ArgumentNullException ( nameof ( assembly ) ) ;
}
2020-12-18 16:56:54 -05:00
var plugin = _plugins . FirstOrDefault ( p = > p . DllFiles . Contains ( assembly . Location ) ) ;
2020-12-06 18:48:54 -05:00
if ( plugin = = null )
{
// 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 ) ;
2021-02-08 09:38:06 -05:00
File . WriteAllText ( Path . Combine ( path , "meta.json" ) , 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/>
2021-04-06 15:59:47 -04:00
public async Task < bool > GenerateManifest ( 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
2021-06-12 16:20:35 -04:00
await using var fileStream = AsyncFile . OpenWrite ( imagePath ) ;
2021-02-23 09:11:17 -05:00
2021-02-23 10:03:26 -05:00
try
{
await using var downloadStream = await HttpClientFactory
. 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 ;
}
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 ,
2021-02-23 09:11:17 -05:00
ImagePath = imagePath
2021-02-12 08:33:10 -05:00
} ;
return SaveManifest ( manifest , path ) ;
}
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 ) ;
2020-12-06 18:48:54 -05:00
if ( plugin = = null )
{
// 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 )
| | manifest . Id ! = 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 ) ;
if ( plugin ! = null )
{
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 ) ;
if ( predecessor ! = null )
{
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 ;
var metafile = Path . Combine ( dir , "meta.json" ) ;
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
{
2021-01-12 10:03:13 -05: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
2021-01-12 10:03:13 -05:00
if ( manifest ! = 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 ) )
{
2021-03-08 23:57:38 -05:00
entry . DllFiles = Directory . GetFiles ( entry . Path , "*.dll" , SearchOption . AllDirectories ) ;
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
/// <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 ) ;
2020-12-23 11:28:50 -05:00
if ( previousVersion = = 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 ;
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 ;
2020-12-18 04:04:40 -05:00
}
2020-12-06 18:48:54 -05:00
}
}