From 60f65f0bf0978129aa702d3cc4fc89e21b9e332b Mon Sep 17 00:00:00 2001 From: Donkey Date: Wed, 31 Jul 2024 16:59:12 +0200 Subject: [PATCH] feat: initial commit of list command, wip tests are missing --- TwinpackCli/Commands/ListCommand.cs | 64 ++++++++++ TwinpackCli/Program.cs | 3 +- TwinpackCli/TwinpackCli.csproj | 1 + .../Core/AutomationInterfaceUtils.cs | 120 ++++++++++++++++++ .../Core/PackageServerCollection.cs | 62 ++++++++- TwinpackShared/Core/TwinpackService.cs | 38 +++++- TwinpackShared/Models/Adapter.cs | 6 +- TwinpackShared/Models/ConfigFactory.cs | 5 +- TwinpackShared/TwinpackShared.projitems | 1 + TwinpackTests/ConfigFactoryTest.cs | 1 + .../PackageVersionToBooleanConverter.cs | 21 +++ 11 files changed, 316 insertions(+), 6 deletions(-) create mode 100644 TwinpackCli/Commands/ListCommand.cs create mode 100644 TwinpackShared/Core/AutomationInterfaceUtils.cs create mode 100644 TwinpackVsixShared/Dialogs/Converters/PackageVersionToBooleanConverter.cs diff --git a/TwinpackCli/Commands/ListCommand.cs b/TwinpackCli/Commands/ListCommand.cs new file mode 100644 index 0000000..2730b4d --- /dev/null +++ b/TwinpackCli/Commands/ListCommand.cs @@ -0,0 +1,64 @@ +using CommandLine; +using NLog.Fluent; +using NuGet.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Linq; +using System.Threading.Tasks; +using Twinpack.Core; +using Twinpack.Models; +using Twinpack.Protocol; + +namespace Twinpack.Commands +{ + [Verb("list", HelpText = @"Searches all uses packages sources defined ./Zeugwerk/config.json or in the first solution found in the current directory")] + public class ListCommand : Command + { + [Value(0, MetaName = "search term", Required = false)] + public string? SearchTerm { get; set; } + + [Option("take", Required = false, Default = null, HelpText = "Limit the number of results to return")] + public int? Take { get; set; } + + [Option("plc", Required = false, Default = null, HelpText = "Filter for specific plcs, by default all plcs are considered")] + public IEnumerable PlcFilter { get; set; } + + [Option("outdated", Required = false, Default = null, HelpText = "Only show outdated packages")] + public bool Outdated { get; set; } + + public override async Task ExecuteAsync() + { + await PackagingServerRegistry.InitializeAsync(); + _twinpack = new TwinpackService(PackagingServerRegistry.Servers); + + await _twinpack.LoginAsync(); + + var rootPath = Environment.CurrentDirectory; + var config = ConfigFactory.Load(rootPath); + + if (config == null) + { + config = await ConfigFactory.CreateFromSolutionFileAsync( + rootPath, + continueWithoutSolution: false, + packageServers: PackagingServerRegistry.Servers.Where(x => x.Connected), + plcTypeFilter: null); + } + + foreach (var project in config.Projects) + { + project.Plcs = project.Plcs.Where(x => !PlcFilter.Any() || PlcFilter.Contains(x.Name)).ToList(); + } + + var packages = await _twinpack.RetrieveInstalledPackagesAsync(config, SearchTerm); + foreach (var package in packages.Where(x => !Outdated || x.IsUpdateable)) + { + Console.WriteLine($"{package.Name} {package.InstalledVersion} {(package.IsUpdateable ? $"-> {package.UpdateVersion}" : "")}"); + } + + return 0; + } + } +} diff --git a/TwinpackCli/Program.cs b/TwinpackCli/Program.cs index 467eb80..9e58ad0 100644 --- a/TwinpackCli/Program.cs +++ b/TwinpackCli/Program.cs @@ -26,9 +26,10 @@ static async Task Main(string[] args) try { - return Parser.Default.ParseArguments(args) + return Parser.Default.ParseArguments(args) .MapResult( (SearchCommand command) => ExecuteAsync(command).GetAwaiter().GetResult(), + (ListCommand command) => ExecuteAsync(command).GetAwaiter().GetResult(), (PullCommand command) => ExecuteAsync(command).GetAwaiter().GetResult(), (PushCommand command) => ExecuteAsync(command).GetAwaiter().GetResult(), errs => 1 diff --git a/TwinpackCli/TwinpackCli.csproj b/TwinpackCli/TwinpackCli.csproj index c7a87ed..24586b5 100644 --- a/TwinpackCli/TwinpackCli.csproj +++ b/TwinpackCli/TwinpackCli.csproj @@ -50,6 +50,7 @@ + diff --git a/TwinpackShared/Core/AutomationInterfaceUtils.cs b/TwinpackShared/Core/AutomationInterfaceUtils.cs new file mode 100644 index 0000000..88e5e2e --- /dev/null +++ b/TwinpackShared/Core/AutomationInterfaceUtils.cs @@ -0,0 +1,120 @@ +using EnvDTE80; +using System; +using TCatSysManagerLib; + +namespace Twinpack.Core +{ + public class AutomationInterfaceService + { + DTE2 _dte; + + public AutomationInterfaceService(DTE2 dte) + { + _dte = dte; + } + + public string ResolveEffectiveVersion(string projectName, string placeholderName) + { + ResolvePlaceholder(LibraryManager(projectName), placeholderName, out _, out string effectiveVersion); + + return effectiveVersion; + } + + private static ITcPlcLibrary ResolvePlaceholder(ITcPlcLibraryManager libManager, string placeholderName, out string distributorName, out string effectiveVersion) + { + // Try to remove the already existing reference + foreach (ITcPlcLibRef item in libManager.References) + { + string itemPlaceholderName; + ITcPlcLibrary plcLibrary; + + try + { + ITcPlcPlaceholderRef2 plcPlaceholder; // this will throw if the library is currently not installed + plcPlaceholder = (ITcPlcPlaceholderRef2)item; + + itemPlaceholderName = plcPlaceholder.PlaceholderName; + + if (plcPlaceholder.EffectiveResolution != null) + plcLibrary = plcPlaceholder.EffectiveResolution; + else + plcLibrary = plcPlaceholder.DefaultResolution; + + effectiveVersion = plcLibrary.Version; + distributorName = plcLibrary.Distributor; + } + catch + { + plcLibrary = (ITcPlcLibrary)item; + effectiveVersion = null; + itemPlaceholderName = plcLibrary.Name.Split(',')[0]; + distributorName = plcLibrary.Distributor; + } + + if (string.Equals(itemPlaceholderName, placeholderName, StringComparison.InvariantCultureIgnoreCase)) + return plcLibrary; + } + + distributorName = null; + effectiveVersion = null; + return null; + } + + private ITcSysManager SystemManager(string projectName = null) + { + var ready = false; + while (!ready) + { + ready = true; + foreach (EnvDTE.Project project in _dte.Solution.Projects) + { + if (project == null) + ready = false; + else if ((projectName == null || project?.Name == projectName) && project.Object as ITcSysManager != null) + return project.Object as ITcSysManager; + } + + if (!ready) + System.Threading.Thread.Sleep(1000); + } + + return null; + } + + private ITcPlcLibraryManager LibraryManager(string projectName = null) + { + var systemManager = SystemManager(projectName); + + if (projectName == null) + { + var plcConfiguration = systemManager.LookupTreeItem("TIPC"); + for (var j = 1; j <= plcConfiguration.ChildCount; j++) + { + var plc = (plcConfiguration.Child[j] as ITcProjectRoot)?.NestedProject; + for (var k = 1; k <= (plc?.ChildCount ?? 0); k++) + { + if (plc.Child[k] as ITcPlcLibraryManager != null) + { + return plc.Child[k] as ITcPlcLibraryManager; + } + } + } + } + else + { + var projectRoot = systemManager.LookupTreeItem($"TIPC^{projectName}"); + var plc = (projectRoot as ITcProjectRoot)?.NestedProject; + for (var k = 1; k <= (plc?.ChildCount ?? 0); k++) + { + if (plc.Child[k] as ITcPlcLibraryManager != null) + { + return plc.Child[k] as ITcPlcLibraryManager; + } + } + + } + + return null; + } + } +} \ No newline at end of file diff --git a/TwinpackShared/Core/PackageServerCollection.cs b/TwinpackShared/Core/PackageServerCollection.cs index 03ea93c..68a0199 100644 --- a/TwinpackShared/Core/PackageServerCollection.cs +++ b/TwinpackShared/Core/PackageServerCollection.cs @@ -1,4 +1,5 @@ -using NLog; +using EnvDTE; +using NLog; using System; using System.Collections.Generic; using System.Linq; @@ -58,6 +59,65 @@ public async IAsyncEnumerable SearchAsync(string filter=null, int? } } + public async Task ResolvePackageAsync(ConfigProject project, ConfigPlcPackage item, AutomationInterfaceService automationInterface, CancellationToken token = default) + { + var catalogItem = new CatalogItem(item); + + foreach (var packageServer in this) + { + catalogItem.PackageServer = packageServer; + + + // try to get the installed package, if we can't find it at least try to resolve it + var packageVersion = await packageServer.GetPackageVersionAsync(new PlcLibrary { DistributorName = item.DistributorName, Name = item.Name, Version = item.Version }, + item.Branch, item.Configuration, item.Target, + cancellationToken: token); + + if (packageVersion != null && item.Version == null) + { + if (automationInterface != null) + { + var effectiveVersion = automationInterface.ResolveEffectiveVersion(project.Name, packageVersion.Title); + packageVersion = await packageServer.GetPackageVersionAsync(new PlcLibrary { DistributorName = item.DistributorName, Name = item.Name, Version = effectiveVersion }, + item.Branch, item.Configuration, item.Target, + cancellationToken: token); + } + else + { + _logger.Warn($"Cannot resolve wildcard reference '{packageVersion.Name}' without automation interface"); + } + } + + var packageVersionLatest = await packageServer.GetPackageVersionAsync(new PlcLibrary { DistributorName = item.DistributorName, Name = item.Name }, + item.Branch, item.Configuration, item.Target, + cancellationToken: token); + + // force the packageVersion references version even if the version was not found + if (packageVersion.Name != null) + { + catalogItem = new CatalogItem(packageServer, packageVersion); + catalogItem.Installed = packageVersion; + catalogItem.IsPlaceholder = item.Version == null; + } + + // a package might be updateable but not available on Twinpack + if (packageVersionLatest.Name != null) + { + catalogItem.Update = packageVersionLatest; + catalogItem.PackageServer = packageServer; + } + + catalogItem.Configuration = item; + + if (packageVersion.Name != null) + return catalogItem; + } + + catalogItem.Configuration = item; + + return catalogItem; + } + public async Task PullAsync(Config config, bool skipInternalPackages = false, IEnumerable filter = null, string cachePath = null, CancellationToken cancellationToken = default) { _logger.Info($"Pulling from Package Server(s) (skip internal packages: {(skipInternalPackages ? "true" : "false")})"); diff --git a/TwinpackShared/Core/TwinpackService.cs b/TwinpackShared/Core/TwinpackService.cs index bd017b9..fcf94c8 100644 --- a/TwinpackShared/Core/TwinpackService.cs +++ b/TwinpackShared/Core/TwinpackService.cs @@ -1,3 +1,5 @@ +using EnvDTE; +using EnvDTE80; using NLog; using NLog.Filters; using System; @@ -21,12 +23,15 @@ public class TwinpackService : INotifyPropertyChanged private PackageServerCollection _packageServers; private IAsyncEnumerator _availablePackagesIt; private string _searchTerm; + private Config _config; + private AutomationInterfaceService _automationInterfaceService; public event PropertyChangedEventHandler PropertyChanged; - public TwinpackService(PackageServerCollection packageServers) + public TwinpackService(PackageServerCollection packageServers, AutomationInterfaceService automationInterfaceService=null) { _packageServers = packageServers; + _automationInterfaceService = automationInterfaceService; } public void InvalidateCache() @@ -79,5 +84,36 @@ public async Task> RetrieveAvailablePackagesAsync(strin x.Name.IndexOf(searchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0) ; } + + public async Task> RetrieveInstalledPackagesAsync(Config config, string searchTerm = null, CancellationToken token = default) + { + var installedPackages = new List(); + + foreach (var project in config.Projects) + { + var packages = project.Plcs.SelectMany(x => x.Packages).Where(x => searchTerm == null || x.Name?.IndexOf(searchTerm) > 0); + + foreach (var package in packages) + { + CatalogItem catalogItem = await _packageServers.ResolvePackageAsync(project, package, _automationInterfaceService, token); + + installedPackages.RemoveAll(x => !string.IsNullOrEmpty(x.Name) && x.Name == catalogItem.Name); + installedPackages.Add(catalogItem); + + if (catalogItem.PackageServer == null) + _logger.Warn($"Package {package.Name} {package.Version} (distributor: {package.DistributorName}) referenced in the configuration can not be found on any package server"); + else + _logger.Info($"Package {package.Name} {package.Version} (distributor: {package.DistributorName}) located on {catalogItem.PackageServer.UrlBase}"); + } + } + + return installedPackages + .Where(x => + searchTerm == null || + x.DisplayName?.IndexOf(searchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0 || + x.DistributorName?.IndexOf(searchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0 || + x.Name.IndexOf(searchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0) + ; + } } } diff --git a/TwinpackShared/Models/Adapter.cs b/TwinpackShared/Models/Adapter.cs index 5e99e07..08343fe 100644 --- a/TwinpackShared/Models/Adapter.cs +++ b/TwinpackShared/Models/Adapter.cs @@ -37,6 +37,7 @@ public CatalogItem(ConfigPlcPackage package) Repository = package.Version; DistributorName = package.DistributorName; DisplayName = Name; + Configuration = package; IsPlaceholder = package.Version == null; } @@ -47,8 +48,11 @@ public CatalogItem(ConfigPlcPackage package) public string InstalledBranch { get { return Installed?.Branch; } } public string InstalledTarget { get { return Installed?.Target; } } public string InstalledConfiguration { get { return Installed?.Configuration; } } - public PackageVersionGetResponse Update { get; set; } + + PackageVersionGetResponse _update; + public PackageVersionGetResponse Update{ get; set; } public PackageVersionGetResponse Installed { get; set; } + public ConfigPlcPackage Configuration { get; set; } public bool IsUpdateable { diff --git a/TwinpackShared/Models/ConfigFactory.cs b/TwinpackShared/Models/ConfigFactory.cs index a03124e..f547ff4 100644 --- a/TwinpackShared/Models/ConfigFactory.cs +++ b/TwinpackShared/Models/ConfigFactory.cs @@ -137,8 +137,9 @@ public static async Task CreateFromSolutionAsync(EnvDTE.Solution solutio return config; } - public static async Task CreateFromSolutionFileAsync(string path = ".", bool continueWithoutSolution = false, Protocol.IPackageServer packageServer = null, IEnumerable plcTypeFilter=null, CancellationToken cancellationToken = default) + public static async Task CreateFromSolutionFileAsync(string path=".", bool continueWithoutSolution=false, IEnumerable packageServers=null, IEnumerable plcTypeFilter=null, CancellationToken cancellationToken = default) { + packageServers = packageServers == null ? new List() : packageServers; Config config = new Config(); var solutions = Directory.GetFiles(path, "*.sln", SearchOption.AllDirectories); @@ -190,7 +191,7 @@ public static async Task CreateFromSolutionFileAsync(string path = ".", foreach (var plc in project.Plcs) { - var plcConfig = await ConfigPlcProjectFactory.CreateAsync(plc.FilePath, packageServer, cancellationToken); + var plcConfig = await ConfigPlcProjectFactory.CreateAsync(plc.FilePath, packageServers, cancellationToken); if(plcTypeFilter == null || plcTypeFilter.Contains(plcConfig.PlcType)) projectConfig.Plcs.Add(plcConfig); diff --git a/TwinpackShared/TwinpackShared.projitems b/TwinpackShared/TwinpackShared.projitems index 03c6ace..a6e5154 100644 --- a/TwinpackShared/TwinpackShared.projitems +++ b/TwinpackShared/TwinpackShared.projitems @@ -9,6 +9,7 @@ TwinpackShared + diff --git a/TwinpackTests/ConfigFactoryTest.cs b/TwinpackTests/ConfigFactoryTest.cs index 2b0ff68..4f2aff0 100644 --- a/TwinpackTests/ConfigFactoryTest.cs +++ b/TwinpackTests/ConfigFactoryTest.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Twinpack.Models; +using Twinpack.Protocol; namespace TwinpackTests { diff --git a/TwinpackVsixShared/Dialogs/Converters/PackageVersionToBooleanConverter.cs b/TwinpackVsixShared/Dialogs/Converters/PackageVersionToBooleanConverter.cs new file mode 100644 index 0000000..82cb03a --- /dev/null +++ b/TwinpackVsixShared/Dialogs/Converters/PackageVersionToBooleanConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Windows.Data; +using System.Windows; +using System.Globalization; +using Twinpack.Models; + +namespace Twinpack.Dialogs +{ + public class PackageVersionToBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (value as PackageVersionGetResponse)?.Name != null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +}