aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Plugins/PluginManager.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/Plugins/PluginManager.cs')
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs182
1 files changed, 126 insertions, 56 deletions
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 1ab01252d..d70a15dbc 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -1,19 +1,23 @@
-#nullable enable
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Common.Json.Converters;
+using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -30,9 +34,11 @@ namespace Emby.Server.Implementations.Plugins
private readonly ILogger<PluginManager> _logger;
private readonly IApplicationHost _appHost;
private readonly ServerConfiguration _config;
- private readonly IList<LocalPlugin> _plugins;
+ private readonly List<LocalPlugin> _plugins;
private readonly Version _minimumVersion;
+ private IHttpClientFactory? _httpClientFactory;
+
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
@@ -51,7 +57,7 @@ namespace Emby.Server.Implementations.Plugins
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_pluginsPath = pluginsPath;
_appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
- _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions())
+ _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options)
{
WriteIndented = true
};
@@ -72,10 +78,18 @@ namespace Emby.Server.Implementations.Plugins
_plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
}
+ private IHttpClientFactory HttpClientFactory
+ {
+ get
+ {
+ return _httpClientFactory ??= _appHost.Resolve<IHttpClientFactory>();
+ }
+ }
+
/// <summary>
/// Gets the Plugins.
/// </summary>
- public IList<LocalPlugin> Plugins => _plugins;
+ public IReadOnlyList<LocalPlugin> Plugins => _plugins;
/// <summary>
/// Returns all the assemblies.
@@ -112,9 +126,8 @@ namespace Emby.Server.Implementations.Plugins
{
assembly = Assembly.LoadFrom(file);
- // This force loads all reference dll's that the plugin uses in the try..catch block.
- // Removing this will cause JF to bomb out if referenced dll's cause issues.
- assembly.GetExportedTypes();
+ // Load all required types to verify that the plugin will load
+ assembly.GetTypes();
}
catch (FileLoadException ex)
{
@@ -122,6 +135,20 @@ namespace Emby.Server.Implementations.Plugins
ChangePluginState(plugin, PluginStatus.Malfunctioned);
continue;
}
+ catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception
+ {
+ _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;
+ }
+#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;
+ }
_logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
yield return assembly;
@@ -134,9 +161,7 @@ namespace Emby.Server.Implementations.Plugins
/// </summary>
public void CreatePlugins()
{
- _ = _appHost.GetExports<IPlugin>(CreatePluginInstance)
- .Where(i => i != null)
- .ToArray();
+ _ = _appHost.GetExports<IPlugin>(CreatePluginInstance);
}
/// <summary>
@@ -246,11 +271,7 @@ namespace Emby.Server.Implementations.Plugins
// If no version is given, return the current instance.
var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList();
- plugin = plugins.FirstOrDefault(p => p.Instance != null);
- if (plugin == null)
- {
- plugin = plugins.OrderByDescending(p => p.Version).FirstOrDefault();
- }
+ plugin = plugins.FirstOrDefault(p => p.Instance != null) ?? plugins.OrderByDescending(p => p.Version).FirstOrDefault();
}
else
{
@@ -320,34 +341,76 @@ namespace Emby.Server.Implementations.Plugins
ChangePluginState(plugin, PluginStatus.Malfunctioned);
}
- /// <summary>
- /// Saves the manifest back to disk.
- /// </summary>
- /// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
- /// <param name="path">The path where to save the manifest.</param>
- /// <returns>True if successful.</returns>
+ /// <inheritdoc/>
public bool SaveManifest(PluginManifest manifest, string path)
{
- if (manifest == null)
- {
- return false;
- }
-
try
{
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
- File.WriteAllText(Path.Combine(path, "meta.json"), data, Encoding.UTF8);
+ File.WriteAllText(Path.Combine(path, "meta.json"), data);
return true;
}
-#pragma warning disable CA1031 // Do not catch general exception types
- catch (Exception e)
-#pragma warning restore CA1031 // Do not catch general exception types
+ catch (ArgumentException e)
{
- _logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
+ _logger.LogWarning(e, "Unable to save plugin manifest due to invalid value. {Path}", path);
return false;
}
}
+ /// <inheritdoc/>
+ public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
+ {
+ if (packageInfo == null)
+ {
+ return false;
+ }
+
+ var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
+ var imagePath = string.Empty;
+
+ if (!string.IsNullOrEmpty(packageInfo.ImageUrl))
+ {
+ var url = new Uri(packageInfo.ImageUrl);
+ imagePath = Path.Join(path, url.Segments[^1]);
+
+ await using var fileStream = AsyncFile.OpenWrite(imagePath);
+
+ 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;
+ }
+ }
+
+ var manifest = new PluginManifest
+ {
+ Category = packageInfo.Category,
+ Changelog = versionInfo.Changelog ?? string.Empty,
+ Description = packageInfo.Description,
+ Id = packageInfo.Id,
+ Name = packageInfo.Name,
+ Overview = packageInfo.Overview,
+ Owner = packageInfo.Owner,
+ TargetAbi = versionInfo.TargetAbi ?? string.Empty,
+ Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture),
+ Version = versionInfo.Version,
+ Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
+ AutoUpdate = true,
+ ImagePath = imagePath
+ };
+
+ return SaveManifest(manifest, path);
+ }
+
/// <summary>
/// Changes a plugin's load status.
/// </summary>
@@ -374,7 +437,7 @@ namespace Emby.Server.Implementations.Plugins
private LocalPlugin? GetPluginByAssembly(Assembly assembly)
{
// Find which plugin it is by the path.
- return _plugins.FirstOrDefault(p => string.Equals(p.Path, Path.GetDirectoryName(assembly.Location), StringComparison.Ordinal));
+ return _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location, StringComparer.Ordinal));
}
/// <summary>
@@ -394,11 +457,12 @@ namespace Emby.Server.Implementations.Plugins
try
{
_logger.LogDebug("Creating instance of {Type}", type);
- var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type);
+ // _appHost.ServiceProvider is already assigned when we create the plugins
+ var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider!, type);
if (plugin == null)
{
// Create a dummy record for the providers.
- // TODO: remove this code, if all provided have been released as separate plugins.
+ // TODO: remove this code once all provided have been released as separate plugins.
plugin = new LocalPlugin(
instance.AssemblyFilePath,
true,
@@ -421,15 +485,17 @@ namespace Emby.Server.Implementations.Plugins
{
plugin.Instance = instance;
var manifest = plugin.Manifest;
- var pluginStr = plugin.Instance.Version.ToString();
+ var pluginStr = instance.Version.ToString();
bool changed = false;
- if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal))
+ if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal)
+ || manifest.Id != instance.Id)
{
// 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;
+ manifest.Id = plugin.Instance.Id;
changed = true;
}
@@ -505,39 +571,43 @@ namespace Emby.Server.Implementations.Plugins
return _plugins.Remove(plugin);
}
- private LocalPlugin LoadManifest(string dir)
+ internal LocalPlugin LoadManifest(string dir)
{
Version? version;
PluginManifest? manifest = null;
var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
+ // Only path where this stays null is when File.ReadAllBytes throws an IOException
+ byte[] data = null!;
try
{
- var data = File.ReadAllText(metafile, Encoding.UTF8);
+ data = File.ReadAllBytes(metafile);
manifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions);
}
-#pragma warning disable CA1031 // Do not catch general exception types
- catch (Exception ex)
-#pragma warning restore CA1031 // Do not catch general exception types
+ catch (IOException ex)
{
- _logger.LogError(ex, "Error deserializing {Path}.", dir);
+ _logger.LogError(ex, "Error reading file {Path}.", dir);
}
- }
-
- if (manifest != null)
- {
- if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+ catch (JsonException ex)
{
- targetAbi = _minimumVersion;
+ _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data!));
}
- if (!Version.TryParse(manifest.Version, out version))
+ if (manifest != null)
{
- manifest.Version = _minimumVersion.ToString();
- }
+ if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+ {
+ targetAbi = _minimumVersion;
+ }
- return new LocalPlugin(dir, _appVersion >= targetAbi, manifest);
+ if (!Version.TryParse(manifest.Version, out version))
+ {
+ manifest.Version = _minimumVersion.ToString();
+ }
+
+ return new LocalPlugin(dir, _appVersion >= targetAbi, manifest);
+ }
}
// No metafile, so lets see if the folder is versioned.
@@ -559,7 +629,7 @@ namespace Emby.Server.Implementations.Plugins
// Auto-create a plugin manifest, so we can disable it, if it fails to load.
manifest = new PluginManifest
{
- Status = PluginStatus.Restart,
+ Status = PluginStatus.Active,
Name = metafile,
AutoUpdate = false,
Id = metafile.GetMD5(),
@@ -599,7 +669,7 @@ namespace Emby.Server.Implementations.Plugins
var entry = versions[x];
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
{
- entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories));
+ entry.DllFiles = Directory.GetFiles(entry.Path, "*.dll", SearchOption.AllDirectories);
if (entry.IsEnabledAndSupported)
{
lastName = entry.Name;