diff --git a/src/UniGetUI.PackageEngine.Interfaces/IPackageManager.cs b/src/UniGetUI.PackageEngine.Interfaces/IPackageManager.cs index bbb8f7a79c..069d52cdcc 100644 --- a/src/UniGetUI.PackageEngine.Interfaces/IPackageManager.cs +++ b/src/UniGetUI.PackageEngine.Interfaces/IPackageManager.cs @@ -85,6 +85,10 @@ public interface IPackageManager /// public IReadOnlyList GetInstalledPackages(); + public long InstalledPackagesGeneration { get; } + + public void InvalidateInstalledPackages(); + /// /// Refreshes the Package Manager sources/indexes /// Each manager MUST implement this method. diff --git a/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs b/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs index f641d19428..ef9849431c 100644 --- a/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs +++ b/src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs @@ -852,7 +852,14 @@ internal static List ParseUpdatesResponse( } protected sealed override IReadOnlyList GetInstalledPackages_UnSafe() => - TaskRecycler>.RunOrAttach(_getInstalledPackages_UnSafe); + TaskRecycler>.RunOrAttach( + ListInstalledPackagesForGeneration, + InstalledPackagesGeneration + ); + + private IReadOnlyList ListInstalledPackagesForGeneration( + long installedPackagesGeneration + ) => _getInstalledPackages_UnSafe(); protected abstract IReadOnlyList _getInstalledPackages_UnSafe(); } diff --git a/src/UniGetUI.PackageEngine.Managers.Scoop/Scoop.cs b/src/UniGetUI.PackageEngine.Managers.Scoop/Scoop.cs index 4cfeae59b5..e8a4a3e30b 100644 --- a/src/UniGetUI.PackageEngine.Managers.Scoop/Scoop.cs +++ b/src/UniGetUI.PackageEngine.Managers.Scoop/Scoop.cs @@ -440,9 +440,13 @@ protected override IReadOnlyList GetAvailableUpdates_UnSafe() } protected override IReadOnlyList GetInstalledPackages_UnSafe() => - TaskRecycler>.RunOrAttach(_getInstalledPackages_UnSafe, 15); + TaskRecycler>.RunOrAttach( + _getInstalledPackages_UnSafe, + InstalledPackagesGeneration, + 15 + ); - private IReadOnlyList _getInstalledPackages_UnSafe() + private IReadOnlyList _getInstalledPackages_UnSafe(long installedPackagesGeneration) { using Process p = new() { diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs index 62db41e21e..54d17f742e 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs @@ -475,21 +475,24 @@ nativePackage.AvailableVersions is { } versions private IReadOnlyList GetCachedLocalWinGetPackages(int? cacheSeconds = null) { - long sourceIndexGeneration = WinGet.SourceIndexGeneration; + (long, long) generations = ( + WinGet.SourceIndexGeneration, + Manager.InstalledPackagesGeneration + ); return cacheSeconds is null ? TaskRecycler>.RunOrAttach( EnumerateLocalWinGetPackages, - sourceIndexGeneration + generations ) : TaskRecycler>.RunOrAttach( EnumerateLocalWinGetPackages, - sourceIndexGeneration, + generations, cacheSeconds.Value ); } - private IReadOnlyList EnumerateLocalWinGetPackages(long sourceIndexGeneration) + private IReadOnlyList EnumerateLocalWinGetPackages((long, long) generations) { return _localPackagesProvider is not null ? _localPackagesProvider() diff --git a/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs b/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs index 0c63fc369f..6d1960d04c 100644 --- a/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs +++ b/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs @@ -137,6 +137,7 @@ public PackageOperation( if (status is OperationStatus.Canceled) Package.SetTag(PackageTag.Default); }; + OperationSucceeded += (_, _) => Package.Manager.InvalidateInstalledPackages(); OperationSucceeded += (_, _) => RecordPendingRebootState(); OperationSucceeded += (_, _) => HandleSuccess(); OperationFailed += (_, _) => HandleFailure(); diff --git a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/Classes/NullPackageManager.cs b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/Classes/NullPackageManager.cs index cf780edf6d..a12f620849 100644 --- a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/Classes/NullPackageManager.cs +++ b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/Classes/NullPackageManager.cs @@ -20,6 +20,10 @@ public class NullPackageManager : IPackageManager public bool LastInstalledListingFailed => false; + public long InstalledPackagesGeneration => 0; + + public void InvalidateInstalledPackages() { } + public static NullPackageManager Instance = new(); public ManagerProperties Properties { get; } public ManagerCapabilities Capabilities { get; } diff --git a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs index 8dc88b0dc4..a3c65d6a33 100644 --- a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs +++ b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs @@ -546,6 +546,13 @@ private IReadOnlyList _getAvailableUpdates(bool SecondAttempt) /// public bool LastInstalledListingFailed { get; private set; } + private long _installedPackagesGeneration; + + public long InstalledPackagesGeneration => Volatile.Read(ref _installedPackagesGeneration); + + public void InvalidateInstalledPackages() => + Interlocked.Increment(ref _installedPackagesGeneration); + public IReadOnlyList GetInstalledPackages() { LastInstalledListingFailed = false; diff --git a/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs index 0e064f1755..85da46c718 100644 --- a/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs @@ -544,6 +544,27 @@ private static Func ReportsWaitingOnTheGate(AbstractOperation operation) return () => reported; } + [Fact] + public async Task ASuccessfulOperationInvalidatesTheInstalledPackagesOfItsManager() + { + if (!OperatingSystem.IsWindows()) + return; + + var manager = CreateSlowProcessManager(serializesOperations: false); + InitializeLoaders(); + long generationBefore = manager.InstalledPackagesGeneration; + using var operation = new UpdatePackageOperation( + new PackageBuilder().WithManager(manager).WithId("updated").Build(), + new InstallOptions(), + IgnoreParallelInstalls: true + ); + + await operation.MainThread().WaitAsync(TimeSpan.FromSeconds(30)); + + Assert.Equal(OperationStatus.Succeeded, operation.Status); + Assert.NotEqual(generationBefore, manager.InstalledPackagesGeneration); + } + [Fact] public async Task OperationsOfASerializingManagerNeverRunTheirProcessesConcurrently() { diff --git a/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs index 29c113224f..bce2dfba6b 100644 --- a/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs @@ -900,6 +900,31 @@ public void NativeWinGetHelperReusesTheCatalogSnapshotWhileTheSourceIndexIsUncha Assert.Equal(1, snapshots); } + [Fact] + public void NativeWinGetHelperTakesAFreshSnapshotOnceTheInstalledPackagesAreInvalidated() + { + WinGet.MarkSourceIndexRefreshed(); + var manager = new TestableWinGet(); + int snapshots = 0; + var helper = new NativeWinGetHelper( + manager, + systemCliHelperFactory: null, + skipInitialization: true, + localPackagesProvider: () => + { + snapshots++; + return []; + } + ); + + helper.GetInstalledPackages_UnSafe(); + manager.InvalidateInstalledPackages(); + helper.GetInstalledPackages_UnSafe(); + helper.GetAvailableUpdates_UnSafe(); + + Assert.Equal(2, snapshots); + } + [Fact] public void RefreshPackageIndexesAdvancesTheSourceIndexGenerationWhenTheCliCallFails() { diff --git a/src/UniGetUI.Tests/CatalogEditorTests.cs b/src/UniGetUI.Tests/CatalogEditorTests.cs index 7734adb0ab..f1c89a8d30 100644 --- a/src/UniGetUI.Tests/CatalogEditorTests.cs +++ b/src/UniGetUI.Tests/CatalogEditorTests.cs @@ -665,6 +665,8 @@ private sealed class SearchManager : IPackageManager public bool IdentifiersAreQuotedOnCommandLine => true; public bool LastUpdatesListingFailed => false; public bool LastInstalledListingFailed => false; + public long InstalledPackagesGeneration => 0; + public void InvalidateInstalledPackages() { } public int? CompareVersions(string versionA, string versionB) => throw new NotSupportedException(); public void Initialize() => throw new NotSupportedException(); public bool IsEnabled() => true;