From 74c56788fc69f4ee5a3bab762594452b98c9204a Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 9 Sep 2026 14:57:42 -0400 Subject: [PATCH 1/3] Take a fresh WinGet catalog snapshot after refreshing the source index --- .../ClientHelpers/NativeWinGetHelper.cs | 20 +++++-- .../WinGet.cs | 26 +++++++-- .../WinGetManagerTests.cs | 58 +++++++++++++++++++ 3 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs index 70386c1dfa..cb296cf961 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs @@ -448,19 +448,27 @@ nativePackage.AvailableVersions is { } versions private IReadOnlyList GetCachedLocalWinGetPackages(int? cacheSeconds = null) { - if (_localPackagesProvider is not null) - { - return _localPackagesProvider(); - } + long sourceIndexGeneration = WinGet.SourceIndexGeneration; return cacheSeconds is null - ? TaskRecycler>.RunOrAttach(GetLocalWinGetPackages) + ? TaskRecycler>.RunOrAttach( + EnumerateLocalWinGetPackages, + sourceIndexGeneration + ) : TaskRecycler>.RunOrAttach( - GetLocalWinGetPackages, + EnumerateLocalWinGetPackages, + sourceIndexGeneration, cacheSeconds.Value ); } + private IReadOnlyList EnumerateLocalWinGetPackages(long sourceIndexGeneration) + { + return _localPackagesProvider is not null + ? _localPackagesProvider() + : GetLocalWinGetPackages(); + } + private IReadOnlyList GetAvailableUpdatesFromSystemCli(Exception ex) { var unwrappedException = UnwrapException(ex); diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs index a668c2e698..add00707fa 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs @@ -92,6 +92,13 @@ public void Dispose() } } + private static long _sourceIndexGeneration; + + internal static long SourceIndexGeneration => Volatile.Read(ref _sourceIndexGeneration); + + internal static void MarkSourceIndexRefreshed() => + Interlocked.Increment(ref _sourceIndexGeneration); + public WinGet() { Capabilities = new ManagerCapabilities @@ -811,12 +818,19 @@ public override void RefreshPackageIndexes() p.StartInfo.Environment["TMP"] = WinGetTemp; } - p.Start(); - logger.AddToStdOut(p.StandardOutput.ReadToEnd()); - logger.AddToStdErr(p.StandardError.ReadToEnd()); - logger.Close(p.ExitCode); - p.WaitForExit(); - p.Close(); + try + { + p.Start(); + logger.AddToStdOut(p.StandardOutput.ReadToEnd()); + logger.AddToStdErr(p.StandardError.ReadToEnd()); + logger.Close(p.ExitCode); + p.WaitForExit(); + p.Close(); + } + finally + { + MarkSourceIndexRefreshed(); + } } private string GetCliToolProxyArgument() diff --git a/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs index ecee1fec0b..4f4a559e3e 100644 --- a/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs @@ -735,6 +735,64 @@ public void NativeWinGetHelperUsesSystemCliFallbackForUpdatesWhenCompositeCatalo Assert.Equal("2.0.0", package.NewVersionString); } + [Fact] + public void NativeWinGetHelperTakesANewCatalogSnapshotAfterTheSourceIndexIsRefreshed() + { + WinGet.MarkSourceIndexRefreshed(); + int snapshots = 0; + var helper = new NativeWinGetHelper( + new TestableWinGet(), + systemCliHelperFactory: null, + skipInitialization: true, + localPackagesProvider: () => + { + snapshots++; + return []; + } + ); + + helper.GetInstalledPackages_UnSafe(); + Assert.Equal(1, snapshots); + + WinGet.MarkSourceIndexRefreshed(); + helper.GetAvailableUpdates_UnSafe(); + + Assert.Equal(2, snapshots); + } + + [Fact] + public void NativeWinGetHelperReusesTheCatalogSnapshotWhileTheSourceIndexIsUnchanged() + { + WinGet.MarkSourceIndexRefreshed(); + int snapshots = 0; + var helper = new NativeWinGetHelper( + new TestableWinGet(), + systemCliHelperFactory: null, + skipInitialization: true, + localPackagesProvider: () => + { + snapshots++; + return []; + } + ); + + helper.GetInstalledPackages_UnSafe(); + helper.GetAvailableUpdates_UnSafe(); + + Assert.Equal(1, snapshots); + } + + [Fact] + public void RefreshPackageIndexesAdvancesTheSourceIndexGenerationWhenTheCliCallFails() + { + var manager = new TestableWinGet(); + long generationBefore = WinGet.SourceIndexGeneration; + + Assert.ThrowsAny(manager.RefreshPackageIndexes); + + Assert.NotEqual(generationBefore, WinGet.SourceIndexGeneration); + } + [Fact] public void NativeWinGetHelperSelectReachableCatalogsSkipsUnavailableSources() { From 0d1fb3a6e9539f236d2c95751d7658e8411fa180 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 9 Sep 2026 15:42:27 -0400 Subject: [PATCH 2/3] Keep a successful index refresh across the update-listing retry --- .../Manager/PackageManager.cs | 19 +++++++++++---- .../Fakes/TestPackageManager.cs | 7 ++++++ .../PackageManagerTests.cs | 24 ++++++++++++++++++- .../WinGetManagerTests.cs | 1 + 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs index e78cad554b..103775a81b 100644 --- a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs +++ b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs @@ -329,7 +329,7 @@ private static void KillListingProcesses(List processes) } } - private void RefreshPackageIndexesSafely() + private bool RefreshPackageIndexesSafely() { try { @@ -342,6 +342,7 @@ private void RefreshPackageIndexesSafely() "RefreshPackageIndexes", allowDisablingTimeout: false ); + return true; } catch (Exception e) { @@ -353,6 +354,7 @@ private void RefreshPackageIndexesSafely() + $"({e.GetType().Name}: {e.Message}). The available updates will be listed " + "with the indexes as they are, which may result in an incomplete list." ); + return false; } } @@ -451,19 +453,26 @@ private IReadOnlyList _findPackages(string query, bool SecondAttempt) public IReadOnlyList GetAvailableUpdates() { LastUpdatesListingFailed = false; - return _getAvailableUpdates(false); + return _getAvailableUpdates(false, indexesAlreadyRefreshed: false); } - private IReadOnlyList _getAvailableUpdates(bool SecondAttempt) + private IReadOnlyList _getAvailableUpdates( + bool SecondAttempt, + bool indexesAlreadyRefreshed + ) { if (!IsReady()) { Logger.Warn($"Manager {Name} is disabled but yet GetAvailableUpdates was called"); return []; } + bool indexesRefreshed = indexesAlreadyRefreshed; try { - RefreshPackageIndexesSafely(); + if (!indexesAlreadyRefreshed) + { + indexesRefreshed = RefreshPackageIndexesSafely(); + } var packages = RunListingTaskWithTimeout( GetAvailableUpdates_UnSafe, @@ -486,7 +495,7 @@ private IReadOnlyList _getAvailableUpdates(bool SecondAttempt) $"Since this was the first attempt, {Name}.AttemptFastRepair() will be called and the procedure will be restarted" ); AttemptFastRepair(); - return _getAvailableUpdates(true); + return _getAvailableUpdates(true, indexesRefreshed); } Logger.Error("Error finding updates on manager " + Name); diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageManager.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageManager.cs index 0a697fab71..096fa45655 100644 --- a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageManager.cs +++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageManager.cs @@ -14,6 +14,7 @@ public sealed class TestPackageManager : PackageManager private Func> _findPackages = _ => []; private Func> _getAvailableUpdates = static () => []; private Func> _getInstalledPackages = static () => []; + private Action? _refreshPackageIndexes; private IReadOnlyList _candidateExecutableFiles; public TestPackageManager(string name = "TestManager", string? displayName = null) @@ -104,6 +105,11 @@ public void SetAvailableUpdates(Func> getAvailableUpdates _getAvailableUpdates = getAvailableUpdates; } + public void SetRefreshPackageIndexes(Action refreshPackageIndexes) + { + _refreshPackageIndexes = refreshPackageIndexes; + } + public void SetInstalledPackages(Func> getInstalledPackages) { _getInstalledPackages = getInstalledPackages; @@ -199,5 +205,6 @@ public override void AttemptFastRepair() public override void RefreshPackageIndexes() { RefreshPackageIndexesCalls++; + _refreshPackageIndexes?.Invoke(); } } diff --git a/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs index 0aab8e4b23..9dc3e2fff9 100644 --- a/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs @@ -266,7 +266,7 @@ public void FindPackagesRetriesOnceAfterFailure() } [Fact] - public void GetAvailableUpdatesRetriesOnceAndRefreshesIndexesPerAttempt() + public void GetAvailableUpdatesRetriesOnceAndKeepsTheIndexesItAlreadyRefreshed() { var manager = CreateReadyManager(); var attempts = 0; @@ -283,6 +283,28 @@ public void GetAvailableUpdatesRetriesOnceAndRefreshesIndexesPerAttempt() var package = Assert.Single(packages); Assert.Equal("Contoso.Update", package.Id); Assert.Equal(1, manager.AttemptFastRepairCalls); + Assert.Equal(1, manager.RefreshPackageIndexesCalls); + } + + [Fact] + public void GetAvailableUpdatesRefreshesIndexesAgainWhenTheFirstRefreshFailed() + { + var manager = CreateReadyManager(); + manager.SetRefreshPackageIndexes( + () => throw new InvalidOperationException("refresh failed") + ); + var attempts = 0; + manager.SetAvailableUpdates(() => + { + attempts++; + return attempts == 1 + ? throw new InvalidOperationException("updates failed") + : [CreatePackage(manager, "Contoso.Update", "Contoso Update", "1.0.0", "2.0.0")]; + }); + + var packages = manager.GetAvailableUpdates(); + + Assert.Single(packages); Assert.Equal(2, manager.RefreshPackageIndexesCalls); } diff --git a/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs index 4f4a559e3e..cd578a32ae 100644 --- a/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs @@ -778,6 +778,7 @@ public void NativeWinGetHelperReusesTheCatalogSnapshotWhileTheSourceIndexIsUncha helper.GetInstalledPackages_UnSafe(); helper.GetAvailableUpdates_UnSafe(); + helper.GetAvailableUpdates_UnSafe(); Assert.Equal(1, snapshots); } From 83f8910da9c06fbcdb9e19be471c7d5e87564fba Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 9 Sep 2026 16:05:05 -0400 Subject: [PATCH 3/3] Never re-refresh the package indexes on the update-listing retry --- .../Manager/PackageManager.cs | 18 +++++--------- .../Fakes/TestPackageManager.cs | 7 ------ .../PackageManagerTests.cs | 24 +------------------ 3 files changed, 7 insertions(+), 42 deletions(-) diff --git a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs index 103775a81b..2d8ba8cf11 100644 --- a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs +++ b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs @@ -329,7 +329,7 @@ private static void KillListingProcesses(List processes) } } - private bool RefreshPackageIndexesSafely() + private void RefreshPackageIndexesSafely() { try { @@ -342,7 +342,6 @@ private bool RefreshPackageIndexesSafely() "RefreshPackageIndexes", allowDisablingTimeout: false ); - return true; } catch (Exception e) { @@ -354,7 +353,6 @@ private bool RefreshPackageIndexesSafely() + $"({e.GetType().Name}: {e.Message}). The available updates will be listed " + "with the indexes as they are, which may result in an incomplete list." ); - return false; } } @@ -453,25 +451,21 @@ private IReadOnlyList _findPackages(string query, bool SecondAttempt) public IReadOnlyList GetAvailableUpdates() { LastUpdatesListingFailed = false; - return _getAvailableUpdates(false, indexesAlreadyRefreshed: false); + return _getAvailableUpdates(false); } - private IReadOnlyList _getAvailableUpdates( - bool SecondAttempt, - bool indexesAlreadyRefreshed - ) + private IReadOnlyList _getAvailableUpdates(bool SecondAttempt) { if (!IsReady()) { Logger.Warn($"Manager {Name} is disabled but yet GetAvailableUpdates was called"); return []; } - bool indexesRefreshed = indexesAlreadyRefreshed; try { - if (!indexesAlreadyRefreshed) + if (!SecondAttempt) { - indexesRefreshed = RefreshPackageIndexesSafely(); + RefreshPackageIndexesSafely(); } var packages = RunListingTaskWithTimeout( @@ -495,7 +489,7 @@ bool indexesAlreadyRefreshed $"Since this was the first attempt, {Name}.AttemptFastRepair() will be called and the procedure will be restarted" ); AttemptFastRepair(); - return _getAvailableUpdates(true, indexesRefreshed); + return _getAvailableUpdates(true); } Logger.Error("Error finding updates on manager " + Name); diff --git a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageManager.cs b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageManager.cs index 096fa45655..0a697fab71 100644 --- a/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageManager.cs +++ b/src/UniGetUI.PackageEngine.Tests/Infrastructure/Fakes/TestPackageManager.cs @@ -14,7 +14,6 @@ public sealed class TestPackageManager : PackageManager private Func> _findPackages = _ => []; private Func> _getAvailableUpdates = static () => []; private Func> _getInstalledPackages = static () => []; - private Action? _refreshPackageIndexes; private IReadOnlyList _candidateExecutableFiles; public TestPackageManager(string name = "TestManager", string? displayName = null) @@ -105,11 +104,6 @@ public void SetAvailableUpdates(Func> getAvailableUpdates _getAvailableUpdates = getAvailableUpdates; } - public void SetRefreshPackageIndexes(Action refreshPackageIndexes) - { - _refreshPackageIndexes = refreshPackageIndexes; - } - public void SetInstalledPackages(Func> getInstalledPackages) { _getInstalledPackages = getInstalledPackages; @@ -205,6 +199,5 @@ public override void AttemptFastRepair() public override void RefreshPackageIndexes() { RefreshPackageIndexesCalls++; - _refreshPackageIndexes?.Invoke(); } } diff --git a/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs index 9dc3e2fff9..7a8ccaf2fc 100644 --- a/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PackageManagerTests.cs @@ -266,7 +266,7 @@ public void FindPackagesRetriesOnceAfterFailure() } [Fact] - public void GetAvailableUpdatesRetriesOnceAndKeepsTheIndexesItAlreadyRefreshed() + public void GetAvailableUpdatesRetriesOnceWithoutRefreshingIndexesAgain() { var manager = CreateReadyManager(); var attempts = 0; @@ -286,28 +286,6 @@ public void GetAvailableUpdatesRetriesOnceAndKeepsTheIndexesItAlreadyRefreshed() Assert.Equal(1, manager.RefreshPackageIndexesCalls); } - [Fact] - public void GetAvailableUpdatesRefreshesIndexesAgainWhenTheFirstRefreshFailed() - { - var manager = CreateReadyManager(); - manager.SetRefreshPackageIndexes( - () => throw new InvalidOperationException("refresh failed") - ); - var attempts = 0; - manager.SetAvailableUpdates(() => - { - attempts++; - return attempts == 1 - ? throw new InvalidOperationException("updates failed") - : [CreatePackage(manager, "Contoso.Update", "Contoso Update", "1.0.0", "2.0.0")]; - }); - - var packages = manager.GetAvailableUpdates(); - - Assert.Single(packages); - Assert.Equal(2, manager.RefreshPackageIndexesCalls); - } - [Fact] public void GetInstalledPackagesReturnsEmptyAfterSecondFailure() {