From e78d16872424266052c3719412b0178283239e11 Mon Sep 17 00:00:00 2001 From: James A Sutherland Date: Thu, 4 Jan 2024 18:02:32 -0600 Subject: [PATCH] Update rdmpplugins.txt on plugin addition, deletion --- .../ExecuteCommandPrunePlugin.cs | 2 +- .../CommandLine/Runners/PackPluginRunner.cs | 31 +++---- Rdmp.Core/Curation/Data/LoadModuleAssembly.cs | 84 +++++++++++++++---- Rdmp.Core/Rdmp.Core.csproj | 1 + Rdmp.Core/Startup/Startup.cs | 12 +-- .../ExecuteCommandDeletePlugin.cs | 22 ++--- 6 files changed, 97 insertions(+), 55 deletions(-) diff --git a/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandPrunePlugin.cs b/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandPrunePlugin.cs index d89a942d99..e44459d527 100644 --- a/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandPrunePlugin.cs +++ b/Rdmp.Core/CommandExecution/AtomicCommands/ExecuteCommandPrunePlugin.cs @@ -23,7 +23,7 @@ namespace Rdmp.Core.CommandExecution.AtomicCommands; /// dll at runtime and so these dlls will just bloat your plugin. Use this command to prune out /// those files. /// -public partial class ExecuteCommandPrunePlugin : BasicCommandExecution +public sealed partial class ExecuteCommandPrunePlugin : BasicCommandExecution { private string _file; diff --git a/Rdmp.Core/CommandLine/Runners/PackPluginRunner.cs b/Rdmp.Core/CommandLine/Runners/PackPluginRunner.cs index 6ec4280dec..1981393641 100644 --- a/Rdmp.Core/CommandLine/Runners/PackPluginRunner.cs +++ b/Rdmp.Core/CommandLine/Runners/PackPluginRunner.cs @@ -12,6 +12,7 @@ using System.Xml.Linq; using Rdmp.Core.CommandExecution.AtomicCommands; using Rdmp.Core.CommandLine.Options; +using Rdmp.Core.Curation.Data; using Rdmp.Core.DataFlowPipeline; using Rdmp.Core.Repositories; using Rdmp.Core.ReusableLibraryCode.Checks; @@ -23,13 +24,13 @@ namespace Rdmp.Core.CommandLine.Runners; /// /// Uploads a packed plugin (.nupkg) into a consumable plugin for RDMP /// -public class PackPluginRunner : IRunner +public sealed partial class PackPluginRunner : IRunner { private readonly PackOptions _packOpts; public const string PluginPackageSuffix = ".nupkg"; - public const string PluginPackageManifest = ".nuspec"; + private const string PluginPackageManifest = ".nuspec"; - private static readonly Regex VersionSuffix = new("-.*$"); + private static readonly Regex VersionSuffix = VersionSuffixRe(); public PackPluginRunner(PackOptions packOpts) { @@ -44,7 +45,7 @@ public int Run(IRDMPPlatformRepositoryServiceLocator repositoryLocator, IDataLoa if (!toCommit.Exists) throw new FileNotFoundException($"Could not find file '{toCommit}'"); - if (toCommit.Extension.ToLowerInvariant() != PluginPackageSuffix) + if (!toCommit.Name.EndsWith(PluginPackageSuffix, StringComparison.OrdinalIgnoreCase)) throw new NotSupportedException($"Plugins must be packaged as {PluginPackageSuffix}"); //the version of the plugin e.g. MyPlugin.nupkg version 1.0.0.0 @@ -62,7 +63,7 @@ public int Run(IRDMPPlatformRepositoryServiceLocator repositoryLocator, IDataLoa //find the manifest that lists name, version etc using (var zf = ZipFile.OpenRead(toCommit.FullName)) { - var manifests = zf.Entries.Where(e => e.FullName.EndsWith(PluginPackageManifest)).ToArray(); + var manifests = zf.Entries.Where(static e => e.FullName.EndsWith(PluginPackageManifest, StringComparison.OrdinalIgnoreCase)).ToArray(); if (manifests.Length != 1) throw new Exception( @@ -79,7 +80,7 @@ public int Run(IRDMPPlatformRepositoryServiceLocator repositoryLocator, IDataLoa var rdmpDependencyNode = doc.Descendants(ns + "dependency") - .FirstOrDefault(e => e?.Attribute("id")?.Value == "HIC.RDMP.Plugin") ?? throw new Exception( + .FirstOrDefault(static e => e?.Attribute("id")?.Value == "HIC.RDMP.Plugin") ?? throw new Exception( "Expected a single tag with id = HIC.RDMP.Plugin (in order to determine plugin compatibility). Ensure your nuspec file includes a dependency on this package."); rdmpDependencyVersion = new Version(VersionSuffix.Replace(rdmpDependencyNode?.Attribute("version")?.Value ?? "", "")); @@ -90,23 +91,11 @@ public int Run(IRDMPPlatformRepositoryServiceLocator repositoryLocator, IDataLoa throw new NotSupportedException( $"Plugin version {pluginVersion} is incompatible with current running version of RDMP ({runningSoftwareVersion})."); - UploadFile(repositoryLocator, checkNotifier, toCommit, pluginVersion, rdmpDependencyVersion); + LoadModuleAssembly.UploadFile(checkNotifier, toCommit); return 0; } - private static void UploadFile(IRDMPPlatformRepositoryServiceLocator repositoryLocator, - ICheckNotifier checkNotifier, FileInfo toCommit, Version pluginVersion, Version rdmpDependencyVersion) - { - try - { - toCommit.CopyTo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, toCommit.Name), true); - } - catch (Exception e) - { - checkNotifier.OnCheckPerformed(new CheckEventArgs($"Failed copying plugin {toCommit.Name}", - CheckResult.Fail, e)); - throw; - } - } + [GeneratedRegex("-.*$")] + private static partial Regex VersionSuffixRe(); } \ No newline at end of file diff --git a/Rdmp.Core/Curation/Data/LoadModuleAssembly.cs b/Rdmp.Core/Curation/Data/LoadModuleAssembly.cs index 07c00a752b..26316f1f60 100644 --- a/Rdmp.Core/Curation/Data/LoadModuleAssembly.cs +++ b/Rdmp.Core/Curation/Data/LoadModuleAssembly.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using ICSharpCode.SharpZipLib.Zip; +using Rdmp.Core.ReusableLibraryCode.Checks; namespace Rdmp.Core.Curation.Data; @@ -17,7 +18,12 @@ namespace Rdmp.Core.Curation.Data; /// public sealed class LoadModuleAssembly { - internal static readonly List Assemblies=new(); + private static readonly bool IsWin = AppDomain.CurrentDomain.GetAssemblies() + .Any(static a => a.FullName?.StartsWith("Rdmp.UI", StringComparison.Ordinal) == true); + + private static readonly string PluginsList = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "rdmpplugins.txt"); + + internal static readonly List Assemblies = []; private readonly FileInfo _file; private LoadModuleAssembly(FileInfo file) @@ -25,27 +31,36 @@ private LoadModuleAssembly(FileInfo file) _file = file; } + /// + /// List the plugin files to load + /// + /// + internal static IEnumerable PluginFiles() + { + return File.Exists(PluginsList) + ? File.ReadAllLines(PluginsList) + .Select(static name => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, name)) + : Directory.EnumerateFiles(AppDomain.CurrentDomain.BaseDirectory, "*.nupkg"); + } + /// /// Unpack the plugin DLL files, excluding any Windows UI specific dlls when not running a Windows GUI /// - internal static IEnumerable> GetContents(string path) + internal static IEnumerable<(string, MemoryStream)> GetContents(string path) { var info = new FileInfo(path); if (!info.Exists || info.Length < 100) yield break; // Ignore missing or empty files var pluginStream = info.OpenRead(); - Assemblies.Add(new LoadModuleAssembly(info)); - - var isWin = AppDomain.CurrentDomain.GetAssemblies() - .Any(static a => a.FullName?.StartsWith("Rdmp.UI", StringComparison.Ordinal) == true); - if (!pluginStream.CanSeek) throw new ArgumentException("Seek needed", nameof(path)); + Assemblies.Add(new LoadModuleAssembly(info)); + using var zip = new ZipFile(pluginStream); foreach (var e in zip.Cast() - .Where(static e => e.IsFile && e.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) - .Where(e => isWin || !e.Name.Contains("/windows/"))) + .Where(static e => e.IsFile && e.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && + (IsWin || !e.Name.Contains("/windows/")))) { using var s = zip.GetInputStream(e); using var ms2 = new MemoryStream(); @@ -59,15 +74,56 @@ internal static IEnumerable> GetContents(string /// Copy the plugin nupkg to the given directory /// /// - public string DownloadAssembly(DirectoryInfo downloadDirectory) + public void DownloadAssembly(DirectoryInfo downloadDirectory) { if (!downloadDirectory.Exists) downloadDirectory.Create(); - var targetFile=Path.Combine(downloadDirectory.FullName, _file.Name); - _file.CopyTo(targetFile,true); - return targetFile; + var targetFile = Path.Combine(downloadDirectory.FullName, _file.Name); + _file.CopyTo(targetFile, true); + } + + /// + /// Delete the plugin file from disk, and remove it from rdmpplugins.txt if in use + /// + public void Delete() + { + _file.Delete(); + if (!File.Exists(PluginsList)) return; + + var tmp = $"{PluginsList}.tmp"; + File.WriteAllLines(tmp, File.ReadAllLines(PluginsList).Where(l => !l.Contains(_file.Name))); + File.Move(tmp, PluginsList, true); } - public void Delete() => _file.Delete(); public override string ToString() => _file.Name; + + public static void UploadFile(ICheckNotifier checkNotifier, FileInfo toCommit) + { + try + { + toCommit.CopyTo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, toCommit.Name), true); + if (!File.Exists(PluginsList)) return; + + // Now the tricky bit: add this new file, and remove any other versions of the same + // e.g. adding DummyPlugin-1.2.3 should delete DummyPlugin-2.0 and DummyPlugin-1.0 if present + var list = File.ReadAllLines(PluginsList); + var tmp = $"{PluginsList}.tmp"; + + var versionPos = toCommit.Name.IndexOf('-'); + if (versionPos != -1) + { + var stub = toCommit.Name[..(versionPos + 1)]; + list = list.Where(l => !l.StartsWith(stub, StringComparison.OrdinalIgnoreCase)).ToArray(); + } + + File.WriteAllLines(tmp, list.Union([toCommit.Name])); + File.Move(tmp, PluginsList, true); + } + catch (Exception e) + { + checkNotifier.OnCheckPerformed(new CheckEventArgs($"Failed copying plugin {toCommit.Name}", + CheckResult.Fail, e)); + throw; + } + } } \ No newline at end of file diff --git a/Rdmp.Core/Rdmp.Core.csproj b/Rdmp.Core/Rdmp.Core.csproj index 75bb24d031..cea418e93f 100644 --- a/Rdmp.Core/Rdmp.Core.csproj +++ b/Rdmp.Core/Rdmp.Core.csproj @@ -29,6 +29,7 @@ true + latest diff --git a/Rdmp.Core/Startup/Startup.cs b/Rdmp.Core/Startup/Startup.cs index dcea3429ea..6f3629d561 100644 --- a/Rdmp.Core/Startup/Startup.cs +++ b/Rdmp.Core/Startup/Startup.cs @@ -115,6 +115,7 @@ public void DoStartup(ICheckNotifier notifier) //only load data export manager if catalogue worked if (!foundCatalogue) return; + LoadMEF(RepositoryLocator.CatalogueRepository, notifier); //find tier 2 databases @@ -138,10 +139,10 @@ public void DoStartup(ICheckNotifier notifier) e)); } - FindTier3Databases(RepositoryLocator.CatalogueRepository, notifier); + FindTier3Databases(notifier); } - private void FindTier3Databases(ICatalogueRepository catalogueRepository, ICheckNotifier notifier) + private void FindTier3Databases(ICheckNotifier notifier) { foreach (var patcher in _patcherManager.GetTier3Patchers(PluginPatcherFound)) FindWithPatcher(patcher, notifier); @@ -241,12 +242,7 @@ private void FindWithPatcher(IPatcher patcher, ICheckNotifier notifier) /// private static void LoadMEF(ICatalogueRepository catalogueRepository, ICheckNotifier notifier) { - var pluginsList = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "rdmpplugins.txt"); - var packageFiles = File.Exists(pluginsList) - ? File.ReadAllLines(pluginsList) - .Select(static name => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, name)) - : Directory.EnumerateFiles(AppDomain.CurrentDomain.BaseDirectory, "*.nupkg"); - foreach (var (name, body) in packageFiles.SelectMany(LoadModuleAssembly.GetContents)) + foreach (var (name, body) in LoadModuleAssembly.PluginFiles().SelectMany(LoadModuleAssembly.GetContents)) try { AssemblyLoadContext.Default.LoadFromStream(body); diff --git a/Rdmp.UI/CommandExecution/AtomicCommands/ExecuteCommandDeletePlugin.cs b/Rdmp.UI/CommandExecution/AtomicCommands/ExecuteCommandDeletePlugin.cs index f57f67de6d..a4eae128d8 100644 --- a/Rdmp.UI/CommandExecution/AtomicCommands/ExecuteCommandDeletePlugin.cs +++ b/Rdmp.UI/CommandExecution/AtomicCommands/ExecuteCommandDeletePlugin.cs @@ -11,11 +11,13 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using System; + namespace Rdmp.UI.CommandExecution.AtomicCommands; -public class ExecuteCommandDeletePlugin : BasicUICommandExecution +public sealed class ExecuteCommandDeletePlugin : BasicUICommandExecution { private readonly LoadModuleAssembly _assembly; + public ExecuteCommandDeletePlugin(IActivateItems activator, LoadModuleAssembly assembly) : base(activator) { _assembly = assembly; @@ -27,18 +29,16 @@ public override Image GetImage(IIconProvider iconProvider) => public override void Execute() { base.Execute(); - if (YesNo($"Are you sure you want to delete {_assembly}?", "Delete Plugin")) + if (!YesNo($"Are you sure you want to delete {_assembly}?", "Delete Plugin")) return; + + try { _assembly.Delete(); - try - { - _assembly.Delete(); - Show("Changes will take effect on restart"); - } - catch (SystemException ex) - { - Show($"Could not delete the {_assembly} plugin.", ex); - } + Show("Changes will take effect on restart"); + } + catch (SystemException ex) + { + Show($"Could not delete the {_assembly} plugin.", ex); } } } \ No newline at end of file