aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
authorGreenback <jimcartlidge@yahoo.co.uk>2020-12-06 23:48:54 +0000
committerGreenback <jimcartlidge@yahoo.co.uk>2020-12-14 16:14:39 +0000
commit7986465cf785ca385fd1765326887e550bced033 (patch)
tree8859e9b336326081b9dfdc8d550d7f7d6d6652b7 /Emby.Server.Implementations
parentf2c2beca0f9a249edd7cc967c328d32ebdc30092 (diff)
Initial upload
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs250
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj8
-rw-r--r--Emby.Server.Implementations/Plugins/Active.pngbin0 -> 1422 bytes
-rw-r--r--Emby.Server.Implementations/Plugins/Malfunction.pngbin0 -> 2091 bytes
-rw-r--r--Emby.Server.Implementations/Plugins/NotSupported.pngbin0 -> 2046 bytes
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs674
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManifest.cs60
-rw-r--r--Emby.Server.Implementations/Plugins/RestartRequired.pngbin0 -> 1996 bytes
-rw-r--r--Emby.Server.Implementations/Plugins/Superceded.pngbin0 -> 2136 bytes
-rw-r--r--Emby.Server.Implementations/Plugins/blank.pngbin0 -> 120 bytes
-rw-r--r--Emby.Server.Implementations/Plugins/disabled.pngbin0 -> 1790 bytes
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs2
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs456
13 files changed, 991 insertions, 459 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index d74ea0352..f88c6c620 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -34,7 +34,6 @@ using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
-using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
@@ -119,7 +118,9 @@ namespace Emby.Server.Implementations
private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
+ private readonly IPluginManager _pluginManager;
+ private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private string[] _urlPrefixes;
@@ -181,16 +182,6 @@ namespace Emby.Server.Implementations
protected IServiceCollection ServiceCollection { get; }
- private IPlugin[] _plugins;
-
- private IReadOnlyList<LocalPlugin> _pluginsManifests;
-
- /// <summary>
- /// Gets the plugins.
- /// </summary>
- /// <value>The plugins.</value>
- public IReadOnlyList<IPlugin> Plugins => _plugins;
-
/// <summary>
/// Gets the logger factory.
/// </summary>
@@ -294,6 +285,14 @@ namespace Emby.Server.Implementations
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
+
+ _pluginManager = new PluginManager(
+ LoggerFactory,
+ this,
+ ServerConfigurationManager.Configuration,
+ ApplicationPaths.PluginsPath,
+ ApplicationPaths.CachePath,
+ ApplicationVersion);
}
/// <summary>
@@ -393,8 +392,26 @@ namespace Emby.Server.Implementations
/// <returns>System.Object.</returns>
protected object CreateInstanceSafe(Type type)
{
+ if (_creatingInstances == null)
+ {
+ _creatingInstances = new List<Type>();
+ }
+
+ if (_creatingInstances.IndexOf(type) != -1)
+ {
+ Logger.LogError("DI Loop detected.");
+ Logger.LogError("Attempted creation of {Type}", type.FullName);
+ foreach (var entry in _creatingInstances)
+ {
+ Logger.LogError("Called from: {stack}", entry.FullName);
+ }
+
+ throw new ExternalException("DI Loop detected.");
+ }
+
try
{
+ _creatingInstances.Add(type);
Logger.LogDebug("Creating instance of {Type}", type);
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
}
@@ -403,6 +420,10 @@ namespace Emby.Server.Implementations
Logger.LogError(ex, "Error creating {Type}", type);
return null;
}
+ finally
+ {
+ _creatingInstances.Remove(type);
+ }
}
/// <summary>
@@ -412,11 +433,7 @@ namespace Emby.Server.Implementations
/// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>();
- /// <summary>
- /// Gets the export types.
- /// </summary>
- /// <typeparam name="T">The type.</typeparam>
- /// <returns>IEnumerable{Type}.</returns>
+ /// <inheritdoc/>
public IEnumerable<Type> GetExportTypes<T>()
{
var currentType = typeof(T);
@@ -445,6 +462,27 @@ namespace Emby.Server.Implementations
return parts;
}
+ /// <inheritdoc />
+ public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
+ {
+ // Convert to list so this isn't executed for each iteration
+ var parts = GetExportTypes<T>()
+ .Select(i => defaultFunc(i))
+ .Where(i => i != null)
+ .Cast<T>()
+ .ToList();
+
+ if (manageLifetime)
+ {
+ lock (_disposableParts)
+ {
+ _disposableParts.AddRange(parts.OfType<IDisposable>());
+ }
+ }
+
+ return parts;
+ }
+
/// <summary>
/// Runs the startup tasks.
/// </summary>
@@ -509,7 +547,7 @@ namespace Emby.Server.Implementations
RegisterServices();
- RegisterPluginServices();
+ _pluginManager.RegisterServices(ServiceCollection);
}
/// <summary>
@@ -523,7 +561,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this);
-
+ ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
@@ -768,34 +806,7 @@ namespace Emby.Server.Implementations
}
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
- _plugins = GetExports<IPlugin>()
- .Where(i => i != null)
- .ToArray();
-
- if (Plugins != null)
- {
- foreach (var plugin in Plugins)
- {
- if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin)
- {
- // Ensure the version number matches the Plugin Manifest information.
- foreach (var item in _pluginsManifests)
- {
- if (Path.GetDirectoryName(plugin.AssemblyFilePath).Equals(item.Path, StringComparison.OrdinalIgnoreCase))
- {
- // Update version number to that of the manifest.
- assemblyPlugin.SetAttributes(
- plugin.AssemblyFilePath,
- Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(plugin.AssemblyFilePath)),
- item.Version);
- break;
- }
- }
- }
-
- Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
- }
- }
+ _pluginManager.CreatePlugins();
_urlPrefixes = GetUrlPrefixes().ToArray();
@@ -834,22 +845,6 @@ namespace Emby.Server.Implementations
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
}
- private void RegisterPluginServices()
- {
- foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
- {
- try
- {
- var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
- instance.RegisterServices(ServiceCollection);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
- }
- }
- }
-
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
{
foreach (var ass in assemblies)
@@ -862,11 +857,13 @@ namespace Emby.Server.Implementations
catch (FileNotFoundException ex)
{
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
+ _pluginManager.FailPlugin(ass);
continue;
}
catch (TypeLoadException ex)
{
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
+ _pluginManager.FailPlugin(ass);
continue;
}
@@ -1005,129 +1002,15 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal();
- /// <inheritdoc/>
- public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
- {
- var minimumVersion = new Version(0, 0, 0, 1);
- var versions = new List<LocalPlugin>();
- if (!Directory.Exists(path))
- {
- // Plugin path doesn't exist, don't try to enumerate subfolders.
- return Enumerable.Empty<LocalPlugin>();
- }
-
- var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
-
- foreach (var dir in directories)
- {
- try
- {
- var metafile = Path.Combine(dir, "meta.json");
- if (File.Exists(metafile))
- {
- var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
-
- if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
- {
- targetAbi = minimumVersion;
- }
-
- if (!Version.TryParse(manifest.Version, out var version))
- {
- version = minimumVersion;
- }
-
- if (ApplicationVersion >= targetAbi)
- {
- // Only load Plugins if the plugin is built for this version or below.
- versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
- }
- }
- else
- {
- // No metafile, so lets see if the folder is versioned.
- metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
-
- int versionIndex = dir.LastIndexOf('_');
- if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion))
- {
- // Versioned folder.
- versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
- }
- else
- {
- // Un-versioned folder - Add it under the path name and version 0.0.0.1.
- versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
- }
- }
- }
- catch
- {
- continue;
- }
- }
-
- 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--)
- {
- if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
- {
- versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
- lastName = versions[x].Name;
- continue;
- }
-
- if (!string.IsNullOrEmpty(lastName) && cleanup)
- {
- // Attempt a cleanup of old folders.
- try
- {
- Logger.LogDebug("Deleting {Path}", versions[x].Path);
- Directory.Delete(versions[x].Path, true);
- }
- catch (Exception e)
- {
- Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
- }
-
- versions.RemoveAt(x);
- }
- }
-
- return versions;
- }
-
/// <summary>
/// Gets the composable part assemblies.
/// </summary>
/// <returns>IEnumerable{Assembly}.</returns>
protected IEnumerable<Assembly> GetComposablePartAssemblies()
{
- if (Directory.Exists(ApplicationPaths.PluginsPath))
+ foreach (var p in _pluginManager.LoadAssemblies())
{
- _pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList();
- foreach (var plugin in _pluginsManifests)
- {
- foreach (var file in plugin.DllFiles)
- {
- Assembly plugAss;
- try
- {
- plugAss = Assembly.LoadFrom(file);
- }
- catch (FileLoadException ex)
- {
- Logger.LogError(ex, "Failed to load assembly {Path}", file);
- continue;
- }
-
- Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
- yield return plugAss;
- }
- }
+ yield return p;
}
// Include composable parts in the Model assembly
@@ -1369,17 +1252,6 @@ namespace Emby.Server.Implementations
}
}
- /// <summary>
- /// Removes the plugin.
- /// </summary>
- /// <param name="plugin">The plugin.</param>
- public void RemovePlugin(IPlugin plugin)
- {
- var list = _plugins.ToList();
- list.Remove(plugin);
- _plugins = list.ToArray();
- }
-
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 91c4648c6..dd6022982 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -73,6 +73,12 @@
<EmbeddedResource Include="Localization\countries.json" />
<EmbeddedResource Include="Localization\Core\*.json" />
<EmbeddedResource Include="Localization\Ratings\*.csv" />
+ <EmbeddedResource Include="Plugins\blank.png" />
+ <EmbeddedResource Include="Plugins\Superceded.png" />
+ <EmbeddedResource Include="Plugins\Disabled.png" />
+ <EmbeddedResource Include="Plugins\NotSupported.png" />
+ <EmbeddedResource Include="Plugins\Malfunction.png" />
+ <EmbeddedResource Include="Plugins\RestartRequired.png" />
+ <EmbeddedResource Include="Plugins\Active.png" />
</ItemGroup>
-
</Project>
diff --git a/Emby.Server.Implementations/Plugins/Active.png b/Emby.Server.Implementations/Plugins/Active.png
new file mode 100644
index 000000000..3722ee520
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/Active.png
Binary files differ
diff --git a/Emby.Server.Implementations/Plugins/Malfunction.png b/Emby.Server.Implementations/Plugins/Malfunction.png
new file mode 100644
index 000000000..d4726150e
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/Malfunction.png
Binary files differ
diff --git a/Emby.Server.Implementations/Plugins/NotSupported.png b/Emby.Server.Implementations/Plugins/NotSupported.png
new file mode 100644
index 000000000..a13c1f7c1
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/NotSupported.png
Binary files differ
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
new file mode 100644
index 000000000..8596dfcf8
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -0,0 +1,674 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Json;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Plugins;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations
+{
+ /// <summary>
+ /// Defines the <see cref="PluginManager" />.
+ /// </summary>
+ public class PluginManager : IPluginManager
+ {
+ private const int OffsetFromTopRightCorner = 38;
+
+ private readonly string _pluginsPath;
+ private readonly Version _appVersion;
+ private readonly JsonSerializerOptions _jsonOptions;
+ private readonly ILogger<PluginManager> _logger;
+ private readonly IApplicationHost _appHost;
+ private readonly string _imagesPath;
+ private readonly ServerConfiguration _config;
+ private readonly IList<LocalPlugin> _plugins;
+ private readonly Version _nextVersion;
+ private readonly Version _minimumVersion;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginManager"/> class.
+ /// </summary>
+ /// <param name="loggerfactory">The <see cref="ILoggerFactory"/>.</param>
+ /// <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="imagesPath">The image cache path.</param>
+ /// <param name="appVersion">The application version.</param>
+ public PluginManager(
+ ILoggerFactory loggerfactory,
+ IApplicationHost appHost,
+ ServerConfiguration config,
+ string pluginsPath,
+ string imagesPath,
+ Version appVersion)
+ {
+ _logger = loggerfactory.CreateLogger<PluginManager>();
+ _pluginsPath = pluginsPath;
+ _appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
+ _jsonOptions = JsonDefaults.GetOptions();
+ _jsonOptions.PropertyNameCaseInsensitive = true;
+ _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ _config = config;
+ _appHost = appHost;
+ _imagesPath = imagesPath;
+ _nextVersion = new Version(_appVersion.Major, _appVersion.Minor + 2, _appVersion.Build, _appVersion.Revision);
+ _minimumVersion = new Version(0, 0, 0, 1);
+ _plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
+ }
+
+ /// <summary>
+ /// Gets the Plugins.
+ /// </summary>
+ public IList<LocalPlugin> Plugins => _plugins;
+
+ /// <summary>
+ /// Returns all the assemblies.
+ /// </summary>
+ /// <returns>An IEnumerable{Assembly}.</returns>
+ public IEnumerable<Assembly> LoadAssemblies()
+ {
+ foreach (var plugin in _plugins)
+ {
+ foreach (var file in plugin.DllFiles)
+ {
+ try
+ {
+ plugin.Assembly = Assembly.LoadFrom(file);
+ }
+ catch (FileLoadException ex)
+ {
+ _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file);
+ ChangePluginState(plugin, PluginStatus.Malfunction);
+ continue;
+ }
+
+ _logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugin.Assembly.FullName, file);
+ yield return plugin.Assembly;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Creates all the plugin instances.
+ /// </summary>
+ public void CreatePlugins()
+ {
+ var createdPlugins = _appHost.GetExports<IPlugin>(CreatePluginInstance)
+ .Where(i => i != null)
+ .ToArray();
+ }
+
+ /// <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>())
+ {
+ var plugin = GetPluginByType(pluginServiceRegistrator.Assembly.GetType());
+ if (plugin == null)
+ {
+ throw new NullReferenceException();
+ }
+
+ CheckIfStillSuperceded(plugin);
+ 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);
+ if (ChangePluginState(plugin, PluginStatus.Malfunction))
+ {
+ _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.
+ if (plugin == null || _plugins.Any(p => p.Manifest.Equals(plugin.Manifest)))
+ {
+ 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));
+ }
+
+ plugin.Instance?.OnUninstalling();
+
+ if (DeletePlugin(plugin))
+ {
+ return true;
+ }
+
+ // Unable to delete, so disable.
+ return ChangePluginState(plugin, PluginStatus.Disabled);
+ }
+
+ /// <summary>
+ /// Attempts to find the plugin with and id of <paramref name="id"/>.
+ /// </summary>
+ /// <param name="id">Id of plugin.</param>
+ /// <param name="version">The version of the plugin to locate.</param>
+ /// <param name="plugin">A <see cref="LocalPlugin"/> if found, otherwise null.</param>
+ /// <returns>Boolean value signifying the success of the search.</returns>
+ public bool TryGetPlugin(Guid id, Version? version, out LocalPlugin? plugin)
+ {
+ if (version == null)
+ {
+ // If no version is given, return the largest version number. (This is for backwards compatibility).
+ plugin = _plugins.Where(p => p.Id.Equals(id)).OrderByDescending(p => p.Version).FirstOrDefault();
+ }
+ else
+ {
+ plugin = _plugins.FirstOrDefault(p => p.Id.Equals(id) && p.Version.Equals(version));
+ }
+
+ return plugin != null;
+ }
+
+ /// <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))
+ {
+ UpdateSuccessors(plugin);
+ }
+ }
+
+ /// <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))
+ {
+ UpdateSuccessors(plugin);
+ }
+ }
+
+ /// <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>
+ private void UpdateSuccessors(LocalPlugin plugin)
+ {
+ // This value is memory only - so that the web will show restart required.
+ plugin.Manifest.Status = PluginStatus.RestartRequired;
+
+ // Detect whether there is another version of this plugin that needs disabling.
+ var predecessor = _plugins.OrderByDescending(p => p.Version)
+ .FirstOrDefault(
+ p => p.Id.Equals(plugin.Id)
+ && p.Name.Equals(plugin.Name, StringComparison.OrdinalIgnoreCase)
+ && p.IsEnabledAndSupported
+ && p.Version != plugin.Version);
+
+ if (predecessor == null)
+ {
+ return;
+ }
+
+ if (!ChangePluginState(predecessor, PluginStatus.Superceded))
+ {
+ _logger.LogError("Unable to disable version {Version} of {Name}", predecessor.Version, predecessor.Name);
+ }
+ }
+
+ /// <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));
+ }
+
+ var plugin = _plugins.Where(p => assembly.Equals(p.Assembly)).FirstOrDefault();
+ if (plugin == null)
+ {
+ // A plugin's assembly didn't cause this issue, so ignore it.
+ return;
+ }
+
+ ChangePluginState(plugin, PluginStatus.Malfunction);
+ }
+
+ /// <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>
+ 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);
+ 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
+ {
+ _logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
+ return false;
+ }
+ }
+
+ /// <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;
+ SaveManifest(plugin.Manifest, plugin.Path);
+ try
+ {
+ var data = JsonSerializer.Serialize(plugin.Manifest, _jsonOptions);
+ File.WriteAllText(Path.Combine(plugin.Path, "meta.json"), data, Encoding.UTF8);
+ 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
+ {
+ _logger.LogWarning(e, "Unable to disable plugin {Path}", plugin.Path);
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Finds the plugin record using the type.
+ /// </summary>
+ /// <param name="type">The <see cref="Type"/> being sought.</param>
+ /// <returns>The matching record, or null if not found.</returns>
+ private LocalPlugin? GetPluginByType(Type type)
+ {
+ // Find which plugin it is by the path.
+ return _plugins.FirstOrDefault(p => string.Equals(p.Path, Path.GetDirectoryName(type.Assembly.Location), StringComparison.Ordinal));
+ }
+
+ /// <summary>
+ /// Creates the instance safe.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns>System.Object.</returns>
+ private object? CreatePluginInstance(Type type)
+ {
+ // Find the record for this plugin.
+ var plugin = GetPluginByType(type);
+
+ if (plugin != null)
+ {
+ CheckIfStillSuperceded(plugin);
+
+ if (plugin.IsEnabledAndSupported == true)
+ {
+ _logger.LogInformation("Skipping disabled plugin {Version} of {Name} ", plugin.Version, plugin.Name);
+ return null;
+ }
+ }
+
+ try
+ {
+ _logger.LogDebug("Creating instance of {Type}", type);
+ var instance = ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type);
+ if (plugin == null)
+ {
+ // Create a dummy record for the providers.
+ var pInstance = (IPlugin)instance;
+ plugin = new LocalPlugin(
+ pInstance.AssemblyFilePath,
+ true,
+ new PluginManifest
+ {
+ Guid = pInstance.Id,
+ Status = PluginStatus.Active,
+ Name = pInstance.Name,
+ Version = pInstance.Version.ToString(),
+ MaxAbi = _nextVersion.ToString()
+ })
+ {
+ Instance = pInstance
+ };
+
+ _plugins.Add(plugin);
+
+ plugin.Manifest.Status = PluginStatus.Active;
+ }
+ else
+ {
+ plugin.Instance = (IPlugin)instance;
+ var manifest = plugin.Manifest;
+ var pluginStr = plugin.Instance.Version.ToString();
+ if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal))
+ {
+ // 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.Status = PluginStatus.Active;
+ SaveManifest(manifest, plugin.Path);
+ }
+
+ _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)
+ {
+ if (ChangePluginState(plugin, PluginStatus.Malfunction))
+ {
+ _logger.LogInformation("Plugin {Path} has been disabled.", plugin.Path);
+ return null;
+ }
+ }
+
+ _logger.LogDebug("Unable to auto-disable.");
+ return null;
+ }
+ }
+
+ private void CheckIfStillSuperceded(LocalPlugin plugin)
+ {
+ 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
+ {
+ _logger.LogDebug("Deleting {Path}", plugin.Path);
+ Directory.Delete(plugin.Path, 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}", plugin.Path);
+ return false;
+ }
+
+ return _plugins.Remove(plugin);
+ }
+
+ private LocalPlugin? LoadManifest(string dir)
+ {
+ try
+ {
+ Version? version;
+ PluginManifest? manifest = null;
+ var metafile = Path.Combine(dir, "meta.json");
+ if (File.Exists(metafile))
+ {
+ try
+ {
+ var data = File.ReadAllText(metafile, Encoding.UTF8);
+ 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
+ {
+ _logger.LogError(ex, "Error deserializing {Path}.", dir);
+ }
+ }
+
+ if (manifest != null)
+ {
+ if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+ {
+ targetAbi = _minimumVersion;
+ }
+
+ if (!Version.TryParse(manifest.MaxAbi, out var maxAbi))
+ {
+ maxAbi = _appVersion;
+ }
+
+ if (!Version.TryParse(manifest.Version, out version))
+ {
+ manifest.Version = _minimumVersion.ToString();
+ }
+
+ return new LocalPlugin(dir, _appVersion >= targetAbi && _appVersion <= maxAbi, manifest);
+ }
+
+ // 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.
+ metafile = Path.GetFileName(dir[..versionIndex]) ?? dir[..versionIndex];
+ version = Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version? parsedVersion) ? parsedVersion : _appVersion;
+ }
+ else
+ {
+ // Un-versioned folder - Add it under the path name and version it suitable for this instance.
+ version = _appVersion;
+ }
+
+ // Auto-create a plugin manifest, so we can disable it, if it fails to load.
+ // NOTE: This Plugin is marked as valid for two upgrades, at which point, it can be assumed the
+ // code base will have changed sufficiently to make it invalid.
+ manifest = new PluginManifest
+ {
+ Status = PluginStatus.RestartRequired,
+ Name = metafile,
+ AutoUpdate = false,
+ Guid = metafile.GetMD5(),
+ TargetAbi = _appVersion.ToString(),
+ MaxAbi = _nextVersion.ToString(),
+ Version = version.ToString()
+ };
+
+ return new LocalPlugin(dir, true, manifest);
+ }
+#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, "Something went wrong!");
+ return null;
+ }
+ }
+
+ /// <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);
+ LocalPlugin? entry;
+ foreach (var dir in directories)
+ {
+ entry = LoadManifest(dir);
+ if (entry != null)
+ {
+ versions.Add(entry);
+ }
+ }
+
+ 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--)
+ {
+ entry = versions[x];
+ if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories));
+ if (entry.IsEnabledAndSupported)
+ {
+ lastName = entry.Name;
+ continue;
+ }
+ }
+
+ if (string.IsNullOrEmpty(lastName))
+ {
+ continue;
+ }
+
+ var manifest = entry.Manifest;
+ 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);
+ }
+
+ versions.RemoveAt(x);
+ }
+
+ if (!cleaned)
+ {
+ if (manifest == null)
+ {
+ _logger.LogWarning("Unable to disable plugin {Path}", entry.Path);
+ continue;
+ }
+
+ // Update the manifest so its not loaded next time.
+ manifest.Status = PluginStatus.Disabled;
+ SaveManifest(manifest, entry.Path);
+ }
+ }
+
+ // Only want plugin folders which have files.
+ return versions.Where(p => p.DllFiles.Count != 0);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Plugins/PluginManifest.cs b/Emby.Server.Implementations/Plugins/PluginManifest.cs
deleted file mode 100644
index 33762791b..000000000
--- a/Emby.Server.Implementations/Plugins/PluginManifest.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System;
-
-namespace Emby.Server.Implementations.Plugins
-{
- /// <summary>
- /// Defines a Plugin manifest file.
- /// </summary>
- public class PluginManifest
- {
- /// <summary>
- /// Gets or sets the category of the plugin.
- /// </summary>
- public string Category { get; set; }
-
- /// <summary>
- /// Gets or sets the changelog information.
- /// </summary>
- public string Changelog { get; set; }
-
- /// <summary>
- /// Gets or sets the description of the plugin.
- /// </summary>
- public string Description { get; set; }
-
- /// <summary>
- /// Gets or sets the Global Unique Identifier for the plugin.
- /// </summary>
- public Guid Guid { get; set; }
-
- /// <summary>
- /// Gets or sets the Name of the plugin.
- /// </summary>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets an overview of the plugin.
- /// </summary>
- public string Overview { get; set; }
-
- /// <summary>
- /// Gets or sets the owner of the plugin.
- /// </summary>
- public string Owner { get; set; }
-
- /// <summary>
- /// Gets or sets the compatibility version for the plugin.
- /// </summary>
- public string TargetAbi { get; set; }
-
- /// <summary>
- /// Gets or sets the timestamp of the plugin.
- /// </summary>
- public DateTime Timestamp { get; set; }
-
- /// <summary>
- /// Gets or sets the Version number of the plugin.
- /// </summary>
- public string Version { get; set; }
- }
-}
diff --git a/Emby.Server.Implementations/Plugins/RestartRequired.png b/Emby.Server.Implementations/Plugins/RestartRequired.png
new file mode 100644
index 000000000..65fd102a2
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/RestartRequired.png
Binary files differ
diff --git a/Emby.Server.Implementations/Plugins/Superceded.png b/Emby.Server.Implementations/Plugins/Superceded.png
new file mode 100644
index 000000000..251e70535
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/Superceded.png
Binary files differ
diff --git a/Emby.Server.Implementations/Plugins/blank.png b/Emby.Server.Implementations/Plugins/blank.png
new file mode 100644
index 000000000..f81ae3243
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/blank.png
Binary files differ
diff --git a/Emby.Server.Implementations/Plugins/disabled.png b/Emby.Server.Implementations/Plugins/disabled.png
new file mode 100644
index 000000000..eeb8ffefc
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/disabled.png
Binary files differ
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
index 161fa0580..a69380cbb 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
@@ -8,10 +8,10 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Updates;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks
{
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index ef346dd5d..b0a1750bd 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#nullable enable
using System;
using System.Collections.Concurrent;
@@ -12,7 +12,6 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
-using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
@@ -41,17 +40,15 @@ namespace Emby.Server.Implementations.Updates
private readonly IEventManager _eventManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
- private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _jsonSerializerOptions;
+ private readonly IPluginManager _pluginManager;
/// <summary>
/// Gets the application host.
/// </summary>
/// <value>The application host.</value>
private readonly IServerApplicationHost _applicationHost;
-
private readonly IZipClient _zipClient;
-
private readonly object _currentInstallationsLock = new object();
/// <summary>
@@ -64,6 +61,17 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InstallationManager"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger{InstallationManager}"/>.</param>
+ /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
+ /// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
+ /// <param name="eventManager">The <see cref="IEventManager"/>.</param>
+ /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
+ /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="zipClient">The <see cref="IZipClient"/>.</param>
+ /// <param name="pluginManager">The <see cref="IPluginManager"/>.</param>
public InstallationManager(
ILogger<InstallationManager> logger,
IServerApplicationHost appHost,
@@ -71,8 +79,8 @@ namespace Emby.Server.Implementations.Updates
IEventManager eventManager,
IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
- IFileSystem fileSystem,
- IZipClient zipClient)
+ IZipClient zipClient,
+ IPluginManager pluginManager)
{
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
@@ -83,16 +91,17 @@ namespace Emby.Server.Implementations.Updates
_eventManager = eventManager;
_httpClientFactory = httpClientFactory;
_config = config;
- _fileSystem = fileSystem;
_zipClient = zipClient;
_jsonSerializerOptions = JsonDefaults.GetOptions();
+ _jsonSerializerOptions.PropertyNameCaseInsensitive = true;
+ _pluginManager = pluginManager;
}
/// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc />
- public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default)
+ public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
{
try
{
@@ -103,13 +112,39 @@ namespace Emby.Server.Implementations.Updates
return Array.Empty<PackageInfo>();
}
+ var minimumVersion = new Version(0, 0, 0, 1);
// Store the repository and repository url with each version, as they may be spread apart.
foreach (var entry in packages)
{
- foreach (var ver in entry.versions)
+ for (int a = entry.Versions.Count - 1; a >= 0; a--)
{
- ver.repositoryName = manifestName;
- ver.repositoryUrl = manifest;
+ var ver = entry.Versions[a];
+ ver.RepositoryName = manifestName;
+ ver.RepositoryUrl = manifest;
+
+ if (!filterIncompatible)
+ {
+ continue;
+ }
+
+ if (!Version.TryParse(ver.TargetAbi, out var targetAbi))
+ {
+ targetAbi = minimumVersion;
+ }
+
+ if (!Version.TryParse(ver.MaxAbi, out var maxAbi))
+ {
+ maxAbi = _applicationHost.ApplicationVersion;
+ }
+
+ // Only show plugins that fall between targetAbi and maxAbi
+ if (_applicationHost.ApplicationVersion >= targetAbi && _applicationHost.ApplicationVersion <= maxAbi)
+ {
+ continue;
+ }
+
+ // Not compatible with this version so remove it.
+ entry.Versions.Remove(ver);
}
}
@@ -132,69 +167,61 @@ namespace Emby.Server.Implementations.Updates
}
}
- private static void MergeSort(IList<VersionInfo> source, IList<VersionInfo> dest)
- {
- int sLength = source.Count - 1;
- int dLength = dest.Count;
- int s = 0, d = 0;
- var sourceVersion = source[0].VersionNumber;
- var destVersion = dest[0].VersionNumber;
-
- while (d < dLength)
- {
- if (sourceVersion.CompareTo(destVersion) >= 0)
- {
- if (s < sLength)
- {
- sourceVersion = source[++s].VersionNumber;
- }
- else
- {
- // Append all of destination to the end of source.
- while (d < dLength)
- {
- source.Add(dest[d++]);
- }
-
- break;
- }
- }
- else
- {
- source.Insert(s++, dest[d++]);
- if (d >= dLength)
- {
- break;
- }
-
- sLength++;
- destVersion = dest[d].VersionNumber;
- }
- }
- }
-
/// <inheritdoc />
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
{
var result = new List<PackageInfo>();
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
{
- if (repository.Enabled)
+ if (repository.Enabled && repository.Url != null)
{
- // Where repositories have the same content, the details of the first is taken.
- foreach (var package in await GetPackages(repository.Name, repository.Url, cancellationToken).ConfigureAwait(true))
+ // Where repositories have the same content, the details from the first is taken.
+ foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true))
{
- if (!Guid.TryParse(package.guid, out var packageGuid))
+ if (!Guid.TryParse(package.Guid, out var packageGuid))
{
// Package doesn't have a valid GUID, skip.
continue;
}
- var existing = FilterPackages(result, package.name, packageGuid).FirstOrDefault();
+ var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault();
+
+ // Remove invalid versions from the valid package.
+ for (var i = package.Versions.Count - 1; i >= 0; i--)
+ {
+ var version = package.Versions[i];
+
+ // Update the manifests, if anything changes.
+ if (_pluginManager.TryGetPlugin(packageGuid, version.VersionNumber, out LocalPlugin? plugin))
+ {
+ bool noChange = string.Equals(plugin!.Manifest.MaxAbi, version.MaxAbi, StringComparison.Ordinal)
+ || string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal);
+ if (!noChange)
+ {
+ plugin.Manifest.MaxAbi = version.MaxAbi ?? string.Empty;
+ plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
+ _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
+ }
+ }
+
+ // Remove versions with a target abi that is greater then the current application version.
+ if ((Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
+ || (Version.TryParse(version.MaxAbi, out var maxAbi) && _applicationHost.ApplicationVersion > maxAbi))
+ {
+ package.Versions.RemoveAt(i);
+ }
+ }
+
+ // Don't add a package that doesn't have any compatible versions.
+ if (package.Versions.Count == 0)
+ {
+ continue;
+ }
+
if (existing != null)
{
// Assumption is both lists are ordered, so slot these into the correct place.
- MergeSort(existing.versions, package.versions);
+ MergeSortedList(existing.Versions, package.Versions);
}
else
{
@@ -210,23 +237,23 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc />
public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
- string name = null,
- Guid guid = default,
- Version specificVersion = null)
+ string? name = null,
+ Guid? guid = default,
+ Version? specificVersion = null)
{
if (name != null)
{
- availablePackages = availablePackages.Where(x => x.name.Equals(name, StringComparison.OrdinalIgnoreCase));
+ availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
if (guid != Guid.Empty)
{
- availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid);
+ availablePackages = availablePackages.Where(x => Guid.Parse(x.Guid) == guid);
}
if (specificVersion != null)
{
- availablePackages = availablePackages.Where(x => x.versions.Where(y => y.VersionNumber.Equals(specificVersion)).Any());
+ availablePackages = availablePackages.Where(x => x.Versions.Any(y => y.VersionNumber.Equals(specificVersion)));
}
return availablePackages;
@@ -235,10 +262,10 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc />
public IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
- string name = null,
- Guid guid = default,
- Version minVersion = null,
- Version specificVersion = null)
+ string? name = null,
+ Guid? guid = default,
+ Version? minVersion = null,
+ Version? specificVersion = null)
{
var package = FilterPackages(availablePackages, name, guid, specificVersion).FirstOrDefault();
@@ -249,8 +276,9 @@ namespace Emby.Server.Implementations.Updates
}
var appVer = _applicationHost.ApplicationVersion;
- var availableVersions = package.versions
- .Where(x => Version.Parse(x.targetAbi) <= appVer);
+ var availableVersions = package.Versions
+ .Where(x => (string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer)
+ && (string.IsNullOrEmpty(x.MaxAbi) || Version.Parse(x.MaxAbi) >= appVer));
if (specificVersion != null)
{
@@ -265,12 +293,12 @@ namespace Emby.Server.Implementations.Updates
{
yield return new InstallationInfo
{
- Changelog = v.changelog,
- Guid = new Guid(package.guid),
- Name = package.name,
+ Changelog = v.Changelog,
+ Guid = new Guid(package.Guid),
+ Name = package.Name,
Version = v.VersionNumber,
- SourceUrl = v.sourceUrl,
- Checksum = v.checksum
+ SourceUrl = v.SourceUrl,
+ Checksum = v.Checksum
};
}
}
@@ -282,20 +310,6 @@ namespace Emby.Server.Implementations.Updates
return GetAvailablePluginUpdates(catalog);
}
- private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
- {
- var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath);
- foreach (var plugin in plugins)
- {
- var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
- var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
- if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid))
- {
- yield return version;
- }
- }
- }
-
/// <inheritdoc />
public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
{
@@ -373,24 +387,140 @@ namespace Emby.Server.Implementations.Updates
}
/// <summary>
- /// Installs the package internal.
+ /// Uninstalls a plugin.
/// </summary>
- /// <param name="package">The package.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns><see cref="Task" />.</returns>
- private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
+ /// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param>
+ public void UninstallPlugin(LocalPlugin plugin)
{
- // Set last update time if we were installed before
- IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
- ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase));
+ if (plugin == null)
+ {
+ return;
+ }
- // Do the install
- await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
+ if (plugin.Instance?.CanUninstall == false)
+ {
+ _logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
+ return;
+ }
- // Do plugin-specific processing
- _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
+ plugin.Instance?.OnUninstalling();
- return plugin != null;
+ // Remove it the quick way for now
+ _pluginManager.RemovePlugin(plugin);
+
+ _eventManager.Publish(new PluginUninstalledEventArgs(plugin.GetPluginInfo()));
+
+ _applicationHost.NotifyPendingRestart();
+ }
+
+ /// <inheritdoc/>
+ public bool CancelInstallation(Guid id)
+ {
+ lock (_currentInstallationsLock)
+ {
+ var install = _currentInstallations.Find(x => x.info.Guid == id);
+ if (install == default((InstallationInfo, CancellationTokenSource)))
+ {
+ return false;
+ }
+
+ install.token.Cancel();
+ _currentInstallations.Remove(install);
+ return true;
+ }
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and optionally managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources or <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ lock (_currentInstallationsLock)
+ {
+ foreach (var (info, token) in _currentInstallations)
+ {
+ token.Dispose();
+ }
+
+ _currentInstallations.Clear();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Merges two sorted lists.
+ /// </summary>
+ /// <param name="source">The source <see cref="IList{VersionInfo}"/> instance to merge.</param>
+ /// <param name="dest">The destination <see cref="IList{VersionInfo}"/> instance to merge with.</param>
+ private static void MergeSortedList(IList<VersionInfo> source, IList<VersionInfo> dest)
+ {
+ int sLength = source.Count - 1;
+ int dLength = dest.Count;
+ int s = 0, d = 0;
+ var sourceVersion = source[0].VersionNumber;
+ var destVersion = dest[0].VersionNumber;
+
+ while (d < dLength)
+ {
+ if (sourceVersion.CompareTo(destVersion) >= 0)
+ {
+ if (s < sLength)
+ {
+ sourceVersion = source[++s].VersionNumber;
+ }
+ else
+ {
+ // Append all of destination to the end of source.
+ while (d < dLength)
+ {
+ source.Add(dest[d++]);
+ }
+
+ break;
+ }
+ }
+ else
+ {
+ source.Insert(s++, dest[d++]);
+ if (d >= dLength)
+ {
+ break;
+ }
+
+ sLength++;
+ destVersion = dest[d].VersionNumber;
+ }
+ }
+ }
+
+ private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
+ {
+ var plugins = _pluginManager.Plugins;
+ foreach (var plugin in plugins)
+ {
+ if (plugin.Manifest?.AutoUpdate == false)
+ {
+ continue;
+ }
+
+ var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
+ var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
+
+ if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid))
+ {
+ yield return version;
+ }
+ }
}
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
@@ -434,7 +564,9 @@ namespace Emby.Server.Implementations.Updates
{
Directory.Delete(targetDir, true);
}
+#pragma warning disable CA1031 // Do not catch general exception types
catch
+#pragma warning restore CA1031 // Do not catch general exception types
{
// Ignore any exceptions.
}
@@ -442,119 +574,27 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
-
-#pragma warning restore CA5351
+ _pluginManager.ImportPluginFrom(targetDir);
}
- /// <summary>
- /// Uninstalls a plugin.
- /// </summary>
- /// <param name="plugin">The plugin.</param>
- public void UninstallPlugin(IPlugin plugin)
- {
- if (!plugin.CanUninstall)
- {
- _logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
- return;
- }
-
- plugin.OnUninstalling();
-
- // Remove it the quick way for now
- _applicationHost.RemovePlugin(plugin);
-
- var path = plugin.AssemblyFilePath;
- bool isDirectory = false;
- // Check if we have a plugin directory we should remove too
- if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath)
- {
- path = Path.GetDirectoryName(plugin.AssemblyFilePath);
- isDirectory = true;
- }
-
- // Make this case-insensitive to account for possible incorrect assembly naming
- var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
- .FirstOrDefault(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase));
-
- if (!string.IsNullOrWhiteSpace(file))
- {
- path = file;
- }
-
- try
- {
- if (isDirectory)
- {
- _logger.LogInformation("Deleting plugin directory {0}", path);
- Directory.Delete(path, true);
- }
- else
- {
- _logger.LogInformation("Deleting plugin file {0}", path);
- _fileSystem.DeleteFile(path);
- }
- }
- catch
- {
- // Ignore file errors.
- }
-
- var list = _config.Configuration.UninstalledPlugins.ToList();
- var filename = Path.GetFileName(path);
- if (!list.Contains(filename, StringComparer.OrdinalIgnoreCase))
- {
- list.Add(filename);
- _config.Configuration.UninstalledPlugins = list.ToArray();
- _config.SaveConfiguration();
- }
-
- _eventManager.Publish(new PluginUninstalledEventArgs(plugin));
-
- _applicationHost.NotifyPendingRestart();
- }
-
- /// <inheritdoc/>
- public bool CancelInstallation(Guid id)
+ private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
- lock (_currentInstallationsLock)
+ // Set last update time if we were installed before
+ LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Guid) && p.Version.Equals(package.Version))
+ ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
+ if (plugin != null)
{
- var install = _currentInstallations.Find(x => x.info.Guid == id);
- if (install == default((InstallationInfo, CancellationTokenSource)))
- {
- return false;
- }
-
- install.token.Cancel();
- _currentInstallations.Remove(install);
- return true;
+ plugin.Manifest.Timestamp = DateTime.UtcNow;
+ _pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
}
- }
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
+ // Do the install
+ await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
- /// <summary>
- /// Releases unmanaged and optionally managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources or <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (dispose)
- {
- lock (_currentInstallationsLock)
- {
- foreach (var tuple in _currentInstallations)
- {
- tuple.token.Dispose();
- }
+ // Do plugin-specific processing
+ _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
- _currentInstallations.Clear();
- }
- }
+ return plugin != null;
}
}
}