| | | 1 | | using System; |
| | | 2 | | using System.Collections.Generic; |
| | | 3 | | using System.Globalization; |
| | | 4 | | using System.IO; |
| | | 5 | | using System.Linq; |
| | | 6 | | using System.Net.Http; |
| | | 7 | | using System.Reflection; |
| | | 8 | | using System.Runtime.Loader; |
| | | 9 | | using System.Text; |
| | | 10 | | using System.Text.Json; |
| | | 11 | | using System.Threading.Tasks; |
| | | 12 | | using Emby.Server.Implementations.Library; |
| | | 13 | | using Jellyfin.Extensions.Json; |
| | | 14 | | using Jellyfin.Extensions.Json.Converters; |
| | | 15 | | using MediaBrowser.Common.Extensions; |
| | | 16 | | using MediaBrowser.Common.Net; |
| | | 17 | | using MediaBrowser.Common.Plugins; |
| | | 18 | | using MediaBrowser.Controller; |
| | | 19 | | using MediaBrowser.Controller.Plugins; |
| | | 20 | | using MediaBrowser.Model.Configuration; |
| | | 21 | | using MediaBrowser.Model.IO; |
| | | 22 | | using MediaBrowser.Model.Plugins; |
| | | 23 | | using MediaBrowser.Model.Updates; |
| | | 24 | | using Microsoft.Extensions.DependencyInjection; |
| | | 25 | | using Microsoft.Extensions.Logging; |
| | | 26 | | |
| | | 27 | | namespace Emby.Server.Implementations.Plugins |
| | | 28 | | { |
| | | 29 | | /// <summary> |
| | | 30 | | /// Defines the <see cref="PluginManager" />. |
| | | 31 | | /// </summary> |
| | | 32 | | public sealed class PluginManager : IPluginManager, IDisposable |
| | | 33 | | { |
| | | 34 | | private const string MetafileName = "meta.json"; |
| | | 35 | | |
| | | 36 | | private readonly string _pluginsPath; |
| | | 37 | | private readonly Version _appVersion; |
| | | 38 | | private readonly List<AssemblyLoadContext> _assemblyLoadContexts; |
| | | 39 | | private readonly JsonSerializerOptions _jsonOptions; |
| | | 40 | | private readonly ILogger<PluginManager> _logger; |
| | | 41 | | private readonly IServerApplicationHost _appHost; |
| | | 42 | | private readonly ServerConfiguration _config; |
| | | 43 | | private readonly List<LocalPlugin> _plugins; |
| | | 44 | | private readonly Version _minimumVersion; |
| | | 45 | | |
| | | 46 | | private IHttpClientFactory? _httpClientFactory; |
| | | 47 | | |
| | | 48 | | /// <summary> |
| | | 49 | | /// Initializes a new instance of the <see cref="PluginManager"/> class. |
| | | 50 | | /// </summary> |
| | | 51 | | /// <param name="logger">The <see cref="ILogger{PluginManager}"/>.</param> |
| | | 52 | | /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param> |
| | | 53 | | /// <param name="config">The <see cref="ServerConfiguration"/>.</param> |
| | | 54 | | /// <param name="pluginsPath">The plugin path.</param> |
| | | 55 | | /// <param name="appVersion">The application version.</param> |
| | | 56 | | public PluginManager( |
| | | 57 | | ILogger<PluginManager> logger, |
| | | 58 | | IServerApplicationHost appHost, |
| | | 59 | | ServerConfiguration config, |
| | | 60 | | string pluginsPath, |
| | | 61 | | Version appVersion) |
| | | 62 | | { |
| | 38 | 63 | | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
| | 38 | 64 | | _pluginsPath = pluginsPath; |
| | 38 | 65 | | _appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion)); |
| | 38 | 66 | | _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options) |
| | 38 | 67 | | { |
| | 38 | 68 | | WriteIndented = true |
| | 38 | 69 | | }; |
| | | 70 | | |
| | | 71 | | // We need to use the default GUID converter, so we need to remove any custom ones. |
| | 684 | 72 | | for (int a = _jsonOptions.Converters.Count - 1; a >= 0; a--) |
| | | 73 | | { |
| | 342 | 74 | | if (_jsonOptions.Converters[a] is JsonGuidConverter convertor) |
| | | 75 | | { |
| | 38 | 76 | | _jsonOptions.Converters.Remove(convertor); |
| | 38 | 77 | | break; |
| | | 78 | | } |
| | | 79 | | } |
| | | 80 | | |
| | 38 | 81 | | _config = config; |
| | 38 | 82 | | _appHost = appHost; |
| | 38 | 83 | | _minimumVersion = new Version(0, 0, 0, 1); |
| | 38 | 84 | | _plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>(); |
| | | 85 | | |
| | 38 | 86 | | _assemblyLoadContexts = new List<AssemblyLoadContext>(); |
| | 38 | 87 | | } |
| | | 88 | | |
| | | 89 | | private IHttpClientFactory HttpClientFactory |
| | | 90 | | { |
| | | 91 | | get |
| | | 92 | | { |
| | 0 | 93 | | return _httpClientFactory ??= _appHost.Resolve<IHttpClientFactory>(); |
| | | 94 | | } |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | /// <summary> |
| | | 98 | | /// Gets the Plugins. |
| | | 99 | | /// </summary> |
| | 22 | 100 | | public IReadOnlyList<LocalPlugin> Plugins => _plugins; |
| | | 101 | | |
| | | 102 | | /// <summary> |
| | | 103 | | /// Returns all the assemblies. |
| | | 104 | | /// </summary> |
| | | 105 | | /// <returns>An IEnumerable{Assembly}.</returns> |
| | | 106 | | public IEnumerable<Assembly> LoadAssemblies() |
| | | 107 | | { |
| | | 108 | | // Attempt to remove any deleted plugins and change any successors to be active. |
| | | 109 | | for (int i = _plugins.Count - 1; i >= 0; i--) |
| | | 110 | | { |
| | | 111 | | var plugin = _plugins[i]; |
| | | 112 | | if (plugin.Manifest.Status == PluginStatus.Deleted && DeletePlugin(plugin)) |
| | | 113 | | { |
| | | 114 | | // See if there is another version, and if so make that active. |
| | | 115 | | ProcessAlternative(plugin); |
| | | 116 | | } |
| | | 117 | | } |
| | | 118 | | |
| | | 119 | | // Now load the assemblies.. |
| | | 120 | | foreach (var plugin in _plugins) |
| | | 121 | | { |
| | | 122 | | UpdatePluginSupersededStatus(plugin); |
| | | 123 | | |
| | | 124 | | if (plugin.IsEnabledAndSupported == false) |
| | | 125 | | { |
| | | 126 | | _logger.LogInformation("Skipping disabled plugin {Version} of {Name} ", plugin.Version, plugin.Name) |
| | | 127 | | continue; |
| | | 128 | | } |
| | | 129 | | |
| | | 130 | | var assemblyLoadContext = new PluginLoadContext(plugin.Path); |
| | | 131 | | _assemblyLoadContexts.Add(assemblyLoadContext); |
| | | 132 | | |
| | | 133 | | var assemblies = new List<Assembly>(plugin.DllFiles.Count); |
| | | 134 | | var loadedAll = true; |
| | | 135 | | |
| | | 136 | | foreach (var file in plugin.DllFiles) |
| | | 137 | | { |
| | | 138 | | try |
| | | 139 | | { |
| | | 140 | | assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file)); |
| | | 141 | | } |
| | | 142 | | catch (FileLoadException ex) |
| | | 143 | | { |
| | | 144 | | _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin", file); |
| | | 145 | | ChangePluginState(plugin, PluginStatus.Malfunctioned); |
| | | 146 | | loadedAll = false; |
| | | 147 | | break; |
| | | 148 | | } |
| | | 149 | | #pragma warning disable CA1031 // Do not catch general exception types |
| | | 150 | | catch (Exception ex) |
| | | 151 | | #pragma warning restore CA1031 // Do not catch general exception types |
| | | 152 | | { |
| | | 153 | | _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling pl |
| | | 154 | | ChangePluginState(plugin, PluginStatus.Malfunctioned); |
| | | 155 | | loadedAll = false; |
| | | 156 | | break; |
| | | 157 | | } |
| | | 158 | | } |
| | | 159 | | |
| | | 160 | | if (!loadedAll) |
| | | 161 | | { |
| | | 162 | | continue; |
| | | 163 | | } |
| | | 164 | | |
| | | 165 | | foreach (var assembly in assemblies) |
| | | 166 | | { |
| | | 167 | | try |
| | | 168 | | { |
| | | 169 | | // Load all required types to verify that the plugin will load |
| | | 170 | | assembly.GetTypes(); |
| | | 171 | | } |
| | | 172 | | catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocume |
| | | 173 | | { |
| | | 174 | | _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references |
| | | 175 | | ChangePluginState(plugin, PluginStatus.NotSupported); |
| | | 176 | | break; |
| | | 177 | | } |
| | | 178 | | #pragma warning disable CA1031 // Do not catch general exception types |
| | | 179 | | catch (Exception ex) |
| | | 180 | | #pragma warning restore CA1031 // Do not catch general exception types |
| | | 181 | | { |
| | | 182 | | _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling pl |
| | | 183 | | ChangePluginState(plugin, PluginStatus.Malfunctioned); |
| | | 184 | | break; |
| | | 185 | | } |
| | | 186 | | |
| | | 187 | | _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, assembly.Locatio |
| | | 188 | | yield return assembly; |
| | | 189 | | } |
| | | 190 | | } |
| | | 191 | | } |
| | | 192 | | |
| | | 193 | | /// <summary> |
| | | 194 | | /// Creates all the plugin instances. |
| | | 195 | | /// </summary> |
| | | 196 | | public void CreatePlugins() |
| | | 197 | | { |
| | 21 | 198 | | _ = _appHost.GetExports<IPlugin>(CreatePluginInstance); |
| | 21 | 199 | | } |
| | | 200 | | |
| | | 201 | | /// <summary> |
| | | 202 | | /// Registers the plugin's services with the DI. |
| | | 203 | | /// Note: DI is not yet instantiated yet. |
| | | 204 | | /// </summary> |
| | | 205 | | /// <param name="serviceCollection">A <see cref="ServiceCollection"/> instance.</param> |
| | | 206 | | public void RegisterServices(IServiceCollection serviceCollection) |
| | | 207 | | { |
| | 42 | 208 | | foreach (var pluginServiceRegistrator in _appHost.GetExportTypes<IPluginServiceRegistrator>()) |
| | | 209 | | { |
| | 0 | 210 | | var plugin = GetPluginByAssembly(pluginServiceRegistrator.Assembly); |
| | 0 | 211 | | if (plugin is null) |
| | | 212 | | { |
| | 0 | 213 | | _logger.LogError("Unable to find plugin in assembly {Assembly}", pluginServiceRegistrator.Assembly.F |
| | 0 | 214 | | continue; |
| | | 215 | | } |
| | | 216 | | |
| | 0 | 217 | | UpdatePluginSupersededStatus(plugin); |
| | 0 | 218 | | if (!plugin.IsEnabledAndSupported) |
| | | 219 | | { |
| | | 220 | | continue; |
| | | 221 | | } |
| | | 222 | | |
| | | 223 | | try |
| | | 224 | | { |
| | 0 | 225 | | var instance = (IPluginServiceRegistrator?)Activator.CreateInstance(pluginServiceRegistrator); |
| | 0 | 226 | | instance?.RegisterServices(serviceCollection, _appHost); |
| | 0 | 227 | | } |
| | | 228 | | #pragma warning disable CA1031 // Do not catch general exception types |
| | 0 | 229 | | catch (Exception ex) |
| | | 230 | | #pragma warning restore CA1031 // Do not catch general exception types |
| | | 231 | | { |
| | 0 | 232 | | _logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator. |
| | 0 | 233 | | if (ChangePluginState(plugin, PluginStatus.Malfunctioned)) |
| | | 234 | | { |
| | 0 | 235 | | _logger.LogInformation("Disabling plugin {Path}", plugin.Path); |
| | | 236 | | } |
| | 0 | 237 | | } |
| | | 238 | | } |
| | 21 | 239 | | } |
| | | 240 | | |
| | | 241 | | /// <summary> |
| | | 242 | | /// Imports a plugin manifest from <paramref name="folder"/>. |
| | | 243 | | /// </summary> |
| | | 244 | | /// <param name="folder">Folder of the plugin.</param> |
| | | 245 | | public void ImportPluginFrom(string folder) |
| | | 246 | | { |
| | 0 | 247 | | ArgumentException.ThrowIfNullOrEmpty(folder); |
| | | 248 | | |
| | | 249 | | // Load the plugin. |
| | 0 | 250 | | var plugin = LoadManifest(folder); |
| | | 251 | | // Make sure we haven't already loaded this. |
| | 0 | 252 | | if (_plugins.Any(p => p.Manifest.Equals(plugin.Manifest))) |
| | | 253 | | { |
| | 0 | 254 | | return; |
| | | 255 | | } |
| | | 256 | | |
| | 0 | 257 | | _plugins.Add(plugin); |
| | 0 | 258 | | EnablePlugin(plugin); |
| | 0 | 259 | | } |
| | | 260 | | |
| | | 261 | | /// <summary> |
| | | 262 | | /// Removes the plugin reference '<paramref name="plugin"/>. |
| | | 263 | | /// </summary> |
| | | 264 | | /// <param name="plugin">The plugin.</param> |
| | | 265 | | /// <returns>Outcome of the operation.</returns> |
| | | 266 | | public bool RemovePlugin(LocalPlugin plugin) |
| | | 267 | | { |
| | 0 | 268 | | ArgumentNullException.ThrowIfNull(plugin); |
| | | 269 | | |
| | 0 | 270 | | if (DeletePlugin(plugin)) |
| | | 271 | | { |
| | 0 | 272 | | ProcessAlternative(plugin); |
| | 0 | 273 | | return true; |
| | | 274 | | } |
| | | 275 | | |
| | 0 | 276 | | _logger.LogWarning("Unable to delete {Path}, so marking as deleteOnStartup.", plugin.Path); |
| | | 277 | | // Unable to delete, so disable. |
| | 0 | 278 | | if (ChangePluginState(plugin, PluginStatus.Deleted)) |
| | | 279 | | { |
| | 0 | 280 | | ProcessAlternative(plugin); |
| | 0 | 281 | | return true; |
| | | 282 | | } |
| | | 283 | | |
| | 0 | 284 | | return false; |
| | | 285 | | } |
| | | 286 | | |
| | | 287 | | /// <summary> |
| | | 288 | | /// Attempts to find the plugin with and id of <paramref name="id"/>. |
| | | 289 | | /// </summary> |
| | | 290 | | /// <param name="id">The <see cref="Guid"/> of plugin.</param> |
| | | 291 | | /// <param name="version">Optional <see cref="Version"/> of the plugin to locate.</param> |
| | | 292 | | /// <returns>A <see cref="LocalPlugin"/> if located, or null if not.</returns> |
| | | 293 | | public LocalPlugin? GetPlugin(Guid id, Version? version = null) |
| | | 294 | | { |
| | | 295 | | LocalPlugin? plugin; |
| | | 296 | | |
| | 258 | 297 | | if (version is null) |
| | | 298 | | { |
| | | 299 | | // If no version is given, return the current instance. |
| | 0 | 300 | | var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList(); |
| | | 301 | | |
| | 0 | 302 | | plugin = plugins.FirstOrDefault(p => p.Instance is not null) ?? plugins.MaxBy(p => p.Version); |
| | | 303 | | } |
| | | 304 | | else |
| | | 305 | | { |
| | | 306 | | // Match id and version number. |
| | 258 | 307 | | plugin = _plugins.FirstOrDefault(p => p.Id.Equals(id) && p.Version.Equals(version)); |
| | | 308 | | } |
| | | 309 | | |
| | 258 | 310 | | return plugin; |
| | | 311 | | } |
| | | 312 | | |
| | | 313 | | /// <summary> |
| | | 314 | | /// Enables the plugin, disabling all other versions. |
| | | 315 | | /// </summary> |
| | | 316 | | /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param> |
| | | 317 | | public void EnablePlugin(LocalPlugin plugin) |
| | | 318 | | { |
| | 0 | 319 | | ArgumentNullException.ThrowIfNull(plugin); |
| | | 320 | | |
| | 0 | 321 | | if (ChangePluginState(plugin, PluginStatus.Active)) |
| | | 322 | | { |
| | | 323 | | // See if there is another version, and if so, supercede it. |
| | 0 | 324 | | ProcessAlternative(plugin); |
| | | 325 | | } |
| | 0 | 326 | | } |
| | | 327 | | |
| | | 328 | | /// <summary> |
| | | 329 | | /// Disable the plugin. |
| | | 330 | | /// </summary> |
| | | 331 | | /// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param> |
| | | 332 | | public void DisablePlugin(LocalPlugin plugin) |
| | | 333 | | { |
| | 0 | 334 | | ArgumentNullException.ThrowIfNull(plugin); |
| | | 335 | | |
| | | 336 | | // Update the manifest on disk |
| | 0 | 337 | | if (ChangePluginState(plugin, PluginStatus.Disabled)) |
| | | 338 | | { |
| | | 339 | | // If there is another version, activate it. |
| | 0 | 340 | | ProcessAlternative(plugin); |
| | | 341 | | } |
| | 0 | 342 | | } |
| | | 343 | | |
| | | 344 | | /// <summary> |
| | | 345 | | /// Disable the plugin. |
| | | 346 | | /// </summary> |
| | | 347 | | /// <param name="assembly">The <see cref="Assembly"/> of the plug to disable.</param> |
| | | 348 | | public void FailPlugin(Assembly assembly) |
| | | 349 | | { |
| | | 350 | | // Only save if disabled. |
| | 0 | 351 | | ArgumentNullException.ThrowIfNull(assembly); |
| | | 352 | | |
| | 0 | 353 | | var plugin = _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location)); |
| | 0 | 354 | | if (plugin is null) |
| | | 355 | | { |
| | | 356 | | // A plugin's assembly didn't cause this issue, so ignore it. |
| | 0 | 357 | | return; |
| | | 358 | | } |
| | | 359 | | |
| | 0 | 360 | | ChangePluginState(plugin, PluginStatus.Malfunctioned); |
| | 0 | 361 | | } |
| | | 362 | | |
| | | 363 | | /// <inheritdoc/> |
| | | 364 | | public bool SaveManifest(PluginManifest manifest, string path) |
| | | 365 | | { |
| | | 366 | | try |
| | | 367 | | { |
| | 14 | 368 | | var data = JsonSerializer.Serialize(manifest, _jsonOptions); |
| | 14 | 369 | | File.WriteAllText(Path.Combine(path, MetafileName), data); |
| | 14 | 370 | | return true; |
| | | 371 | | } |
| | 0 | 372 | | catch (ArgumentException e) |
| | | 373 | | { |
| | 0 | 374 | | _logger.LogWarning(e, "Unable to save plugin manifest due to invalid value. {Path}", path); |
| | 0 | 375 | | return false; |
| | | 376 | | } |
| | 14 | 377 | | } |
| | | 378 | | |
| | | 379 | | /// <inheritdoc/> |
| | | 380 | | public async Task<bool> PopulateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus sta |
| | | 381 | | { |
| | | 382 | | var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString()); |
| | | 383 | | var imagePath = string.Empty; |
| | | 384 | | |
| | | 385 | | if (!string.IsNullOrEmpty(packageInfo.ImageUrl)) |
| | | 386 | | { |
| | | 387 | | var url = new Uri(packageInfo.ImageUrl); |
| | | 388 | | imagePath = Path.Join(path, url.Segments[^1]); |
| | | 389 | | |
| | | 390 | | var fileStream = AsyncFile.OpenWrite(imagePath); |
| | | 391 | | Stream? downloadStream = null; |
| | | 392 | | try |
| | | 393 | | { |
| | | 394 | | downloadStream = await HttpClientFactory |
| | | 395 | | .CreateClient(NamedClient.Default) |
| | | 396 | | .GetStreamAsync(url) |
| | | 397 | | .ConfigureAwait(false); |
| | | 398 | | |
| | | 399 | | await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false); |
| | | 400 | | } |
| | | 401 | | catch (HttpRequestException ex) |
| | | 402 | | { |
| | | 403 | | _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath); |
| | | 404 | | imagePath = string.Empty; |
| | | 405 | | } |
| | | 406 | | finally |
| | | 407 | | { |
| | | 408 | | await fileStream.DisposeAsync().ConfigureAwait(false); |
| | | 409 | | if (downloadStream is not null) |
| | | 410 | | { |
| | | 411 | | await downloadStream.DisposeAsync().ConfigureAwait(false); |
| | | 412 | | } |
| | | 413 | | } |
| | | 414 | | } |
| | | 415 | | |
| | | 416 | | var manifest = new PluginManifest |
| | | 417 | | { |
| | | 418 | | Category = packageInfo.Category, |
| | | 419 | | Changelog = versionInfo.Changelog ?? string.Empty, |
| | | 420 | | Description = packageInfo.Description, |
| | | 421 | | Id = packageInfo.Id, |
| | | 422 | | Name = packageInfo.Name, |
| | | 423 | | Overview = packageInfo.Overview, |
| | | 424 | | Owner = packageInfo.Owner, |
| | | 425 | | TargetAbi = versionInfo.TargetAbi ?? string.Empty, |
| | | 426 | | Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo |
| | | 427 | | Version = versionInfo.Version, |
| | | 428 | | Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled |
| | | 429 | | AutoUpdate = true, |
| | | 430 | | ImagePath = imagePath |
| | | 431 | | }; |
| | | 432 | | |
| | | 433 | | if (!await ReconcileManifest(manifest, path).ConfigureAwait(false)) |
| | | 434 | | { |
| | | 435 | | // An error occurred during reconciliation and saving could be undesirable. |
| | | 436 | | return false; |
| | | 437 | | } |
| | | 438 | | |
| | | 439 | | return SaveManifest(manifest, path); |
| | | 440 | | } |
| | | 441 | | |
| | | 442 | | /// <inheritdoc /> |
| | | 443 | | public void Dispose() |
| | | 444 | | { |
| | 42 | 445 | | foreach (var assemblyLoadContext in _assemblyLoadContexts) |
| | | 446 | | { |
| | 0 | 447 | | assemblyLoadContext.Unload(); |
| | | 448 | | } |
| | 21 | 449 | | } |
| | | 450 | | |
| | | 451 | | /// <summary> |
| | | 452 | | /// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the p |
| | | 453 | | /// If no file is found, no reconciliation occurs. |
| | | 454 | | /// </summary> |
| | | 455 | | /// <param name="manifest">The <see cref="PluginManifest"/> to reconcile against.</param> |
| | | 456 | | /// <param name="path">The plugin path.</param> |
| | | 457 | | /// <returns>The reconciled <see cref="PluginManifest"/>.</returns> |
| | | 458 | | private async Task<bool> ReconcileManifest(PluginManifest manifest, string path) |
| | | 459 | | { |
| | | 460 | | try |
| | | 461 | | { |
| | | 462 | | var metafile = Path.Combine(path, MetafileName); |
| | | 463 | | if (!File.Exists(metafile)) |
| | | 464 | | { |
| | | 465 | | _logger.LogInformation("No local manifest exists for plugin {Plugin}. Skipping manifest reconciliati |
| | | 466 | | return true; |
| | | 467 | | } |
| | | 468 | | |
| | | 469 | | using var metaStream = File.OpenRead(metafile); |
| | | 470 | | var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions).Conf |
| | | 471 | | localManifest ??= new PluginManifest(); |
| | | 472 | | |
| | | 473 | | if (!Equals(localManifest.Id, manifest.Id)) |
| | | 474 | | { |
| | | 475 | | _logger.LogError("The manifest ID {LocalUUID} did not match the package info ID {PackageUUID}.", loc |
| | | 476 | | manifest.Status = PluginStatus.Malfunctioned; |
| | | 477 | | } |
| | | 478 | | |
| | | 479 | | if (localManifest.Version != manifest.Version) |
| | | 480 | | { |
| | | 481 | | // Package information provides the version and is the source of truth. Pre-packages meta.json is as |
| | | 482 | | _logger.LogWarning("The version of the local manifest was {LocalVersion}, but {PackageVersion} was e |
| | | 483 | | } |
| | | 484 | | |
| | | 485 | | // Explicitly mapping properties instead of using reflection is preferred here. |
| | | 486 | | manifest.Category = string.IsNullOrEmpty(localManifest.Category) ? manifest.Category : localManifest.Cat |
| | | 487 | | manifest.AutoUpdate = localManifest.AutoUpdate; // Preserve whatever is local. Package info does not hav |
| | | 488 | | manifest.Changelog = string.IsNullOrEmpty(localManifest.Changelog) ? manifest.Changelog : localManifest. |
| | | 489 | | manifest.Description = string.IsNullOrEmpty(localManifest.Description) ? manifest.Description : localMan |
| | | 490 | | manifest.Name = string.IsNullOrEmpty(localManifest.Name) ? manifest.Name : localManifest.Name; |
| | | 491 | | manifest.Overview = string.IsNullOrEmpty(localManifest.Overview) ? manifest.Overview : localManifest.Ove |
| | | 492 | | manifest.Owner = string.IsNullOrEmpty(localManifest.Owner) ? manifest.Owner : localManifest.Owner; |
| | | 493 | | manifest.TargetAbi = string.IsNullOrEmpty(localManifest.TargetAbi) ? manifest.TargetAbi : localManifest. |
| | | 494 | | manifest.Timestamp = localManifest.Timestamp.Equals(default) ? manifest.Timestamp : localManifest.Timest |
| | | 495 | | manifest.ImagePath = string.IsNullOrEmpty(localManifest.ImagePath) ? manifest.ImagePath : localManifest. |
| | | 496 | | manifest.Assemblies = localManifest.Assemblies; |
| | | 497 | | |
| | | 498 | | return true; |
| | | 499 | | } |
| | | 500 | | catch (Exception e) |
| | | 501 | | { |
| | | 502 | | _logger.LogWarning(e, "Unable to reconcile plugin manifest due to an error. {Path}", path); |
| | | 503 | | return false; |
| | | 504 | | } |
| | | 505 | | } |
| | | 506 | | |
| | | 507 | | /// <summary> |
| | | 508 | | /// Changes a plugin's load status. |
| | | 509 | | /// </summary> |
| | | 510 | | /// <param name="plugin">The <see cref="LocalPlugin"/> instance.</param> |
| | | 511 | | /// <param name="state">The <see cref="PluginStatus"/> of the plugin.</param> |
| | | 512 | | /// <returns>Success of the task.</returns> |
| | | 513 | | private bool ChangePluginState(LocalPlugin plugin, PluginStatus state) |
| | | 514 | | { |
| | 9 | 515 | | if (plugin.Manifest.Status == state || string.IsNullOrEmpty(plugin.Path)) |
| | | 516 | | { |
| | | 517 | | // No need to save as the state hasn't changed. |
| | 0 | 518 | | return true; |
| | | 519 | | } |
| | | 520 | | |
| | 9 | 521 | | plugin.Manifest.Status = state; |
| | 9 | 522 | | return SaveManifest(plugin.Manifest, plugin.Path); |
| | | 523 | | } |
| | | 524 | | |
| | | 525 | | /// <summary> |
| | | 526 | | /// Finds the plugin record using the assembly. |
| | | 527 | | /// </summary> |
| | | 528 | | /// <param name="assembly">The <see cref="Assembly"/> being sought.</param> |
| | | 529 | | /// <returns>The matching record, or null if not found.</returns> |
| | | 530 | | private LocalPlugin? GetPluginByAssembly(Assembly assembly) |
| | | 531 | | { |
| | | 532 | | // Find which plugin it is by the path. |
| | 147 | 533 | | return _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location, StringComparer.Ordinal)); |
| | | 534 | | } |
| | | 535 | | |
| | | 536 | | /// <summary> |
| | | 537 | | /// Creates the instance safe. |
| | | 538 | | /// </summary> |
| | | 539 | | /// <param name="type">The type.</param> |
| | | 540 | | /// <returns>System.Object.</returns> |
| | | 541 | | private IPlugin? CreatePluginInstance(Type type) |
| | | 542 | | { |
| | | 543 | | // Find the record for this plugin. |
| | 147 | 544 | | var plugin = GetPluginByAssembly(type.Assembly); |
| | 147 | 545 | | if (plugin?.Manifest.Status < PluginStatus.Active) |
| | | 546 | | { |
| | 0 | 547 | | return null; |
| | | 548 | | } |
| | | 549 | | |
| | | 550 | | try |
| | | 551 | | { |
| | 147 | 552 | | _logger.LogDebug("Creating instance of {Type}", type); |
| | | 553 | | // _appHost.ServiceProvider is already assigned when we create the plugins |
| | 147 | 554 | | var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider!, type); |
| | 147 | 555 | | if (plugin is null) |
| | | 556 | | { |
| | | 557 | | // Create a dummy record for the providers. |
| | | 558 | | // TODO: remove this code once all provided have been released as separate plugins. |
| | 147 | 559 | | plugin = new LocalPlugin( |
| | 147 | 560 | | instance.AssemblyFilePath, |
| | 147 | 561 | | true, |
| | 147 | 562 | | new PluginManifest |
| | 147 | 563 | | { |
| | 147 | 564 | | Id = instance.Id, |
| | 147 | 565 | | Status = PluginStatus.Active, |
| | 147 | 566 | | Name = instance.Name, |
| | 147 | 567 | | Version = instance.Version.ToString() |
| | 147 | 568 | | }) |
| | 147 | 569 | | { |
| | 147 | 570 | | Instance = instance |
| | 147 | 571 | | }; |
| | | 572 | | |
| | 147 | 573 | | _plugins.Add(plugin); |
| | | 574 | | |
| | 147 | 575 | | plugin.Manifest.Status = PluginStatus.Active; |
| | | 576 | | } |
| | | 577 | | else |
| | | 578 | | { |
| | 0 | 579 | | plugin.Instance = instance; |
| | 0 | 580 | | var manifest = plugin.Manifest; |
| | 0 | 581 | | var pluginStr = instance.Version.ToString(); |
| | 0 | 582 | | bool changed = false; |
| | 0 | 583 | | if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal) |
| | 0 | 584 | | || !manifest.Id.Equals(instance.Id)) |
| | | 585 | | { |
| | | 586 | | // If a plugin without a manifest failed to load due to an external issue (eg config), |
| | | 587 | | // this updates the manifest to the actual plugin values. |
| | 0 | 588 | | manifest.Version = pluginStr; |
| | 0 | 589 | | manifest.Name = plugin.Instance.Name; |
| | 0 | 590 | | manifest.Description = plugin.Instance.Description; |
| | 0 | 591 | | manifest.Id = plugin.Instance.Id; |
| | 0 | 592 | | changed = true; |
| | | 593 | | } |
| | | 594 | | |
| | 0 | 595 | | changed = changed || manifest.Status != PluginStatus.Active; |
| | 0 | 596 | | manifest.Status = PluginStatus.Active; |
| | | 597 | | |
| | 0 | 598 | | if (changed) |
| | | 599 | | { |
| | 0 | 600 | | SaveManifest(manifest, plugin.Path); |
| | | 601 | | } |
| | | 602 | | } |
| | | 603 | | |
| | 147 | 604 | | _logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version); |
| | | 605 | | |
| | 147 | 606 | | return instance; |
| | | 607 | | } |
| | | 608 | | #pragma warning disable CA1031 // Do not catch general exception types |
| | 0 | 609 | | catch (Exception ex) |
| | | 610 | | #pragma warning restore CA1031 // Do not catch general exception types |
| | | 611 | | { |
| | 0 | 612 | | _logger.LogError(ex, "Error creating {Type}", type.FullName); |
| | 0 | 613 | | if (plugin is not null) |
| | | 614 | | { |
| | 0 | 615 | | if (ChangePluginState(plugin, PluginStatus.Malfunctioned)) |
| | | 616 | | { |
| | 0 | 617 | | _logger.LogInformation("Plugin {Path} has been disabled.", plugin.Path); |
| | 0 | 618 | | return null; |
| | | 619 | | } |
| | | 620 | | } |
| | | 621 | | |
| | 0 | 622 | | _logger.LogDebug("Unable to auto-disable."); |
| | 0 | 623 | | return null; |
| | | 624 | | } |
| | 147 | 625 | | } |
| | | 626 | | |
| | | 627 | | private void UpdatePluginSupersededStatus(LocalPlugin plugin) |
| | | 628 | | { |
| | 0 | 629 | | if (plugin.Manifest.Status != PluginStatus.Superseded) |
| | | 630 | | { |
| | 0 | 631 | | return; |
| | | 632 | | } |
| | | 633 | | |
| | 0 | 634 | | var predecessor = _plugins.OrderByDescending(p => p.Version) |
| | 0 | 635 | | .FirstOrDefault(p => p.Id.Equals(plugin.Id) && p.IsEnabledAndSupported && p.Version != plugin.Version); |
| | 0 | 636 | | if (predecessor is not null) |
| | | 637 | | { |
| | 0 | 638 | | return; |
| | | 639 | | } |
| | | 640 | | |
| | 0 | 641 | | plugin.Manifest.Status = PluginStatus.Active; |
| | 0 | 642 | | } |
| | | 643 | | |
| | | 644 | | /// <summary> |
| | | 645 | | /// Attempts to delete a plugin. |
| | | 646 | | /// </summary> |
| | | 647 | | /// <param name="plugin">A <see cref="LocalPlugin"/> instance to delete.</param> |
| | | 648 | | /// <returns>True if successful.</returns> |
| | | 649 | | private bool DeletePlugin(LocalPlugin plugin) |
| | | 650 | | { |
| | | 651 | | // Attempt a cleanup of old folders. |
| | | 652 | | try |
| | | 653 | | { |
| | 0 | 654 | | Directory.Delete(plugin.Path, true); |
| | 0 | 655 | | _logger.LogDebug("Deleted {Path}", plugin.Path); |
| | 0 | 656 | | } |
| | | 657 | | #pragma warning disable CA1031 // Do not catch general exception types |
| | 0 | 658 | | catch |
| | | 659 | | #pragma warning restore CA1031 // Do not catch general exception types |
| | | 660 | | { |
| | 0 | 661 | | return false; |
| | | 662 | | } |
| | | 663 | | |
| | 0 | 664 | | return _plugins.Remove(plugin); |
| | 0 | 665 | | } |
| | | 666 | | |
| | | 667 | | internal LocalPlugin LoadManifest(string dir) |
| | | 668 | | { |
| | | 669 | | Version? version; |
| | 16 | 670 | | PluginManifest? manifest = null; |
| | 16 | 671 | | var metafile = Path.Combine(dir, MetafileName); |
| | 16 | 672 | | if (File.Exists(metafile)) |
| | | 673 | | { |
| | | 674 | | // Only path where this stays null is when File.ReadAllBytes throws an IOException |
| | 16 | 675 | | byte[] data = null!; |
| | | 676 | | try |
| | | 677 | | { |
| | 16 | 678 | | data = File.ReadAllBytes(metafile); |
| | 16 | 679 | | manifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions); |
| | 16 | 680 | | } |
| | 0 | 681 | | catch (IOException ex) |
| | | 682 | | { |
| | 0 | 683 | | _logger.LogError(ex, "Error reading file {Path}.", dir); |
| | 0 | 684 | | } |
| | 0 | 685 | | catch (JsonException ex) |
| | | 686 | | { |
| | 0 | 687 | | _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data)); |
| | 0 | 688 | | } |
| | | 689 | | |
| | 16 | 690 | | if (manifest is not null) |
| | | 691 | | { |
| | 16 | 692 | | if (!Version.TryParse(manifest.TargetAbi, out var targetAbi)) |
| | | 693 | | { |
| | 16 | 694 | | targetAbi = _minimumVersion; |
| | | 695 | | } |
| | | 696 | | |
| | 16 | 697 | | if (!Version.TryParse(manifest.Version, out version)) |
| | | 698 | | { |
| | 12 | 699 | | manifest.Version = _minimumVersion.ToString(); |
| | | 700 | | } |
| | | 701 | | |
| | 16 | 702 | | return new LocalPlugin(dir, _appVersion >= targetAbi, manifest); |
| | | 703 | | } |
| | | 704 | | } |
| | | 705 | | |
| | | 706 | | // No metafile, so lets see if the folder is versioned. |
| | | 707 | | // TODO: Phase this support out in future versions. |
| | 0 | 708 | | metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1]; |
| | 0 | 709 | | int versionIndex = dir.LastIndexOf('_'); |
| | 0 | 710 | | if (versionIndex != -1) |
| | | 711 | | { |
| | | 712 | | // Get the version number from the filename if possible. |
| | 0 | 713 | | metafile = Path.GetFileName(dir[..versionIndex]); |
| | 0 | 714 | | version = Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version? parsedVersion) ? parsedVersi |
| | | 715 | | } |
| | | 716 | | else |
| | | 717 | | { |
| | | 718 | | // Un-versioned folder - Add it under the path name and version it suitable for this instance. |
| | 0 | 719 | | version = _appVersion; |
| | | 720 | | } |
| | | 721 | | |
| | | 722 | | // Auto-create a plugin manifest, so we can disable it, if it fails to load. |
| | 0 | 723 | | manifest = new PluginManifest |
| | 0 | 724 | | { |
| | 0 | 725 | | Status = PluginStatus.Active, |
| | 0 | 726 | | Name = metafile, |
| | 0 | 727 | | AutoUpdate = false, |
| | 0 | 728 | | Id = metafile.GetMD5(), |
| | 0 | 729 | | TargetAbi = _appVersion.ToString(), |
| | 0 | 730 | | Version = version.ToString() |
| | 0 | 731 | | }; |
| | | 732 | | |
| | 0 | 733 | | return new LocalPlugin(dir, true, manifest); |
| | | 734 | | } |
| | | 735 | | |
| | | 736 | | /// <summary> |
| | | 737 | | /// Gets the list of local plugins. |
| | | 738 | | /// </summary> |
| | | 739 | | /// <returns>Enumerable of local plugins.</returns> |
| | | 740 | | private IEnumerable<LocalPlugin> DiscoverPlugins() |
| | | 741 | | { |
| | 15 | 742 | | var versions = new List<LocalPlugin>(); |
| | | 743 | | |
| | 15 | 744 | | if (!Directory.Exists(_pluginsPath)) |
| | | 745 | | { |
| | | 746 | | // Plugin path doesn't exist, don't try to enumerate sub-folders. |
| | 0 | 747 | | return Enumerable.Empty<LocalPlugin>(); |
| | | 748 | | } |
| | | 749 | | |
| | 15 | 750 | | var directories = Directory.EnumerateDirectories(_pluginsPath, "*.*", SearchOption.TopDirectoryOnly); |
| | 60 | 751 | | foreach (var dir in directories) |
| | | 752 | | { |
| | 15 | 753 | | versions.Add(LoadManifest(dir)); |
| | | 754 | | } |
| | | 755 | | |
| | 15 | 756 | | string lastName = string.Empty; |
| | 15 | 757 | | versions.Sort(LocalPlugin.Compare); |
| | | 758 | | // Traverse backwards through the list. |
| | | 759 | | // The first item will be the latest version. |
| | 60 | 760 | | for (int x = versions.Count - 1; x >= 0; x--) |
| | | 761 | | { |
| | 15 | 762 | | var entry = versions[x]; |
| | 15 | 763 | | if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase)) |
| | | 764 | | { |
| | 12 | 765 | | if (!TryGetPluginDlls(entry, out var allowedDlls)) |
| | | 766 | | { |
| | 9 | 767 | | _logger.LogError("One or more assembly paths was invalid. Marking plugin {Plugin} as \"Malfuncti |
| | 9 | 768 | | ChangePluginState(entry, PluginStatus.Malfunctioned); |
| | 9 | 769 | | continue; |
| | | 770 | | } |
| | | 771 | | |
| | 3 | 772 | | entry.DllFiles = allowedDlls; |
| | | 773 | | |
| | 3 | 774 | | if (entry.IsEnabledAndSupported) |
| | | 775 | | { |
| | 3 | 776 | | lastName = entry.Name; |
| | 3 | 777 | | continue; |
| | | 778 | | } |
| | | 779 | | } |
| | | 780 | | |
| | 3 | 781 | | if (string.IsNullOrEmpty(lastName)) |
| | | 782 | | { |
| | | 783 | | continue; |
| | | 784 | | } |
| | | 785 | | |
| | 0 | 786 | | var cleaned = false; |
| | 0 | 787 | | var path = entry.Path; |
| | | 788 | | // Attempt a cleanup of old folders. |
| | | 789 | | try |
| | | 790 | | { |
| | 0 | 791 | | _logger.LogDebug("Deleting {Path}", path); |
| | 0 | 792 | | Directory.Delete(path, true); |
| | 0 | 793 | | cleaned = true; |
| | 0 | 794 | | } |
| | | 795 | | #pragma warning disable CA1031 // Do not catch general exception types |
| | 0 | 796 | | catch (Exception e) |
| | | 797 | | #pragma warning restore CA1031 // Do not catch general exception types |
| | | 798 | | { |
| | 0 | 799 | | _logger.LogWarning(e, "Unable to delete {Path}", path); |
| | 0 | 800 | | } |
| | | 801 | | |
| | 0 | 802 | | if (cleaned) |
| | | 803 | | { |
| | 0 | 804 | | versions.RemoveAt(x); |
| | | 805 | | } |
| | | 806 | | else |
| | | 807 | | { |
| | 0 | 808 | | ChangePluginState(entry, PluginStatus.Deleted); |
| | | 809 | | } |
| | | 810 | | } |
| | | 811 | | |
| | | 812 | | // Only want plugin folders which have files. |
| | 15 | 813 | | return versions.Where(p => p.DllFiles.Count != 0); |
| | | 814 | | } |
| | | 815 | | |
| | | 816 | | /// <summary> |
| | | 817 | | /// Attempts to retrieve valid DLLs from the plugin path. This method will consider the assembly whitelist |
| | | 818 | | /// from the manifest. |
| | | 819 | | /// </summary> |
| | | 820 | | /// <remarks> |
| | | 821 | | /// Loading DLLs from externally supplied paths introduces a path traversal risk. This method |
| | | 822 | | /// uses a safelisting tactic of considering DLLs from the plugin directory and only using |
| | | 823 | | /// the plugin's canonicalized assembly whitelist for comparison. See |
| | | 824 | | /// <see href="https://owasp.org/www-community/attacks/Path_Traversal"/> for more details. |
| | | 825 | | /// </remarks> |
| | | 826 | | /// <param name="plugin">The plugin.</param> |
| | | 827 | | /// <param name="whitelistedDlls">The whitelisted DLLs. If the method returns <see langword="false"/>, this will |
| | | 828 | | /// <returns> |
| | | 829 | | /// <see langword="true"/> if all assemblies listed in the manifest were available in the plugin directory. |
| | | 830 | | /// <see langword="false"/> if any assemblies were invalid or missing from the plugin directory. |
| | | 831 | | /// </returns> |
| | | 832 | | /// <exception cref="ArgumentNullException">If the <see cref="LocalPlugin"/> is null.</exception> |
| | | 833 | | private bool TryGetPluginDlls(LocalPlugin plugin, out IReadOnlyList<string> whitelistedDlls) |
| | | 834 | | { |
| | 12 | 835 | | ArgumentNullException.ThrowIfNull(plugin); |
| | | 836 | | |
| | 12 | 837 | | IReadOnlyList<string> pluginDlls = Directory.GetFiles(plugin.Path, "*.dll", SearchOption.AllDirectories); |
| | | 838 | | |
| | 12 | 839 | | whitelistedDlls = Array.Empty<string>(); |
| | 12 | 840 | | if (pluginDlls.Count > 0 && plugin.Manifest.Assemblies.Count > 0) |
| | | 841 | | { |
| | 12 | 842 | | _logger.LogInformation("Registering whitelisted assemblies for plugin \"{Plugin}\"...", plugin.Name); |
| | | 843 | | |
| | 12 | 844 | | var canonicalizedPaths = new List<string>(); |
| | 45 | 845 | | foreach (var path in plugin.Manifest.Assemblies) |
| | | 846 | | { |
| | 12 | 847 | | var canonicalized = Path.Combine(plugin.Path, path).Canonicalize(); |
| | | 848 | | |
| | | 849 | | // Ensure we stay in the plugin directory. |
| | 12 | 850 | | if (!canonicalized.StartsWith(plugin.Path.NormalizePath(), StringComparison.Ordinal)) |
| | | 851 | | { |
| | 3 | 852 | | _logger.LogError("Assembly path {Path} is not inside the plugin directory.", path); |
| | 3 | 853 | | return false; |
| | | 854 | | } |
| | | 855 | | |
| | 9 | 856 | | canonicalizedPaths.Add(canonicalized); |
| | | 857 | | } |
| | | 858 | | |
| | 9 | 859 | | var intersected = pluginDlls.Intersect(canonicalizedPaths).ToList(); |
| | | 860 | | |
| | 9 | 861 | | if (intersected.Count != canonicalizedPaths.Count) |
| | | 862 | | { |
| | 6 | 863 | | _logger.LogError("Plugin {Plugin} contained assembly paths that were not found in the directory.", p |
| | 6 | 864 | | return false; |
| | | 865 | | } |
| | | 866 | | |
| | 3 | 867 | | whitelistedDlls = intersected; |
| | | 868 | | } |
| | | 869 | | else |
| | | 870 | | { |
| | | 871 | | // No whitelist, default to loading all DLLs in plugin directory. |
| | 0 | 872 | | whitelistedDlls = pluginDlls; |
| | | 873 | | } |
| | | 874 | | |
| | 3 | 875 | | return true; |
| | 3 | 876 | | } |
| | | 877 | | |
| | | 878 | | /// <summary> |
| | | 879 | | /// Changes the status of the other versions of the plugin to "Superseded". |
| | | 880 | | /// </summary> |
| | | 881 | | /// <param name="plugin">The <see cref="LocalPlugin"/> that's master.</param> |
| | | 882 | | private void ProcessAlternative(LocalPlugin plugin) |
| | | 883 | | { |
| | | 884 | | // Detect whether there is another version of this plugin that needs disabling. |
| | 0 | 885 | | var previousVersion = _plugins.OrderByDescending(p => p.Version) |
| | 0 | 886 | | .FirstOrDefault( |
| | 0 | 887 | | p => p.Id.Equals(plugin.Id) |
| | 0 | 888 | | && p.IsEnabledAndSupported |
| | 0 | 889 | | && p.Version != plugin.Version); |
| | | 890 | | |
| | 0 | 891 | | if (previousVersion is null) |
| | | 892 | | { |
| | | 893 | | // This value is memory only - so that the web will show restart required. |
| | 0 | 894 | | plugin.Manifest.Status = PluginStatus.Restart; |
| | 0 | 895 | | plugin.Manifest.AutoUpdate = false; |
| | 0 | 896 | | return; |
| | | 897 | | } |
| | | 898 | | |
| | 0 | 899 | | if (plugin.Manifest.Status == PluginStatus.Active && !ChangePluginState(previousVersion, PluginStatus.Supers |
| | | 900 | | { |
| | 0 | 901 | | _logger.LogError("Unable to enable version {Version} of {Name}", previousVersion.Version, previousVersio |
| | | 902 | | } |
| | 0 | 903 | | else if (plugin.Manifest.Status == PluginStatus.Superseded && !ChangePluginState(previousVersion, PluginStat |
| | | 904 | | { |
| | 0 | 905 | | _logger.LogError("Unable to supercede version {Version} of {Name}", previousVersion.Version, previousVer |
| | | 906 | | } |
| | | 907 | | |
| | | 908 | | // This value is memory only - so that the web will show restart required. |
| | 0 | 909 | | plugin.Manifest.Status = PluginStatus.Restart; |
| | 0 | 910 | | plugin.Manifest.AutoUpdate = false; |
| | 0 | 911 | | } |
| | | 912 | | } |
| | | 913 | | } |