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.cs119
1 files changed, 87 insertions, 32 deletions
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index f699c99d85..f7ad9b9498 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -255,6 +255,14 @@ namespace Emby.Server.Implementations.Plugins
}
_plugins.Add(plugin);
+
+ // Updating a disabled plugin must not enable it again.
+ if (plugin.Manifest.Status == PluginStatus.Disabled)
+ {
+ ProcessAlternative(plugin);
+ return;
+ }
+
EnablePlugin(plugin);
}
@@ -387,29 +395,11 @@ namespace Emby.Server.Implementations.Plugins
var url = new Uri(packageInfo.ImageUrl);
imagePath = Path.Join(path, url.Segments[^1]);
- var fileStream = AsyncFile.OpenWrite(imagePath);
- Stream? downloadStream = null;
- try
- {
- downloadStream = await HttpClientFactory
- .CreateClient(NamedClient.Default)
- .GetStreamAsync(url)
- .ConfigureAwait(false);
-
- await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
- }
- catch (HttpRequestException ex)
+ // The catalog is refreshed on every dashboard visit and rewrites the manifest of
+ // every installed plugin, so only fetch an image that is actually missing.
+ if (!ImageExists(imagePath))
{
- _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
- imagePath = string.Empty;
- }
- finally
- {
- await fileStream.DisposeAsync().ConfigureAwait(false);
- if (downloadStream is not null)
- {
- await downloadStream.DisposeAsync().ConfigureAwait(false);
- }
+ imagePath = await DownloadImage(url, imagePath).ConfigureAwait(false);
}
}
@@ -448,6 +438,67 @@ namespace Emby.Server.Implementations.Plugins
}
}
+ private static bool ImageExists(string imagePath)
+ {
+ var image = new FileInfo(imagePath);
+
+ // A previous download may have been interrupted, leaving an empty file behind.
+ return image.Exists && image.Length > 0;
+ }
+
+ private async Task<string> DownloadImage(Uri url, string imagePath)
+ {
+ // Download to a temporary file and move it into place, so that neither a failed download
+ // nor a concurrent one can be observed as a partially written image.
+ var tempPath = imagePath + "." + Path.GetRandomFileName();
+
+ try
+ {
+ var fileStream = AsyncFile.Create(tempPath);
+ Stream? downloadStream = null;
+ try
+ {
+ downloadStream = await HttpClientFactory
+ .CreateClient(NamedClient.Default)
+ .GetStreamAsync(url)
+ .ConfigureAwait(false);
+
+ await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
+ }
+ finally
+ {
+ await fileStream.DisposeAsync().ConfigureAwait(false);
+ if (downloadStream is not null)
+ {
+ await downloadStream.DisposeAsync().ConfigureAwait(false);
+ }
+ }
+
+ File.Move(tempPath, imagePath, true);
+
+ return imagePath;
+ }
+ catch (Exception ex) when (ex is HttpRequestException or IOException or UnauthorizedAccessException)
+ {
+ _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
+ TryDeleteFile(tempPath);
+
+ return string.Empty;
+ }
+ }
+
+ private void TryDeleteFile(string path)
+ {
+ try
+ {
+ File.Delete(path);
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ _logger.LogWarning(ex, "Unable to delete {Path}.", path);
+ }
+ }
+
/// <summary>
/// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
/// If no file is found, no reconciliation occurs.
@@ -632,9 +683,10 @@ namespace Emby.Server.Implementations.Plugins
return;
}
- var predecessor = _plugins.OrderByDescending(p => p.Version)
- .FirstOrDefault(p => p.Id.Equals(plugin.Id) && p.IsEnabledAndSupported && p.Version != plugin.Version);
- if (predecessor is not null)
+ var successor = _plugins.FirstOrDefault(p => p.Id.Equals(plugin.Id)
+ && p.Version > plugin.Version
+ && (p.IsEnabledAndSupported || p.Manifest.Status == PluginStatus.Disabled));
+ if (successor is not null)
{
return;
}
@@ -763,6 +815,8 @@ namespace Emby.Server.Implementations.Plugins
var entry = versions[x];
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
{
+ lastName = string.Empty;
+
if (!TryGetPluginDlls(entry, out var allowedDlls))
{
_logger.LogError("One or more assembly paths was invalid. Marking plugin {Plugin} as \"Malfunctioned\".", entry.Name);
@@ -772,15 +826,18 @@ namespace Emby.Server.Implementations.Plugins
entry.DllFiles = allowedDlls;
+ // Only clean up older versions when this version will actually be loaded.
if (entry.IsEnabledAndSupported)
{
lastName = entry.Name;
- continue;
}
+
+ continue;
}
if (string.IsNullOrEmpty(lastName))
{
+ // Unnamed plugin, so there is nothing to match older versions against.
continue;
}
@@ -891,9 +948,9 @@ namespace Emby.Server.Implementations.Plugins
if (previousVersion is null)
{
- // This value is memory only - so that the web will show restart required.
- plugin.Manifest.Status = PluginStatus.Restart;
- plugin.Manifest.AutoUpdate = false;
+ // Memory only, so that the web will show restart required. The manifest must keep
+ // holding the persisted state, or a later save would write the wrong state to disk.
+ plugin.RestartRequired = true;
return;
}
@@ -906,9 +963,7 @@ namespace Emby.Server.Implementations.Plugins
_logger.LogError("Unable to supercede version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
}
- // This value is memory only - so that the web will show restart required.
- plugin.Manifest.Status = PluginStatus.Restart;
- plugin.Manifest.AutoUpdate = false;
+ plugin.RestartRequired = true;
}
}
}