From d409af1d2eef362f7734642f6936ff099a6ac4d8 Mon Sep 17 00:00:00 2001 From: alerickson Date: Wed, 14 Oct 2026 11:55:40 -0700 Subject: [PATCH 1/2] Add concurrent queues to methods --- src/code/InstallHelper.cs | 41 ++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 4d2f72c02..b8329e134 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -1068,7 +1068,7 @@ private bool TryInstallToTempPath( var tempDirNameVersion = Path.Combine(tempInstallPath, pkgName, pkgVersion); Directory.CreateDirectory(tempDirNameVersion); - if (!TryExtractToDirectory(pathToFile, tempDirNameVersion, out ErrorRecord error)) + if (!TryExtractToDirectory(pathToFile, tempDirNameVersion, warningMsgs, debugMsgs, out ErrorRecord error)) { Utils.EnqueueIfNotNull(errorMsgs, error); return false; @@ -1296,7 +1296,16 @@ private bool TrySaveNupkgToTempPath( /// - Root-level RID folder entries (e.g., win-x64/) based on the current platform's RID /// - lib/{tfm}/ entries to only extract the best matching Target Framework Moniker /// - private bool TryExtractToDirectory(string zipPath, string extractPath, out ErrorRecord error) + /// + /// This method may run on a background thread (via Parallel.ForEach), so it must not call + /// cmdlet Write* methods directly. Messages are enqueued and written out by the pipeline thread. + /// + private bool TryExtractToDirectory( + string zipPath, + string extractPath, + ConcurrentQueue warningMsgs, + ConcurrentQueue debugMsgs, + out ErrorRecord error) { error = null; // Normalize the path @@ -1321,24 +1330,24 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error bestLibFramework = NuGetFramework.ParseFolder(_targetFramework); if (bestLibFramework == null || bestLibFramework.IsUnsupported) { - _cmdletPassedIn.WriteDebug($"Could not parse specified TargetFramework '{_targetFramework}', falling back to auto-detection."); - bestLibFramework = GetBestLibFramework(archive); + debugMsgs.Enqueue($"Could not parse specified TargetFramework '{_targetFramework}', falling back to auto-detection."); + bestLibFramework = GetBestLibFramework(archive, debugMsgs); } else { - _cmdletPassedIn.WriteDebug($"Using user-specified TargetFramework: {bestLibFramework.GetShortFolderName()}"); + debugMsgs.Enqueue($"Using user-specified TargetFramework: {bestLibFramework.GetShortFolderName()}"); } } else { - bestLibFramework = GetBestLibFramework(archive); + bestLibFramework = GetBestLibFramework(archive, debugMsgs); } // Warn if TFM filtering is skipping assemblies for a different .NET lineage // (e.g., installing on PS7/.NET 8 but package also has net472 for WinPS 5.1) if (bestLibFramework != null) { - WarnIfCrossLineageTfmSkipped(archive, bestLibFramework); + WarnIfCrossLineageTfmSkipped(archive, bestLibFramework, warningMsgs); } foreach (ZipArchiveEntry entry in archive.Entries.Where(entry => entry.CompressedLength > 0)) @@ -1351,7 +1360,7 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error if (!includeEntry) { - _cmdletPassedIn.WriteDebug($"Skipping runtime entry not matching target platform: {entry.FullName}"); + debugMsgs.Enqueue($"Skipping runtime entry not matching target platform: {entry.FullName}"); continue; } } @@ -1359,7 +1368,7 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error // TFM filtering: for lib/ entries, only extract the best matching TFM if (bestLibFramework != null && !ShouldIncludeLibEntry(entry.FullName, bestLibFramework)) { - _cmdletPassedIn.WriteDebug($"Skipping lib entry not matching target framework: {entry.FullName}"); + debugMsgs.Enqueue($"Skipping lib entry not matching target framework: {entry.FullName}"); continue; } @@ -1412,7 +1421,7 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error .Select(s => s[1]) .Distinct(StringComparer.OrdinalIgnoreCase); string available = string.Join(", ", availableTfms); - _cmdletPassedIn.WriteWarning( + warningMsgs.Enqueue( $"The specified TargetFramework '{_targetFramework}' was not found in this package. " + $"No lib/ assemblies were installed. Available TFMs: {(string.IsNullOrEmpty(available) ? "none" : available)}"); } @@ -1427,7 +1436,7 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error if (!hasMatchingRid) { string available = string.Join(", ", availableRids); - _cmdletPassedIn.WriteWarning( + warningMsgs.Enqueue( $"The specified RuntimeIdentifier '{_runtimeIdentifier}' was not found in this package. " + $"No platform-specific assets were installed. Available RIDs: {(string.IsNullOrEmpty(available) ? "none" : available)}"); } @@ -1454,7 +1463,7 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error /// /// The zip archive to analyze. /// The best matching NuGetFramework, or null if no lib/ folders exist or no match is found. - private NuGetFramework GetBestLibFramework(ZipArchive archive) + private NuGetFramework GetBestLibFramework(ZipArchive archive, ConcurrentQueue debugMsgs) { // Collect all TFMs from lib/ folder entries var libFrameworks = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -1504,14 +1513,14 @@ private NuGetFramework GetBestLibFramework(ZipArchive archive) if (bestMatch != null) { - _cmdletPassedIn.WriteDebug($"Selected best matching TFM: {bestMatch.GetShortFolderName()} (from {string.Join(", ", libFrameworks)})"); + debugMsgs.Enqueue($"Selected best matching TFM: {bestMatch.GetShortFolderName()} (from {string.Join(", ", libFrameworks)})"); } return bestMatch; } catch (Exception e) { - _cmdletPassedIn.WriteDebug($"TFM selection failed, extracting all lib/ folders: {e.Message}"); + debugMsgs.Enqueue($"TFM selection failed, extracting all lib/ folders: {e.Message}"); return null; } } @@ -1609,7 +1618,7 @@ private static NuGetFramework GetCurrentFramework() /// .NET Framework assemblies (net472), those will be skipped — which means the module /// won't work if later used on Windows PowerShell 5.1. /// - private void WarnIfCrossLineageTfmSkipped(ZipArchive archive, NuGetFramework bestFramework) + private void WarnIfCrossLineageTfmSkipped(ZipArchive archive, NuGetFramework bestFramework, ConcurrentQueue warningMsgs) { try { @@ -1653,7 +1662,7 @@ private void WarnIfCrossLineageTfmSkipped(ZipArchive archive, NuGetFramework bes string skippedList = string.Join(", ", skippedLineageTfms); string otherHost = bestIsNetCore ? "Windows PowerShell 5.1" : "PowerShell 7+"; string skippedTfm = skippedLineageTfms.First(); - _cmdletPassedIn.WriteWarning( + warningMsgs.Enqueue( $"This package contains assemblies for {skippedList} which were not installed because " + $"the current runtime selected {bestFramework.GetShortFolderName()}. " + $"If you also use this module on {otherHost}, run: Install-PSResource -Name -TargetFramework '{skippedTfm}'"); From cf0c20fa7a9f07f8a42fd80a56990f97f0dd4ef6 Mon Sep 17 00:00:00 2001 From: alerickson Date: Wed, 14 Oct 2026 12:12:43 -0700 Subject: [PATCH 2/2] fix dependency bug --- src/code/InstallHelper.cs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index b8329e134..de5e97f40 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -874,8 +874,27 @@ private ConcurrentDictionary DownloadParentAndDeps( if (!skipDependencyCheck) { // List returned only includes dependencies, so we'll add the parent pkg to this list to pass on to installation method. - parentAndDeps.AddRange(_findHelper.FindDependencyPackages(currentServer, currentResponseUtil, pkgToInstall, repository, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); + IEnumerable depPkgs = _findHelper.FindDependencyPackages(currentServer, currentResponseUtil, pkgToInstall, repository, errorMsgs, warningMsgs, debugMsgs, verboseMsgs); debugMsgs.Enqueue("In InstallHelper::DownloadParentAndDeps(), found all dependencies"); + + // Do not reinstall dependencies that are already installed, unless -Reinstall was specified or an explicit + // -TargetFramework/-RuntimeIdentifier requires re-downloading to merge additional platform content. + // _packagesOnMachine is only mutated on the pipeline thread after this parallel phase, so reading it here is safe. + bool skipInstalledDeps = !_reinstall && string.IsNullOrEmpty(_targetFramework) && string.IsNullOrEmpty(_runtimeIdentifier); + foreach (PSResourceInfo depPkg in depPkgs) + { + if (skipInstalledDeps && _packagesOnMachine != null) + { + string depPkgVersion = string.IsNullOrEmpty(depPkg.Prerelease) ? depPkg.Version.ToString() : $"{depPkg.Version}-{depPkg.Prerelease}"; + if (_packagesOnMachine.Contains($"{depPkg.Name}{depPkgVersion}")) + { + verboseMsgs.Enqueue($"Dependency '{depPkg.Name}' with version '{depPkgVersion}' is already installed."); + continue; + } + } + + parentAndDeps.Add(depPkg); + } } parentAndDeps.Add(pkgToInstall);