From ba2d230d4c9585af3e65cd80cc1974988474ab51 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:56:54 -0700 Subject: [PATCH 01/11] Implement unimplemented concurrency methods for all server classes --- src/code/ContainerRegistryServerAPICalls.cs | 53 ++++- src/code/FindHelper.cs | 88 +++------ src/code/InstallHelper.cs | 4 +- src/code/LocalServerApiCalls.cs | 65 ++++++- src/code/NuGetServerAPICalls.cs | 53 ++++- src/code/V3ServerAPICalls.cs | 205 +++++++++++++++++++- 6 files changed, 388 insertions(+), 80 deletions(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index b7af3b98d..1b011c884 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -82,14 +82,40 @@ public ContainerRegistryServerAPICalls(PSRepositoryInfo repository, PSCmdlet cmd #region Overridden Methods + /// + /// Async find method which allows for searching for single name with specific version. + /// Name: no wildcard support + /// Version: no wildcard support + /// This is the concurrent (parallel) counterpart of FindVersion(). + /// public override Task FindVersionAsync(string packageName, string version, ResourceType type, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("FindVersionAsync is not implemented for ContainerRegistryServerAPICalls."); + debugMsgs.Enqueue("In ContainerRegistryServerAPICalls::FindVersionAsync()"); + FindResults findResponse = FindVersion(packageName, version, type, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } + /// + /// Async find method which allows for searching for single name with version range. + /// Name: no wildcard support + /// Version: supports wildcards + /// This is the concurrent (parallel) counterpart of FindVersionGlobbing(). + /// public override Task FindVersionGlobbingAsync(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("FindVersionGlobbingAsync is not implemented for ContainerRegistryServerAPICalls."); + debugMsgs.Enqueue("In ContainerRegistryServerAPICalls::FindVersionGlobbingAsync()"); + FindResults findResponse = FindVersionGlobbing(packageName, versionRange, includePrerelease, type, getOnlyLatest, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } /// @@ -158,9 +184,21 @@ public override FindResults FindName(string packageName, bool includePrerelease, } + /// + /// Async find method which allows for searching for single name and returns latest version. + /// Name: no wildcard support + /// This is the concurrent (parallel) counterpart of FindName(). + /// public override Task FindNameAsync(string packageName, bool includePrerelease, ResourceType type, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("FindNameAsync is not implemented for ContainerRegistryServerAPICalls."); + debugMsgs.Enqueue("In ContainerRegistryServerAPICalls::FindNameAsync()"); + FindResults findResponse = FindName(packageName, includePrerelease, type, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } /// @@ -329,7 +367,14 @@ public override Stream InstallPackage(string packageName, string packageVersion, /// public override Task InstallPackageAsync(string packageName, string packageVersion, bool includePrerelease, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("FindNameAsync is not implemented for ContainerRegistryServerAPICalls."); + debugMsgs.Enqueue("In ContainerRegistryServerAPICalls::InstallPackageAsync()"); + Stream results = InstallPackage(packageName, packageVersion, includePrerelease, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(results); } /// diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index 1a074b67e..fb5c9a28d 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -912,17 +912,12 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R ConcurrentDictionary> cachedNetworkCalls = new ConcurrentDictionary>(); Task response = null; - if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.V2) { - string key = $"{pkgName}|{_nugetVersion.ToNormalizedString()}|{_type}"; - response = cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionAsync(pkgName, _nugetVersion.ToNormalizedString(), _type, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); - - responses = response.GetAwaiter().GetResult(); + string key = $"{pkgName}|{_nugetVersion.ToNormalizedString()}|{_type}"; + response = cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionAsync(pkgName, _nugetVersion.ToNormalizedString(), _type, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); + + responses = response.GetAwaiter().GetResult(); - Utils.WriteOutConcurrentQueue(_cmdletPassedIn, errorMsgs, warningMsgs, debugMsgs, verboseMsgs); - } - else { - responses = currentServer.FindVersion(pkgName, _nugetVersion.ToNormalizedString(), _type, out errRecord); - } + Utils.WriteOutConcurrentQueue(_cmdletPassedIn, errorMsgs, warningMsgs, debugMsgs, verboseMsgs); } else { @@ -996,15 +991,10 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R { ConcurrentDictionary> cachedNetworkCalls = new ConcurrentDictionary>(); Task response = null; - if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.V2) { - string key = $"{pkgName}|{_versionRange.ToString()}|{_type}"; - response = cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionGlobbingAsync(pkgName, _versionRange, _prerelease, _type, getOnlyLatest: false, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); - - responses = response.GetAwaiter().GetResult(); - } - else { - responses = currentServer.FindVersionGlobbing(pkgName, _versionRange, _prerelease, _type, getOnlyLatest: false, out errRecord); - } + string key = $"{pkgName}|{_versionRange.ToString()}|{_type}"; + response = cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionGlobbingAsync(pkgName, _versionRange, _prerelease, _type, getOnlyLatest: false, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); + + responses = response.GetAwaiter().GetResult(); } else { @@ -1189,7 +1179,7 @@ internal void FindDependencyPackagesHelper(ServerApiCall currentServer, Response //const int PARALLEL_THRESHOLD = 5; // TODO: Trottle limit from user, defaults to 5; int processorCount = Environment.ProcessorCount; int maxDegreeOfParallelism = processorCount * 4; - if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.V2 && currentPkg.Dependencies.Length > processorCount) + if (currentPkg.Dependencies.Length > processorCount) { Parallel.ForEach(currentPkg.Dependencies, new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, dep => { @@ -1293,20 +1283,13 @@ private PSResourceInfo FindDependencyWithSpecificVersion( Task response = null; debugMsgs.Enqueue("In FindHelper::FindDependencyWithSpecificVersion()"); - if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.V2) - { - // See if the network call we're making is already cached, if not, call FindNameAsync() and cache results - string key = $"{dep.Name}|{dep.VersionRange.MaxVersion.ToString()}|{_type}"; - debugMsgs.Enqueue("Checking if network call is cached."); - response = _cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionAsync(dep.Name, dep.VersionRange.MaxVersion.ToString(), _type, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); - - responses = response.GetAwaiter().GetResult(); - } - else - { - responses = currentServer.FindVersion(dep.Name, dep.VersionRange.MaxVersion.ToString(), _type, out errRecord); - } - + + // See if the network call we're making is already cached, if not, call FindNameAsync() and cache results + string key = $"{dep.Name}|{dep.VersionRange.MaxVersion.ToString()}|{_type}"; + debugMsgs.Enqueue("Checking if network call is cached."); + response = _cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionAsync(dep.Name, dep.VersionRange.MaxVersion.ToString(), _type, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); + + responses = response.GetAwaiter().GetResult(); // Error handling and Convert to PSResource object if (errRecord != null) @@ -1369,19 +1352,12 @@ private PSResourceInfo FindDependencyWithLowerBound( Task response = null; debugMsgs.Enqueue("In FindHelper::FindDependencyWithLowerBound()"); - if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.V2) - { - // See if the network call we're making is already cached, if not, call FindNameAsync() and cache results - string key = $"{dep.Name}|*|{_type}"; - debugMsgs.Enqueue("Checking if network call is cached."); - response = _cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindNameAsync(dep.Name, includePrerelease: true, _type, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); - - responses = response.GetAwaiter().GetResult(); - } - else - { - responses = currentServer.FindName(dep.Name, includePrerelease: true, _type, out errRecord); - } + // See if the network call we're making is already cached, if not, call FindNameAsync() and cache results + string key = $"{dep.Name}|*|{_type}"; + debugMsgs.Enqueue("Checking if network call is cached."); + response = _cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindNameAsync(dep.Name, includePrerelease: true, _type, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); + + responses = response.GetAwaiter().GetResult(); // Error handling and Convert to PSResource object if (errRecord != null) @@ -1445,21 +1421,13 @@ private PSResourceInfo FindDependencyWithUpperBound( ConcurrentDictionary> cachedNetworkCalls = new ConcurrentDictionary>(); debugMsgs.Enqueue("In FindHelper::FindDependencyWithUpperBound()"); + // See if the network call we're making is already caced, if not, call FindNameAsync() and cache results + string key = $"{dep.Name}|{dep.VersionRange.MaxVersion.ToString()}|{_type}"; + debugMsgs.Enqueue("Checking if network call is cached."); + response = cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionGlobbingAsync(dep.Name, dep.VersionRange, includePrerelease: true, ResourceType.None, getOnlyLatest: true, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); - if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.V2) - { - // See if the network call we're making is already caced, if not, call FindNameAsync() and cache results - string key = $"{dep.Name}|{dep.VersionRange.MaxVersion.ToString()}|{_type}"; - debugMsgs.Enqueue("Checking if network call is cached."); - response = cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionGlobbingAsync(dep.Name, dep.VersionRange, includePrerelease: true, ResourceType.None, getOnlyLatest: true, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); - - responses = response.GetAwaiter().GetResult(); + responses = response.GetAwaiter().GetResult(); - } - else - { - responses = currentServer.FindVersionGlobbing(dep.Name, dep.VersionRange, includePrerelease: true, ResourceType.None, getOnlyLatest: true, out errRecord); - } // Error handling and Convert to PSResource object if (errRecord != null) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index c89ef0ee9..e3f95b616 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -801,7 +801,7 @@ private ConcurrentDictionary BeginPackageInstall( } else { - // Concurrent updates, currently only implemented for v2 server repositories + // Concurrent updates // Find all dependencies if (!skipDependencyCheck) { @@ -853,7 +853,7 @@ private ConcurrentDictionary InstallParentAndDependencyPackag // TODO: figure out a good threshold and parallel count int processorCount = Environment.ProcessorCount; _cmdletPassedIn.WriteDebug($"parentAndDeps.Count is {parentAndDeps.Count}, processor count is: {processorCount}"); - if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.V2 && parentAndDeps.Count > processorCount) + if (parentAndDeps.Count > processorCount) { _cmdletPassedIn.WriteDebug($"parentAndDeps.Count is greater than processor count"); // Set the maximum degree of parallelism to 32? (Invoke-Command has default of 32, that's where we got this number from) diff --git a/src/code/LocalServerApiCalls.cs b/src/code/LocalServerApiCalls.cs index a8e505acb..1ebb72dcb 100644 --- a/src/code/LocalServerApiCalls.cs +++ b/src/code/LocalServerApiCalls.cs @@ -41,14 +41,40 @@ public LocalServerAPICalls (PSRepositoryInfo repository, PSCmdlet cmdletPassedIn #region Overridden Methods + /// + /// Async find method which allows for searching for single name with specific version. + /// Name: no wildcard support + /// Version: no wildcard support + /// This is the concurrent (parallel) counterpart of FindVersion(). + /// public override Task FindVersionAsync(string packageName, string version, ResourceType type, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException(); + debugMsgs.Enqueue("In LocalServerApiCalls::FindVersionAsync()"); + FindResults findResponse = FindVersionHelper(packageName, version, tags: Utils.EmptyStrArray, type, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } + /// + /// Async find method which allows for searching for single name with version range. + /// Name: no wildcard support + /// Version: supports wildcards + /// This is the concurrent (parallel) counterpart of FindVersionGlobbing(). + /// public override Task FindVersionGlobbingAsync(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException(); + debugMsgs.Enqueue("In LocalServerApiCalls::FindVersionGlobbingAsync()"); + FindResults findResponse = FindVersionGlobbing(packageName, versionRange, includePrerelease, type, getOnlyLatest, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } /// /// Find method which allows for searching for all packages from a repository and returns latest version for each. @@ -124,9 +150,21 @@ public override FindResults FindName(string packageName, bool includePrerelease, return FindNameHelper(packageName, Utils.EmptyStrArray, includePrerelease, type, out errRecord); } + /// + /// Async find method which allows for searching for single name and returns latest version. + /// Name: no wildcard support + /// This is the concurrent (parallel) counterpart of FindName(). + /// public override Task FindNameAsync(string packageName, bool includePrerelease, ResourceType type, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException(); + debugMsgs.Enqueue("In LocalServerApiCalls::FindNameAsync()"); + FindResults findResponse = FindNameHelper(packageName, tags: Utils.EmptyStrArray, includePrerelease, type, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } /// @@ -278,7 +316,26 @@ public override Stream InstallPackage(string packageName, string packageVersion, /// public override Task InstallPackageAsync(string packageName, string packageVersion, bool includePrerelease, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("InstallPackageAsync is not implemented for LocalServerAPICalls."); + debugMsgs.Enqueue("In LocalServerApiCalls::InstallPackageAsync()"); + Stream results = new MemoryStream(); + if (string.IsNullOrEmpty(packageVersion)) + { + errorMsgs.Enqueue(new ErrorRecord( + exception: new ArgumentNullException($"Package version could not be found for {packageName}"), + "PackageVersionNullOrEmptyError", + ErrorCategory.InvalidArgument, + _cmdletPassedIn)); + + return Task.FromResult(results); + } + + results = InstallVersion(packageName, packageVersion, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(results); } #endregion diff --git a/src/code/NuGetServerAPICalls.cs b/src/code/NuGetServerAPICalls.cs index 1c6bb2828..cc4c68223 100644 --- a/src/code/NuGetServerAPICalls.cs +++ b/src/code/NuGetServerAPICalls.cs @@ -49,14 +49,40 @@ public NuGetServerAPICalls (PSRepositoryInfo repository, PSCmdlet cmdletPassedIn #region Overridden Methods + /// + /// Async find method which allows for searching for single name with specific version. + /// Name: no wildcard support + /// Version: no wildcard support + /// This is the concurrent (parallel) counterpart of FindVersion(). + /// public override Task FindVersionAsync(string packageName, string version, ResourceType type, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("FindVersionAsync is not implemented for NuGetServerAPICalls."); + debugMsgs.Enqueue("In NuGetServerAPICalls::FindVersionAsync()"); + FindResults findResponse = FindVersion(packageName, version, type, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } + /// + /// Async find method which allows for searching for single name with version range. + /// Name: no wildcard support + /// Version: supports wildcards + /// This is the concurrent (parallel) counterpart of FindVersionGlobbing(). + /// public override Task FindVersionGlobbingAsync(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("FindVersionGlobbingAsync is not implemented for NuGetServerAPICalls."); + debugMsgs.Enqueue("In NuGetServerAPICalls::FindVersionGlobbingAsync()"); + FindResults findResponse = FindVersionGlobbing(packageName, versionRange, includePrerelease, type, getOnlyLatest, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } /// /// Find method which allows for searching for all packages from a repository and returns latest version for each. @@ -193,9 +219,21 @@ public override FindResults FindName(string packageName, bool includePrerelease, return new FindResults(stringResponse: new string[]{ response }, hashtableResponse: emptyHashResponses, responseType: FindResponseType); } + /// + /// Async find method which allows for searching for single name and returns latest version. + /// Name: no wildcard support + /// This is the concurrent (parallel) counterpart of FindName(). + /// public override Task FindNameAsync(string packageName, bool includePrerelease, ResourceType type, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("FindNameAsync is not implemented for NuGetServerAPICalls."); + debugMsgs.Enqueue("In NuGetServerAPICalls::FindNameAsync()"); + FindResults findResponse = FindName(packageName, includePrerelease, type, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } /// @@ -471,7 +509,14 @@ public override Stream InstallPackage(string packageName, string packageVersion, /// public override Task InstallPackageAsync(string packageName, string packageVersion, bool includePrerelease, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("InstallPackageAsync is not implemented for NuGetServerAPICalls."); + debugMsgs.Enqueue("In NuGetServerAPICalls::InstallPackageAsync()"); + Stream results = InstallPackage(packageName, packageVersion, includePrerelease, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(results); } /// diff --git a/src/code/V3ServerAPICalls.cs b/src/code/V3ServerAPICalls.cs index a2beb1545..3693646a5 100644 --- a/src/code/V3ServerAPICalls.cs +++ b/src/code/V3ServerAPICalls.cs @@ -89,14 +89,43 @@ public V3ServerAPICalls(PSRepositoryInfo repository, PSCmdlet cmdletPassedIn, Ne #region Overridden Methods + /// + /// Async find method which allows for searching for single name with specific version. + /// Name: no wildcard support + /// Version: no wildcard support + /// Examples: Search "NuGet.Server.Core" "3.0.0-beta" + /// This is the concurrent (parallel) counterpart of FindVersion(). + /// public override Task FindVersionAsync(string packageName, string version, ResourceType type, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("FindVersionAsync is not implemented for V3ServerAPICalls."); + debugMsgs.Enqueue("In V3ServerAPICalls::FindVersionAsync()"); + FindResults findResponse = FindVersionHelper(packageName, version, tags: Utils.EmptyStrArray, type, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } + /// + /// Async find method which allows for searching for single name with version range. + /// Name: no wildcard support + /// Version: supports wildcards + /// Examples: Search "NuGet.Server.Core" "[1.0.0.0, 5.0.0.0]" + /// Search "NuGet.Server.Core" "3.*" + /// This is the concurrent (parallel) counterpart of FindVersionGlobbing(). + /// public override Task FindVersionGlobbingAsync(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("FindVersionAsync is not implemented for V3ServerAPICalls."); + debugMsgs.Enqueue("In V3ServerAPICalls::FindVersionGlobbingAsync()"); + FindResults findResponse = FindVersionGlobbing(packageName, versionRange, includePrerelease, type, getOnlyLatest, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } /// @@ -166,9 +195,22 @@ public override FindResults FindName(string packageName, bool includePrerelease, return FindNameHelper(packageName, tags: Utils.EmptyStrArray, includePrerelease, type, out errRecord); } + /// + /// Async find method which allows for searching for single name and returns latest version. + /// Name: no wildcard support + /// Examples: Search "Newtonsoft.Json" + /// This is the concurrent (parallel) counterpart of FindName(). + /// public override Task FindNameAsync(string packageName, bool includePrerelease, ResourceType type, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("FindVersionAsync is not implemented for V3ServerAPICalls."); + debugMsgs.Enqueue("In V3ServerAPICalls::FindNameAsync()"); + FindResults findResponse = FindNameHelper(packageName, tags: Utils.EmptyStrArray, includePrerelease, type, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + } + + return Task.FromResult(findResponse); } /// @@ -239,6 +281,7 @@ public override FindResults FindNameGlobbingWithTag(string packageName, string[] /// public override FindResults FindVersionGlobbing(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, out ErrorRecord errRecord) { + // TODO: pass in ConcurrentQueue for debug messages so this is thread-safe when called from FindVersionGlobbingAsync(). _cmdletPassedIn.WriteDebug("In V3ServerAPICalls::FindVersionGlobbing()"); string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out errRecord); if (errRecord != null) @@ -267,6 +310,7 @@ public override FindResults FindVersionGlobbing(string packageName, VersionRange if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion) && versionRange.Satisfies(pkgVersion)) { + // TODO: pass in ConcurrentQueue for debug messages so this is thread-safe when called from FindVersionGlobbingAsync(). ? _cmdletPassedIn.WriteDebug($"Package version parsed as '{pkgVersion}' satisfies the version range"); if (!pkgVersion.IsPrerelease || includePrerelease) { @@ -353,9 +397,34 @@ public override Stream InstallPackage(string packageName, string packageVersion, /// Examples: Install "PowerShellGet" -Version "3.5.0-alpha" /// Install "PowerShellGet" -Version "3.0.0" /// - public override Task InstallPackageAsync(string packageName, string packageVersion, bool includePrerelease, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) + public override async Task InstallPackageAsync(string packageName, string packageVersion, bool includePrerelease, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - throw new NotImplementedException("InstallPackageAsync is not implemented for NuGetServerAPICalls."); + debugMsgs.Enqueue("In V3ServerAPICalls::InstallPackageAsync()"); + Stream results = new MemoryStream(); + if (string.IsNullOrEmpty(packageVersion)) + { + errorMsgs.Enqueue(new ErrorRecord( + exception: new ArgumentNullException($"Package version could not be found for {packageName}"), + "PackageVersionNullOrEmptyError", + ErrorCategory.InvalidArgument, + this)); + + return results; + } + + if (!NuGetVersion.TryParse(packageVersion, out NuGetVersion requiredVersion)) + { + errorMsgs.Enqueue(new ErrorRecord( + new ArgumentException($"Version {packageVersion} to be installed is not a valid NuGet version."), + "InstallVersionFailure", + ErrorCategory.InvalidArgument, + this)); + + return results; + } + + results = await InstallHelperAsync(packageName, requiredVersion, errorMsgs, warningMsgs, debugMsgs, verboseMsgs); + return results; } #endregion @@ -514,6 +583,7 @@ private FindResults FindTagsFromNuGetRepo(string[] tags, bool includePrerelease, /// private FindResults FindNameHelper(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ErrorRecord errRecord) { + // TODO: pass in ConcurrentQueue for debug messages so this is thread-safe when called from FindNameAsync(). ? _cmdletPassedIn.WriteDebug("In V3ServerAPICalls::FindNameHelper()"); string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out errRecord); if (errRecord != null) @@ -553,6 +623,7 @@ private FindResults FindNameHelper(string packageName, string[] tags, bool inclu if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion)) { + // ? _cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{pkgVersion}'"); if (!pkgVersion.IsPrerelease || includePrerelease) { @@ -610,6 +681,7 @@ private FindResults FindNameHelper(string packageName, string[] tags, bool inclu /// private FindResults FindVersionHelper(string packageName, string version, string[] tags, ResourceType type, out ErrorRecord errRecord) { + // TODO: pass in ConcurrentQueue for debug messages so this is thread-safe when called from FindVersionAsync(). ? _cmdletPassedIn.WriteDebug("In V3ServerAPICalls::FindVersionHelper()"); if (!NuGetVersion.TryParse(version, out NuGetVersion requiredVersion)) { @@ -621,7 +693,7 @@ private FindResults FindVersionHelper(string packageName, string version, string return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - _cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{requiredVersion}'"); + //_cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{requiredVersion}'"); string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out errRecord); if (errRecord != null) @@ -830,6 +902,88 @@ private Stream InstallHelper(string packageName, NuGetVersion version, out Error return content.ReadAsStreamAsync().GetAwaiter().GetResult(); } + /// + /// Helper method that is called by InstallPackageAsync() + /// For InstallName() we want latest version installed (so version parameter passed in will be null), for InstallVersion() we want specified, non-null version installed. + /// This is the async counterpart of InstallHelper() used for concurrent (parallel) installation workflows. + /// + private async Task InstallHelperAsync(string packageName, NuGetVersion version, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) + { + debugMsgs.Enqueue("In V3ServerAPICalls::InstallHelperAsync()"); + Stream pkgStream = null; + bool getLatestVersion = true; + if (version != null) + { + getLatestVersion = false; + } + + string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, packageContentProperty, isSearch: false, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + return pkgStream; + } + + if (versionedResponses.Length == 0) + { + errorMsgs.Enqueue(new ErrorRecord( + new Exception($"Package with name '{packageName}' and version '{version}' could not be found in repository '{Repository.Name}'"), + "InstallFailure", + ErrorCategory.InvalidResult, + this)); + + return null; + } + + string pkgContentUrl = String.Empty; + if (getLatestVersion) + { + pkgContentUrl = versionedResponses[0]; + } + else + { + // loop through responses to find one containing required version + foreach (string response in versionedResponses) + { + // Response will be "packageContent" element value that looks like: "{packageBaseAddress}/{packageName}/{normalizedVersion}/{packageName}.{normalizedVersion}.nupkg" + // Ex: https://api.nuget.org/v3-flatcontainer/test_module/1.0.0/test_module.1.0.0.nupkg + if (response.Contains(version.ToNormalizedString())) + { + pkgContentUrl = response; + break; + } + } + } + + if (String.IsNullOrEmpty(pkgContentUrl)) + { + errorMsgs.Enqueue(new ErrorRecord( + new Exception($"Package with name '{packageName}' and version '{version}' could not be found in repository '{Repository.Name}'"), + "InstallFailure", + ErrorCategory.InvalidResult, + this)); + + return null; + } + + var content = await HttpRequestCallForContentAsync(pkgContentUrl, errorMsgs, warningMsgs, debugMsgs, verboseMsgs); + + if (content is null) + { + errorMsgs.Enqueue(new ErrorRecord( + new Exception($"No content was returned by repository '{Repository.Name}'"), + "InstallFailureContentNullv3Async", + ErrorCategory.InvalidResult, + this)); + + return new MemoryStream(); + } + + pkgStream = await content.ReadAsStreamAsync(); + + return pkgStream; + } + /// /// Gets the versioned package entries from the RegistrationsBaseUrl resource /// i.e when the package Name being searched for does not contain wildcard @@ -1060,6 +1214,7 @@ private string FindSearchQueryService(Dictionary resources, out /// private JsonElement[] GetMetadataElementFromIdLinkElement(JsonElement idLinkElement, string packageName, out string upperVersion, out ErrorRecord errRecord) { + // TODO: pass in ConcurrentQueue to write out debug message. Called from the concurrent install metadata chain so cmdlet methods cannot be used here. ? _cmdletPassedIn.WriteDebug("In V3ServerAPICalls::GetMetadataElementFromIdLinkElement()"); upperVersion = String.Empty; JsonElement[] innerItems = new JsonElement[]{}; @@ -1102,6 +1257,7 @@ private JsonElement[] GetMetadataElementFromIdLinkElement(JsonElement idLinkElem } else { + // TODO: pass in ConcurrentQueue to write out debug message. Called from the concurrent install metadata chain so cmdlet methods cannot be used here. ? _cmdletPassedIn.WriteDebug($"Package with name '{packageName}' did not have 'upper' property so package versions may not be in descending order."); } @@ -1228,6 +1384,7 @@ private string[] GetMetadataElementsFromResponse(string response, string propert } else { + // TODO: pass in ConcurrentQueue to write out debug message. Called from the concurrent install metadata chain so cmdlet methods cannot be used here. ? _cmdletPassedIn.WriteDebug($"Metadata for package with name '{packageName}' did not have inner 'items' or '@Id' properties."); } } @@ -1267,6 +1424,7 @@ private string[] GetMetadataElementsFromResponse(string response, string propert } else { + // TODO: pass in ConcurrentQueue to write out debug message. Called from the concurrent install metadata chain so cmdlet methods cannot be used here. ? _cmdletPassedIn.WriteDebug($"Metadata for package with name '{packageName}' was not of value kind type string or object."); } } @@ -1355,6 +1513,7 @@ private string[] GetVersionedResponsesFromRegistrationsResource(string registrat /// private bool IsLatestVersionFirstForSearch(string[] versionedResponses, out ErrorRecord errRecord) { + // TODO: pass in ConcurrentQueue for debug messages so this is thread-safe when reached from the async find methods. ? _cmdletPassedIn.WriteDebug("In V3ServerAPICalls::IsLatestVersionFirstForSearch()"); errRecord = null; bool latestVersionFirst = true; @@ -1675,6 +1834,40 @@ private HttpContent HttpRequestCallForContent(string requestUrlV3, out ErrorReco return content; } + /// + /// Helper method that makes the HTTP request for the V3 server protocol url passed in for install APIs asynchronously. + /// This is the async counterpart of HttpRequestCallForContent() used for concurrent (parallel) installation workflows. + /// + private async Task HttpRequestCallForContentAsync(string requestUrlV3, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) + { + debugMsgs.Enqueue("In V3ServerAPICalls::HttpRequestCallForContentAsync()"); + HttpContent content = null; + try + { + debugMsgs.Enqueue($"Request url is '{requestUrlV3}'"); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrlV3); + + content = await SendV3RequestForContentAsync(request, _sessionClient); + } + catch (Exception e) + { + errorMsgs.Enqueue(new ErrorRecord( + exception: e, + "HttpRequestCallForContentFailure", + ErrorCategory.InvalidResult, + this)); + + return null; + } + + if (string.IsNullOrEmpty(content?.ToString())) + { + debugMsgs.Enqueue("Response is empty"); + } + + return content; + } + /// /// Helper method called by HttpRequestCall() that makes the HTTP request for string response. /// From fbb940f572eb2b132d2bc196b0cb7ab8620d5e72 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:11:38 -0700 Subject: [PATCH 02/11] build fixes --- src/code/FindHelper.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index fb5c9a28d..693c00aca 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -904,15 +904,13 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R // Example: Find-PSResource -Name "Az" -Version "3.0.0.0" // Example: Find-PSResource -Name "Az" -Version "3.0.0.0" -Tag "Windows" _cmdletPassedIn.WriteDebug("Exact version and package name are specified"); - + string key = string.Empty; FindResults responses = null; if (_tag.Length == 0) { - - ConcurrentDictionary> cachedNetworkCalls = new ConcurrentDictionary>(); Task response = null; - string key = $"{pkgName}|{_nugetVersion.ToNormalizedString()}|{_type}"; + key = $"{pkgName}|{_nugetVersion.ToNormalizedString()}|{_type}"; response = cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionAsync(pkgName, _nugetVersion.ToNormalizedString(), _type, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); responses = response.GetAwaiter().GetResult(); @@ -1318,7 +1316,7 @@ private PSResourceInfo FindDependencyWithSpecificVersion( string pkgVersion = FormatPkgVersionString(depPkg); debugMsgs.Enqueue($"Found dependency '{depPkg.Name}' version '{pkgVersion}'"); - string key = $"{depPkg.Name}{pkgVersion}"; + key = $"{depPkg.Name}{pkgVersion}"; if (!depPkgsFound.ContainsKey(key)) { // Add pkg to collection of packages found then find dependencies @@ -1386,7 +1384,7 @@ private PSResourceInfo FindDependencyWithLowerBound( string pkgVersion = FormatPkgVersionString(depPkg); debugMsgs.Enqueue($"Found dependency '{depPkg.Name}' version '{pkgVersion}'"); - string key = $"{depPkg.Name}{pkgVersion}"; + key = $"{depPkg.Name}{pkgVersion}"; if (!depPkgsFound.ContainsKey(key)) { // Add pkg to collection of packages found then find dependencies @@ -1457,7 +1455,7 @@ private PSResourceInfo FindDependencyWithUpperBound( string pkgVersion = FormatPkgVersionString(depPkg); debugMsgs.Enqueue($"Found dependency '{depPkg.Name}' version '{pkgVersion}'"); - string key = $"{depPkg.Name}{pkgVersion}"; + key = $"{depPkg.Name}{pkgVersion}"; if (!depPkgsFound.ContainsKey(key)) { // Add pkg to collection of packages found then find dependencies From a4730af8fdc5f96f90637b3dbf1c4b36619ec575 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:15:37 +0000 Subject: [PATCH 03/11] Fix thread-safety in ContainerRegistryServerAPICalls.InstallPackageAsync InstallPackageAsync was calling the synchronous InstallPackage() which writes to _cmdletPassedIn.WriteDebug, causing cross-thread cmdlet stream writes when used from Parallel.ForEach. - Refactor InstallPackageAsync to not call InstallPackage(); inline the null-version check and enqueue all messages via the provided queues - Add a queue-aware InstallVersion overload that uses ConcurrentQueue parameters instead of _cmdletPassedIn.Write* calls, for use by the async install path - Keep the original InstallVersion(out ErrorRecord) overload intact for the synchronous path --- src/code/ContainerRegistryServerAPICalls.cs | 84 ++++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index 1b011c884..376c3c053 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -368,12 +368,20 @@ public override Stream InstallPackage(string packageName, string packageVersion, public override Task InstallPackageAsync(string packageName, string packageVersion, bool includePrerelease, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { debugMsgs.Enqueue("In ContainerRegistryServerAPICalls::InstallPackageAsync()"); - Stream results = InstallPackage(packageName, packageVersion, includePrerelease, out ErrorRecord errRecord); - if (errRecord != null) + Stream results = new MemoryStream(); + if (string.IsNullOrEmpty(packageVersion)) { - errorMsgs.Enqueue(errRecord); + errorMsgs.Enqueue(new ErrorRecord( + exception: new ArgumentNullException($"Package version could not be found for {packageName}"), + "PackageVersionNullOrEmptyError", + ErrorCategory.InvalidArgument, + _cmdletPassedIn)); + + return Task.FromResult(results); } + string packageNameForInstall = PrependMARPrefix(packageName); + results = InstallVersion(packageNameForInstall, packageVersion, errorMsgs, debugMsgs, verboseMsgs); return Task.FromResult(results); } @@ -445,6 +453,76 @@ private Stream InstallVersion( return responseContent.ReadAsStreamAsync().Result; } + /// + /// Installs a package with version specified using concurrent queues for output instead of cmdlet streams. + /// Used by the async install path to avoid cross-thread cmdlet stream writes. + /// + private Stream InstallVersion( + string packageName, + string packageVersion, + ConcurrentQueue errorMsgs, + ConcurrentQueue debugMsgs, + ConcurrentQueue verboseMsgs) + { + debugMsgs.Enqueue("In ContainerRegistryServerAPICalls::InstallVersion()"); + string packageNameLowercase = packageName.ToLower(); + string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + } + catch (Exception e) + { + errorMsgs.Enqueue(new ErrorRecord( + exception: e, + "InstallVersionTempDirCreationError", + ErrorCategory.InvalidResult, + _cmdletPassedIn)); + + return null; + } + + string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, isPushOperation: false, out ErrorRecord errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + return null; + } + + verboseMsgs.Enqueue($"Getting manifest for {packageNameLowercase} - {packageVersion}"); + var manifest = GetContainerRegistryRepositoryManifest(packageNameLowercase, packageVersion, containerRegistryAccessToken, out errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + return null; + } + string digest = GetDigestFromManifest(manifest, out errRecord); + if (errRecord != null) + { + errorMsgs.Enqueue(errRecord); + return null; + } + + verboseMsgs.Enqueue($"Downloading blob for {packageNameLowercase} - {packageVersion}"); + HttpContent responseContent; + try + { + responseContent = GetContainerRegistryBlobAsync(packageNameLowercase, digest, containerRegistryAccessToken).Result; + } + catch (Exception e) + { + errorMsgs.Enqueue(new ErrorRecord( + exception: e, + "InstallVersionGetContainerRegistryBlobAsyncError", + ErrorCategory.InvalidResult, + _cmdletPassedIn)); + + return null; + } + + return responseContent.ReadAsStreamAsync().Result; + } + #endregion #region Authentication and Token Methods From f800970c24f0e00a891ea5c04a2624c4b9caf43e Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:55:24 -0700 Subject: [PATCH 04/11] Fix copilot commit for container registry --- src/code/ContainerRegistryServerAPICalls.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index 376c3c053..cd22c1c5d 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -343,7 +343,6 @@ public override Stream InstallPackage(string packageName, string packageVersion, Stream results = new MemoryStream(); if (string.IsNullOrEmpty(packageVersion)) { - errRecord = new ErrorRecord( exception: new ArgumentNullException($"Package version could not be found for {packageName}"), "PackageVersionNullOrEmptyError", ErrorCategory.InvalidArgument, @@ -381,7 +380,7 @@ public override Task InstallPackageAsync(string packageName, string pack } string packageNameForInstall = PrependMARPrefix(packageName); - results = InstallVersion(packageNameForInstall, packageVersion, errorMsgs, debugMsgs, verboseMsgs); + results = InstallVersionAsync(packageNameForInstall, packageVersion, errorMsgs, debugMsgs, verboseMsgs); return Task.FromResult(results); } @@ -457,14 +456,14 @@ private Stream InstallVersion( /// Installs a package with version specified using concurrent queues for output instead of cmdlet streams. /// Used by the async install path to avoid cross-thread cmdlet stream writes. /// - private Stream InstallVersion( + private Stream InstallVersionAsync( string packageName, string packageVersion, ConcurrentQueue errorMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { - debugMsgs.Enqueue("In ContainerRegistryServerAPICalls::InstallVersion()"); + debugMsgs.Enqueue("In ContainerRegistryServerAPICalls::InstallVersionAsync()"); string packageNameLowercase = packageName.ToLower(); string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); try From 6ab7367f660b33782e1ebabcbed9d06117ad322a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:01:27 +0000 Subject: [PATCH 05/11] Avoid cmdlet stream writes in NuGet FindVersionAsync path --- src/code/NuGetServerAPICalls.cs | 62 ++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/code/NuGetServerAPICalls.cs b/src/code/NuGetServerAPICalls.cs index cc4c68223..d8c6b5ffe 100644 --- a/src/code/NuGetServerAPICalls.cs +++ b/src/code/NuGetServerAPICalls.cs @@ -58,7 +58,18 @@ public NuGetServerAPICalls (PSRepositoryInfo repository, PSCmdlet cmdletPassedIn public override Task FindVersionAsync(string packageName, string version, ResourceType type, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { debugMsgs.Enqueue("In NuGetServerAPICalls::FindVersionAsync()"); - FindResults findResponse = FindVersion(packageName, version, type, out ErrorRecord errRecord); + var queryBuilder = new NuGetV2QueryBuilder(new Dictionary{ + { "id", $"'{packageName}'" }, + }); + var filterBuilder = queryBuilder.FilterBuilder; + + // We need to explicitly add 'Id eq ' whenever $filter is used, otherwise arbitrary results are returned. + filterBuilder.AddCriterion($"Id eq '{packageName}'"); + filterBuilder.AddCriterion($"NormalizedVersion eq '{packageName}'"); + + var requestUrl = $"{Repository.Uri}/FindPackagesById()?{queryBuilder.BuildQueryString()}"; + string response = HttpRequestCallAsync(requestUrl, debugMsgs, out ErrorRecord errRecord); + FindResults findResponse = new FindResults(stringResponse: new string[] { response }, hashtableResponse: emptyHashResponses, responseType: FindResponseType); if (errRecord != null) { errorMsgs.Enqueue(errRecord); @@ -617,6 +628,55 @@ private HttpContent HttpRequestCallForContent(string requestUrl, out ErrorRecord return content; } + /// + /// Helper method that makes the HTTP request for the NuGet server protocol url passed in for async find APIs. + /// This helper writes diagnostics to the provided debug queue and avoids cmdlet stream writes. + /// + private string HttpRequestCallAsync(string requestUrl, ConcurrentQueue debugMsgs, out ErrorRecord errRecord) + { + debugMsgs.Enqueue("In NuGetServerAPICalls::HttpRequestCallAsync()"); + errRecord = null; + string response = string.Empty; + + try + { + debugMsgs.Enqueue($"Request url is: '{requestUrl}'"); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + response = SendRequestAsync(request, _sessionClient).GetAwaiter().GetResult(); + } + catch (HttpRequestException e) + { + errRecord = new ErrorRecord( + exception: e, + "HttpRequestFallFailure", + ErrorCategory.ConnectionError, + this); + } + catch (ArgumentNullException e) + { + errRecord = new ErrorRecord( + exception: e, + "HttpRequestFallFailure", + ErrorCategory.ConnectionError, + this); + } + catch (InvalidOperationException e) + { + errRecord = new ErrorRecord( + exception: e, + "HttpRequestFallFailure", + ErrorCategory.ConnectionError, + this); + } + + if (string.IsNullOrEmpty(response)) + { + debugMsgs.Enqueue("Response is empty"); + } + + return response; + } + #endregion #region Private Methods From dbd1689eec89fe96cd08651ae48da755d49f8563 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:13:00 +0000 Subject: [PATCH 06/11] Fix V3 async helper logging to use debug queues --- src/code/V3ServerAPICalls.cs | 61 +++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/src/code/V3ServerAPICalls.cs b/src/code/V3ServerAPICalls.cs index 3693646a5..9df736d7e 100644 --- a/src/code/V3ServerAPICalls.cs +++ b/src/code/V3ServerAPICalls.cs @@ -99,7 +99,7 @@ public V3ServerAPICalls(PSRepositoryInfo repository, PSCmdlet cmdletPassedIn, Ne public override Task FindVersionAsync(string packageName, string version, ResourceType type, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { debugMsgs.Enqueue("In V3ServerAPICalls::FindVersionAsync()"); - FindResults findResponse = FindVersionHelper(packageName, version, tags: Utils.EmptyStrArray, type, out ErrorRecord errRecord); + FindResults findResponse = FindVersionHelper(packageName, version, tags: Utils.EmptyStrArray, type, out ErrorRecord errRecord, debugMsgs); if (errRecord != null) { errorMsgs.Enqueue(errRecord); @@ -119,7 +119,7 @@ public override Task FindVersionAsync(string packageName, string ve public override Task FindVersionGlobbingAsync(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { debugMsgs.Enqueue("In V3ServerAPICalls::FindVersionGlobbingAsync()"); - FindResults findResponse = FindVersionGlobbing(packageName, versionRange, includePrerelease, type, getOnlyLatest, out ErrorRecord errRecord); + FindResults findResponse = FindVersionGlobbingHelper(packageName, versionRange, includePrerelease, type, getOnlyLatest, out ErrorRecord errRecord, debugMsgs); if (errRecord != null) { errorMsgs.Enqueue(errRecord); @@ -204,7 +204,7 @@ public override FindResults FindName(string packageName, bool includePrerelease, public override Task FindNameAsync(string packageName, bool includePrerelease, ResourceType type, ConcurrentQueue errorMsgs, ConcurrentQueue warningMsgs, ConcurrentQueue debugMsgs, ConcurrentQueue verboseMsgs) { debugMsgs.Enqueue("In V3ServerAPICalls::FindNameAsync()"); - FindResults findResponse = FindNameHelper(packageName, tags: Utils.EmptyStrArray, includePrerelease, type, out ErrorRecord errRecord); + FindResults findResponse = FindNameHelper(packageName, tags: Utils.EmptyStrArray, includePrerelease, type, out ErrorRecord errRecord, debugMsgs); if (errRecord != null) { errorMsgs.Enqueue(errRecord); @@ -281,9 +281,13 @@ public override FindResults FindNameGlobbingWithTag(string packageName, string[] /// public override FindResults FindVersionGlobbing(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, out ErrorRecord errRecord) { - // TODO: pass in ConcurrentQueue for debug messages so this is thread-safe when called from FindVersionGlobbingAsync(). - _cmdletPassedIn.WriteDebug("In V3ServerAPICalls::FindVersionGlobbing()"); - string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out errRecord); + return FindVersionGlobbingHelper(packageName, versionRange, includePrerelease, type, getOnlyLatest, out errRecord); + } + + private FindResults FindVersionGlobbingHelper(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, out ErrorRecord errRecord, ConcurrentQueue debugMsgs = null) + { + WriteDebug("In V3ServerAPICalls::FindVersionGlobbing()", debugMsgs); + string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out errRecord, debugMsgs); if (errRecord != null) { return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); @@ -310,8 +314,7 @@ public override FindResults FindVersionGlobbing(string packageName, VersionRange if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion) && versionRange.Satisfies(pkgVersion)) { - // TODO: pass in ConcurrentQueue for debug messages so this is thread-safe when called from FindVersionGlobbingAsync(). ? - _cmdletPassedIn.WriteDebug($"Package version parsed as '{pkgVersion}' satisfies the version range"); + WriteDebug($"Package version parsed as '{pkgVersion}' satisfies the version range", debugMsgs); if (!pkgVersion.IsPrerelease || includePrerelease) { satisfyingVersions.Add(response); @@ -581,11 +584,10 @@ private FindResults FindTagsFromNuGetRepo(string[] tags, bool includePrerelease, /// /// Helper method called by FindName() and FindNameWithTag() /// - private FindResults FindNameHelper(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ErrorRecord errRecord) + private FindResults FindNameHelper(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ErrorRecord errRecord, ConcurrentQueue debugMsgs = null) { - // TODO: pass in ConcurrentQueue for debug messages so this is thread-safe when called from FindNameAsync(). ? - _cmdletPassedIn.WriteDebug("In V3ServerAPICalls::FindNameHelper()"); - string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out errRecord); + WriteDebug("In V3ServerAPICalls::FindNameHelper()", debugMsgs); + string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out errRecord, debugMsgs); if (errRecord != null) { return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); @@ -623,8 +625,7 @@ private FindResults FindNameHelper(string packageName, string[] tags, bool inclu if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion)) { - // ? - _cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{pkgVersion}'"); + WriteDebug($"'{packageName}' version parsed as '{pkgVersion}'", debugMsgs); if (!pkgVersion.IsPrerelease || includePrerelease) { // Versions are always in descending order i.e 5.0.0, 3.0.0, 1.0.0 so grabbing the first match suffices @@ -679,10 +680,9 @@ private FindResults FindNameHelper(string packageName, string[] tags, bool inclu /// /// Helper method called by FindVersion() and FindVersionWithTag() /// - private FindResults FindVersionHelper(string packageName, string version, string[] tags, ResourceType type, out ErrorRecord errRecord) + private FindResults FindVersionHelper(string packageName, string version, string[] tags, ResourceType type, out ErrorRecord errRecord, ConcurrentQueue debugMsgs = null) { - // TODO: pass in ConcurrentQueue for debug messages so this is thread-safe when called from FindVersionAsync(). ? - _cmdletPassedIn.WriteDebug("In V3ServerAPICalls::FindVersionHelper()"); + WriteDebug("In V3ServerAPICalls::FindVersionHelper()", debugMsgs); if (!NuGetVersion.TryParse(version, out NuGetVersion requiredVersion)) { errRecord = new ErrorRecord( @@ -695,7 +695,7 @@ private FindResults FindVersionHelper(string packageName, string version, string } //_cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{requiredVersion}'"); - string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out errRecord); + string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out errRecord, debugMsgs); if (errRecord != null) { return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); @@ -989,7 +989,7 @@ private async Task InstallHelperAsync(string packageName, NuGetVersion v /// i.e when the package Name being searched for does not contain wildcard /// This is called by FindNameHelper(), FindVersionHelper(), FindVersionGlobbing(), InstallHelper() /// - private string[] GetVersionedPackageEntriesFromRegistrationsResource(string packageName, string propertyName, bool isSearch, out ErrorRecord errRecord) + private string[] GetVersionedPackageEntriesFromRegistrationsResource(string packageName, string propertyName, bool isSearch, out ErrorRecord errRecord, ConcurrentQueue debugMsgs = null) { // TODO: pass in ConcurrentQueue to write out debug message. //_cmdletPassedIn.WriteDebug("In V3ServerAPICalls::GetVersionedPackageEntriesFromRegistrationsResource()"); @@ -1006,7 +1006,7 @@ private string[] GetVersionedPackageEntriesFromRegistrationsResource(string pack return responses; } - responses = GetVersionedResponsesFromRegistrationsResource(registrationsBaseUrl, packageName, propertyName, isSearch, out errRecord); + responses = GetVersionedResponsesFromRegistrationsResource(registrationsBaseUrl, packageName, propertyName, isSearch, out errRecord, debugMsgs); if (errRecord != null) { return Utils.EmptyStrArray; @@ -1452,7 +1452,7 @@ private string[] GetMetadataElementsFromResponse(string response, string propert /// The "packageContent" property is used for download, and the value is a URI for the .nupkg file. /// /// - private string[] GetVersionedResponsesFromRegistrationsResource(string registrationsBaseUrl, string packageName, string property, bool isSearch, out ErrorRecord errRecord) + private string[] GetVersionedResponsesFromRegistrationsResource(string registrationsBaseUrl, string packageName, string property, bool isSearch, out ErrorRecord errRecord, ConcurrentQueue debugMsgs = null) { // TODO: pass in ConcurrentQueue to write out debug message. //_cmdletPassedIn.WriteDebug("In V3ServerAPICalls::GetVersionedResponsesFromRegistrationsResource()"); @@ -1490,7 +1490,7 @@ private string[] GetVersionedResponsesFromRegistrationsResource(string registrat if (isSearch) { - if (!IsLatestVersionFirstForSearch(versionedResponseArr, out errRecord)) + if (!IsLatestVersionFirstForSearch(versionedResponseArr, out errRecord, debugMsgs)) { Array.Reverse(versionedResponseArr); } @@ -1511,10 +1511,9 @@ private string[] GetVersionedResponsesFromRegistrationsResource(string registrat /// ADO feeds usually return version entries in descending order, but Nuget.org repository returns them in ascending order. /// Package versions will reflect prerelease preference, but upper version and lower version would not so we don't use them for comparison. /// - private bool IsLatestVersionFirstForSearch(string[] versionedResponses, out ErrorRecord errRecord) + private bool IsLatestVersionFirstForSearch(string[] versionedResponses, out ErrorRecord errRecord, ConcurrentQueue debugMsgs = null) { - // TODO: pass in ConcurrentQueue for debug messages so this is thread-safe when reached from the async find methods. ? - _cmdletPassedIn.WriteDebug("In V3ServerAPICalls::IsLatestVersionFirstForSearch()"); + WriteDebug("In V3ServerAPICalls::IsLatestVersionFirstForSearch()", debugMsgs); errRecord = null; bool latestVersionFirst = true; int versionResponsesCount = versionedResponses.Length; @@ -1606,6 +1605,18 @@ private bool IsLatestVersionFirstForSearch(string[] versionedResponses, out Erro return latestVersionFirst; } + private void WriteDebug(string message, ConcurrentQueue debugMsgs = null) + { + if (debugMsgs == null) + { + _cmdletPassedIn.WriteDebug(message); + } + else + { + debugMsgs.Enqueue(message); + } + } + /// /// Returns true if the nupkg URI entries for each package version are arranged in descending order with respect to the package's version. /// ADO feeds usually return version entries in descending order, but Nuget.org repository returns them in ascending order. From efb000d090039941dc343a493aabf685af192922 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:24:52 -0700 Subject: [PATCH 07/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/code/FindHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index 693c00aca..0410af585 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -1419,7 +1419,7 @@ private PSResourceInfo FindDependencyWithUpperBound( ConcurrentDictionary> cachedNetworkCalls = new ConcurrentDictionary>(); debugMsgs.Enqueue("In FindHelper::FindDependencyWithUpperBound()"); - // See if the network call we're making is already caced, if not, call FindNameAsync() and cache results + // See if the network call we're making is already cached, if not, call FindNameAsync() and cache results string key = $"{dep.Name}|{dep.VersionRange.MaxVersion.ToString()}|{_type}"; debugMsgs.Enqueue("Checking if network call is cached."); response = cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionGlobbingAsync(dep.Name, dep.VersionRange, includePrerelease: true, ResourceType.None, getOnlyLatest: true, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); From 0e9c684f2d7d46ce3bfc9f754e791a6b1ae6221a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:28:13 +0000 Subject: [PATCH 08/11] Fix specific-version async dependency error handling path --- src/code/FindHelper.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index 0410af585..f46b843b6 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -1282,12 +1282,10 @@ private PSResourceInfo FindDependencyWithSpecificVersion( debugMsgs.Enqueue("In FindHelper::FindDependencyWithSpecificVersion()"); - // See if the network call we're making is already cached, if not, call FindNameAsync() and cache results + // Call FindVersionAsync() for dependency with specific version. string key = $"{dep.Name}|{dep.VersionRange.MaxVersion.ToString()}|{_type}"; - debugMsgs.Enqueue("Checking if network call is cached."); - response = _cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionAsync(dep.Name, dep.VersionRange.MaxVersion.ToString(), _type, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); - - responses = response.GetAwaiter().GetResult(); + responses = currentServer.FindVersionAsync(dep.Name, dep.VersionRange.MaxVersion.ToString(), _type, errorMsgs, warningMsgs, debugMsgs, verboseMsgs).GetAwaiter().GetResult(); + errorMsgs.TryPeek(out errRecord); // Error handling and Convert to PSResource object if (errRecord != null) From e3c1af50c786662e0a7ef62c3860a4edf9f35c90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:32:20 +0000 Subject: [PATCH 09/11] Scope async dependency queue handling to current operation --- src/code/FindHelper.cs | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index f46b843b6..0e4c53865 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -1278,14 +1278,41 @@ private PSResourceInfo FindDependencyWithSpecificVersion( PSResourceInfo depPkg = null; ErrorRecord errRecord = null; FindResults responses = null; - Task response = null; debugMsgs.Enqueue("In FindHelper::FindDependencyWithSpecificVersion()"); - + ConcurrentQueue operationErrorMsgs = new ConcurrentQueue(); + ConcurrentQueue operationWarningMsgs = new ConcurrentQueue(); + ConcurrentQueue operationDebugMsgs = new ConcurrentQueue(); + ConcurrentQueue operationVerboseMsgs = new ConcurrentQueue(); + // Call FindVersionAsync() for dependency with specific version. string key = $"{dep.Name}|{dep.VersionRange.MaxVersion.ToString()}|{_type}"; - responses = currentServer.FindVersionAsync(dep.Name, dep.VersionRange.MaxVersion.ToString(), _type, errorMsgs, warningMsgs, debugMsgs, verboseMsgs).GetAwaiter().GetResult(); - errorMsgs.TryPeek(out errRecord); + responses = currentServer.FindVersionAsync(dep.Name, dep.VersionRange.MaxVersion.ToString(), _type, operationErrorMsgs, operationWarningMsgs, operationDebugMsgs, operationVerboseMsgs).GetAwaiter().GetResult(); + + while (operationErrorMsgs.TryDequeue(out ErrorRecord queuedError)) + { + if (errRecord == null) + { + errRecord = queuedError; + } + + errorMsgs.Enqueue(queuedError); + } + + while (operationWarningMsgs.TryDequeue(out string queuedWarning)) + { + warningMsgs.Enqueue(queuedWarning); + } + + while (operationDebugMsgs.TryDequeue(out string queuedDebug)) + { + debugMsgs.Enqueue(queuedDebug); + } + + while (operationVerboseMsgs.TryDequeue(out string queuedVerbose)) + { + verboseMsgs.Enqueue(queuedVerbose); + } // Error handling and Convert to PSResource object if (errRecord != null) From 9b92e77a6ec610b222e488f3946a3a6b7ec3a574 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:38:00 +0000 Subject: [PATCH 10/11] Fix version-range async path: reset errRecord and flush concurrent queues --- src/code/FindHelper.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index 0e4c53865..3d149ab02 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -984,6 +984,7 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R // Example: Find-PSResource -Name "Az" -Version "[1.0.0.0, 3.0.0.0]" _cmdletPassedIn.WriteDebug("Version range and package name are specified"); + errRecord = null; FindResults responses = null; if (_tag.Length == 0) { @@ -993,6 +994,8 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R response = cachedNetworkCalls.GetOrAdd(key, _ => currentServer.FindVersionGlobbingAsync(pkgName, _versionRange, _prerelease, _type, getOnlyLatest: false, errorMsgs, warningMsgs, debugMsgs, verboseMsgs)); responses = response.GetAwaiter().GetResult(); + + Utils.WriteOutConcurrentQueue(_cmdletPassedIn, errorMsgs, warningMsgs, debugMsgs, verboseMsgs); } else { From 900db222e7dba80c8ed6e6383f4dab11de9d6f24 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:31:22 -0700 Subject: [PATCH 11/11] build fixes --- src/code/ContainerRegistryServerAPICalls.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index cd22c1c5d..cd8c4c8be 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -343,6 +343,7 @@ public override Stream InstallPackage(string packageName, string packageVersion, Stream results = new MemoryStream(); if (string.IsNullOrEmpty(packageVersion)) { + errRecord = new ErrorRecord( exception: new ArgumentNullException($"Package version could not be found for {packageName}"), "PackageVersionNullOrEmptyError", ErrorCategory.InvalidArgument,