From bbf00cda66ffb7c2da42b2d369e6481569deb028 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 7 Oct 2026 16:07:57 -0400 Subject: [PATCH 1/5] Report an unmet operation precondition without a stack trace --- .../Helpers/WinGetPkgOperationHelper.cs | 9 ++- .../AbstractOperation.cs | 25 +++++++-- .../Classes/OperationPreconditionException.cs | 7 +++ .../PackageOperationsTests.cs | 56 +++++++++++++++++++ 4 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/Classes/OperationPreconditionException.cs diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs index 4c89179d60..6e30f2a6fd 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/Helpers/WinGetPkgOperationHelper.cs @@ -3,6 +3,7 @@ using UniGetUI.Core.Logging; using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; +using UniGetUI.PackageEngine.Classes.Manager; using UniGetUI.PackageEngine.Classes.Manager.BaseProviders; using UniGetUI.PackageEngine.Enums; using UniGetUI.PackageEngine.Interfaces; @@ -243,20 +244,22 @@ or ElevationRequirement.ElevatesSelf ) { if (CoreTools.IsAdministrator()) - throw new UnauthorizedAccessException( + throw new OperationPreconditionException( CoreTools.Translate( "This package cannot be installed from an elevated context." ) + + " " + CoreTools.Translate( "Please run UniGetUI as a regular user and try again." ) ); if (options.RunAsAdministrator) - throw new UnauthorizedAccessException( + throw new OperationPreconditionException( CoreTools.Translate( "This package cannot be installed from an elevated context." ) + + " " + CoreTools.Translate( "Please check the installation options for this package and try again" ) @@ -276,7 +279,7 @@ or ElevationRequirement.ElevatesSelf } catch (Exception ex) { - if (ex is UnauthorizedAccessException) + if (ex is OperationPreconditionException) throw; Logger.Error("Recovered from fatal WinGet exception:"); diff --git a/src/UniGetUI.PackageEngine.Operations/AbstractOperation.cs b/src/UniGetUI.PackageEngine.Operations/AbstractOperation.cs index 609d71ee92..237ccbcefa 100644 --- a/src/UniGetUI.PackageEngine.Operations/AbstractOperation.cs +++ b/src/UniGetUI.PackageEngine.Operations/AbstractOperation.cs @@ -1,6 +1,7 @@ using UniGetUI.Core.Logging; using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; +using UniGetUI.PackageEngine.Classes.Manager; using UniGetUI.PackageEngine.Enums; namespace UniGetUI.PackageOperations; @@ -395,10 +396,17 @@ private async Task MainThreadCore(CancellationTokenSource runCancellation) } catch (Exception ex) { - Line("An internal error occurred:", LineType.Error); - foreach (var line in ex.ToString().Split("\n")) + if (ex is OperationPreconditionException) { - Line(line, LineType.Error); + Line(ex.Message, LineType.Error); + } + else + { + Line("An internal error occurred:", LineType.Error); + foreach (var line in ex.ToString().Split("\n")) + { + Line(line, LineType.Error); + } } RemoveFromQueue(this); @@ -547,9 +555,16 @@ private async Task _runOperation() ? OperationVeredict.Canceled : OperationVeredict.Failure; Logger.Error(e); - foreach (string l in e.ToString().Split("\n")) + if (e is OperationPreconditionException) + { + Line(e.Message, LineType.Error); + } + else { - Line(l, LineType.Error); + foreach (string l in e.ToString().Split("\n")) + { + Line(l, LineType.Error); + } } } } while (result is OperationVeredict.AutoRetry); diff --git a/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/Classes/OperationPreconditionException.cs b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/Classes/OperationPreconditionException.cs new file mode 100644 index 0000000000..5dc48fba15 --- /dev/null +++ b/src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/Classes/OperationPreconditionException.cs @@ -0,0 +1,7 @@ +namespace UniGetUI.PackageEngine.Classes.Manager; + +public sealed class OperationPreconditionException : Exception +{ + public OperationPreconditionException(string message) + : base(message) { } +} diff --git a/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs index 5252079e67..3343380e9c 100644 --- a/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs @@ -8,6 +8,7 @@ using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; using UniGetUI.Interface.Enums; +using UniGetUI.PackageEngine.Classes.Manager; using UniGetUI.PackageEngine.Enums; using UniGetUI.PackageEngine.Interfaces; using UniGetUI.PackageEngine.Managers.NpmManager; @@ -1871,6 +1872,61 @@ protected override Task PerformOperation() } } + [Fact] + public async Task UnmetPreconditionIsReportedWithoutTheInternalErrorStackTrace() + { + using var operation = new ThrowingStubOperation( + new OperationPreconditionException("Run UniGetUI as a regular user and try again.") + ); + + await operation.MainThread(); + + Assert.Equal(OperationStatus.Failed, operation.Status); + var lines = operation.GetOutput().Select(line => line.Item1).ToList(); + Assert.Contains("Run UniGetUI as a regular user and try again.", lines); + Assert.DoesNotContain("An internal error occurred:", lines); + Assert.DoesNotContain(lines, line => line.Contains("OperationPreconditionException")); + } + + [Fact] + public async Task AnUnexpectedExceptionStillReportsItsStackTrace() + { + using var operation = new ThrowingStubOperation( + new InvalidOperationException("some unexpected failure") + ); + + await operation.MainThread(); + + Assert.Equal(OperationStatus.Failed, operation.Status); + var lines = operation.GetOutput().Select(line => line.Item1).ToList(); + Assert.Contains(lines, line => line.Contains("InvalidOperationException")); + Assert.Contains(lines, line => line.TrimStart().StartsWith("at ", StringComparison.Ordinal)); + } + + private sealed class ThrowingStubOperation : AbstractOperation + { + private readonly Exception _exception; + + public ThrowingStubOperation(Exception exception) + : base(queue_enabled: false) + { + _exception = exception; + Metadata.Status = "Throwing stub status"; + Metadata.Title = "Throwing stub title"; + Metadata.OperationInformation = "Throwing stub info"; + Metadata.SuccessTitle = "Throwing stub success"; + Metadata.SuccessMessage = "Throwing stub success"; + Metadata.FailureTitle = "Throwing stub failure"; + Metadata.FailureMessage = "Throwing stub failure"; + } + + protected override void ApplyRetryAction(string retryMode) { } + + protected override Task PerformOperation() => throw _exception; + + public override Task GetOperationIcon() => Task.FromResult(new Uri("about:blank")); + } + private sealed class CancellationAwareStubOperation : AbstractOperation { public TaskCompletionSource PerformStarted { get; } = new( From 001bdc4f245d7fe92d7f12ec34bfab7a84593940 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 7 Oct 2026 16:08:08 -0400 Subject: [PATCH 2/5] Let a skipped integrity check reach a Windows PowerShell module update --- .../Helpers/PowerShellPkgOperationHelper.cs | 23 +++++--- .../PowerShellManagerTests.cs | 58 +++++++++++++++++++ 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs index 72807ed514..fd9e25d044 100644 --- a/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs @@ -25,15 +25,20 @@ protected override IReadOnlyList _getOperationParameters( bool standalone ) { + bool updatesThroughInstall = + operation is OperationType.Update && options.SkipHashCheck; + bool usesInstallVerb = operation is OperationType.Install || updatesThroughInstall; + List parameters = [ - operation switch - { - OperationType.Install => Manager.Properties.InstallVerb, - OperationType.Update => Manager.Properties.UpdateVerb, - OperationType.Uninstall => Manager.Properties.UninstallVerb, - _ => throw new InvalidDataException("Invalid package operation"), - }, + usesInstallVerb + ? Manager.Properties.InstallVerb + : operation switch + { + OperationType.Update => Manager.Properties.UpdateVerb, + OperationType.Uninstall => Manager.Properties.UninstallVerb, + _ => throw new InvalidDataException("Invalid package operation"), + }, ]; parameters.AddRange(["-Name", package.Id, "-Confirm:$false", "-Force"]); @@ -43,7 +48,7 @@ bool standalone parameters.Add("-AllowPrerelease"); // Update-Module (PowerShellGet) has no -Scope parameter; only Install-Module accepts it - if (operation is OperationType.Install && !package.OverridenOptions.PowerShell_DoNotSetScopeParameter) + if (usesInstallVerb && !package.OverridenOptions.PowerShell_DoNotSetScopeParameter) { // The scope chosen in the options dialog wins; fall back to the auto-detected install scope string scope = options.InstallationScope.Length > 0 @@ -53,7 +58,7 @@ bool standalone } } - if (operation is OperationType.Install) + if (usesInstallVerb) { if (package.OverridenOptions.PowerShell_AllowClobber) parameters.Add("-AllowClobber"); diff --git a/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs index 6c82aa4999..67ded99fd2 100644 --- a/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs @@ -160,6 +160,64 @@ public void GetParameters_UpdateOmitsScope() Assert.DoesNotContain("-Scope", parameters); } + [Fact] + public void GetParameters_UpdateKeepsUpdateModuleWhenIntegrityChecksAreNotSkipped() + { + var manager = new PowerShell(); + var package = BuildInstalledPackage(manager); + + var parameters = manager.OperationHelper.GetParameters( + package, + new InstallOptions(), + OperationType.Update + ); + + Assert.Contains("Update-Module", parameters); + Assert.DoesNotContain("Install-Module", parameters); + Assert.DoesNotContain("-SkipPublisherCheck", parameters); + } + + [Fact] + public void GetParameters_UpdateRunsThroughInstallModuleWhenIntegrityChecksAreSkipped() + { + var manager = new PowerShell(); + var package = BuildInstalledPackage(manager); + + var options = new InstallOptions { SkipHashCheck = true }; + var parameters = manager.OperationHelper.GetParameters( + package, + options, + OperationType.Update + ); + + Assert.Contains("Install-Module", parameters); + Assert.DoesNotContain("Update-Module", parameters); + Assert.Contains("-SkipPublisherCheck", parameters); + Assert.Contains("-Name", parameters); + Assert.Contains(package.Id, parameters); + } + + [Fact] + public void GetParameters_UpdateThroughInstallModuleCarriesTheSelectedScope() + { + var manager = new PowerShell(); + var package = BuildInstalledPackage(manager); + + var options = new InstallOptions + { + SkipHashCheck = true, + InstallationScope = PackageScope.Machine, + }; + var parameters = manager.OperationHelper.GetParameters( + package, + options, + OperationType.Update + ); + + Assert.Contains("-Scope", parameters); + Assert.Contains("AllUsers", parameters); + } + [Theory] [InlineData(OperationType.Install)] [InlineData(OperationType.Update)] From fd7105b50a1b83260051fcaeb82a8b45c79a8be9 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 7 Oct 2026 16:35:32 -0400 Subject: [PATCH 3/5] Keep repository, scope and version intact on a rerouted module update --- .../OverridenInstallationOptions.cs | 3 +- .../Helpers/PowerShellPkgOperationHelper.cs | 18 ++- .../PowerShellManagerTests.cs | 103 ++++++++++++++++++ 3 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs b/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs index 0ef5c946ac..5b7a01178d 100644 --- a/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs +++ b/src/UniGetUI.PackageEngine.Enums/OverridenInstallationOptions.cs @@ -6,6 +6,7 @@ public struct OverridenInstallationOptions public bool? RunAsAdministrator; public bool PowerShell_DoNotSetScopeParameter = false; public bool PowerShell_AllowClobber = false; + public bool PowerShell_UpdateThroughInstall = false; public bool? WinGet_SpecifyVersion = null; public bool Pip_BreakSystemPackages = false; public bool WinGet_DropArchAndScope = false; @@ -21,6 +22,6 @@ public OverridenInstallationOptions(string? scope = null, bool? runAsAdministrat public override string ToString() { - return $""; + return $""; } } diff --git a/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs index fd9e25d044..79ef5c7364 100644 --- a/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs @@ -42,6 +42,11 @@ bool standalone ]; parameters.AddRange(["-Name", package.Id, "-Confirm:$false", "-Force"]); + package.OverridenOptions.PowerShell_UpdateThroughInstall = updatesThroughInstall; + + if (updatesThroughInstall && package.Source.Name.Length > 0) + parameters.AddRange(["-Repository", package.Source.Name]); + if (operation is not OperationType.Uninstall) { if (options.PreRelease) @@ -54,7 +59,11 @@ bool standalone string scope = options.InstallationScope.Length > 0 ? options.InstallationScope : package.OverridenOptions.Scope ?? ""; - parameters.AddRange(["-Scope", scope == PackageScope.Global ? "AllUsers" : "CurrentUser"]); + + if (operation is OperationType.Install || scope.Length > 0) + parameters.AddRange( + ["-Scope", scope == PackageScope.Global ? "AllUsers" : "CurrentUser"] + ); } } @@ -66,7 +75,7 @@ bool standalone if (options.SkipHashCheck) parameters.Add("-SkipPublisherCheck"); - if (options.Version != "") + if (operation is OperationType.Install && options.Version != "") parameters.AddRange(["-RequiredVersion", options.Version]); } @@ -137,7 +146,10 @@ package.OverridenOptions.RunAsAdministrator is not true } if ( - operation is OperationType.Install + ( + operation is OperationType.Install + || package.OverridenOptions.PowerShell_UpdateThroughInstall + ) && output_string.Contains("CommandAlreadyAvailable") && !package.OverridenOptions.PowerShell_AllowClobber ) diff --git a/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs index 67ded99fd2..a8abe5b20e 100644 --- a/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs @@ -218,6 +218,109 @@ public void GetParameters_UpdateThroughInstallModuleCarriesTheSelectedScope() Assert.Contains("AllUsers", parameters); } + [Fact] + public void GetParameters_UpdateThroughInstallModuleOmitsScopeWhenNoneIsKnown() + { + var manager = new PowerShell(); + var package = BuildInstalledPackage(manager); + + var options = new InstallOptions { SkipHashCheck = true }; + var parameters = manager.OperationHelper.GetParameters( + package, + options, + OperationType.Update + ); + + Assert.DoesNotContain("-Scope", parameters); + Assert.DoesNotContain("CurrentUser", parameters); + } + + [Fact] + public void GetParameters_UpdateThroughInstallModuleKeepsTheInstalledRepository() + { + var manager = new PowerShell(); + var package = BuildInstalledPackage(manager); + + var options = new InstallOptions { SkipHashCheck = true }; + var parameters = manager.OperationHelper.GetParameters( + package, + options, + OperationType.Update + ); + + int repositoryIndex = parameters.ToList().IndexOf("-Repository"); + Assert.NotEqual(-1, repositoryIndex); + Assert.Equal(package.Source.Name, parameters[repositoryIndex + 1]); + } + + [Fact] + public void GetParameters_UpdateThroughInstallModuleIgnoresAPinnedVersion() + { + var manager = new PowerShell(); + var package = BuildInstalledPackage(manager); + + var options = new InstallOptions { SkipHashCheck = true, Version = "1.0.0" }; + var parameters = manager.OperationHelper.GetParameters( + package, + options, + OperationType.Update + ); + + Assert.DoesNotContain("-RequiredVersion", parameters); + Assert.DoesNotContain("1.0.0", parameters); + } + + [Fact] + public void GetResult_RetriesWithAllowClobberOnAnUpdateRoutedThroughInstallModule() + { + var manager = new PowerShell(); + var package = BuildInstalledPackage(manager); + + var options = new InstallOptions { SkipHashCheck = true }; + manager.OperationHelper.GetParameters(package, options, OperationType.Update); + + var veredict = manager.OperationHelper.GetResult( + package, + OperationType.Update, + ClobberFailureOutput, + 1 + ); + + Assert.Equal(OperationVeredict.AutoRetry, veredict); + Assert.True(package.OverridenOptions.PowerShell_AllowClobber); + + var parameters = manager.OperationHelper.GetParameters( + package, + options, + OperationType.Update + ); + + Assert.Contains("-AllowClobber", parameters); + } + + [Fact] + public void GetResult_DoesNotRetryWithAllowClobberOnAPlainUpdate() + { + var manager = new PowerShell(); + var package = BuildInstalledPackage(manager); + + manager.OperationHelper.GetParameters( + package, + new InstallOptions(), + OperationType.Update + ); + + var veredict = manager.OperationHelper.GetResult( + package, + OperationType.Update, + ClobberFailureOutput, + 1 + ); + + Assert.Equal(OperationVeredict.Failure, veredict); + Assert.False(package.OverridenOptions.PowerShell_AllowClobber); + } + [Theory] [InlineData(OperationType.Install)] [InlineData(OperationType.Update)] From 15604e5d79c82851510d97fcc0373332b57d8f4a Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 7 Oct 2026 16:54:34 -0400 Subject: [PATCH 4/5] Quote the repository when the module command line is shell interpreted --- .../Helpers/PowerShellPkgOperationHelper.cs | 12 ++- .../PowerShellManagerTests.cs | 76 ++++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs index 79ef5c7364..df1a073eae 100644 --- a/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs @@ -44,8 +44,15 @@ bool standalone package.OverridenOptions.PowerShell_UpdateThroughInstall = updatesThroughInstall; + bool shellInterpreted = standalone || Manager.Status.OperationCallArgs.Count is 0; + if (updatesThroughInstall && package.Source.Name.Length > 0) - parameters.AddRange(["-Repository", package.Source.Name]); + parameters.AddRange( + [ + "-Repository", + shellInterpreted ? QuoteForPowerShell(package.Source.Name) : package.Source.Name, + ] + ); if (operation is not OperationType.Uninstall) { @@ -114,6 +121,9 @@ bool standalone return parameters; } + private static string QuoteForPowerShell(string value) => + $"'{value.Replace("'", "''")}'"; + protected override OperationVeredict _getOperationResult( IPackage package, OperationType operation, diff --git a/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs index a8abe5b20e..ed504e6a5b 100644 --- a/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs @@ -250,7 +250,81 @@ public void GetParameters_UpdateThroughInstallModuleKeepsTheInstalledRepository( int repositoryIndex = parameters.ToList().IndexOf("-Repository"); Assert.NotEqual(-1, repositoryIndex); - Assert.Equal(package.Source.Name, parameters[repositoryIndex + 1]); + Assert.Contains(package.Source.Name, parameters[repositoryIndex + 1]); + } + + [Fact] + public void GetStandaloneParameters_UpdateThroughInstallModuleQuotesTheRepository() + { + var manager = new PowerShell(); + var package = Assert.Single( + PowerShell.ParseInstalledPackages( + ["Devolutions.PowerShell\t1.0.0\tInternal Modules"], + manager + ) + ); + + var options = new InstallOptions { SkipHashCheck = true }; + var parameters = manager.OperationHelper.GetStandaloneParameters( + package, + options, + OperationType.Update + ); + + int repositoryIndex = parameters.ToList().IndexOf("-Repository"); + Assert.NotEqual(-1, repositoryIndex); + Assert.Equal("'Internal Modules'", parameters[repositoryIndex + 1]); + } + + [Fact] + public void GetParameters_UpdateThroughInstallModuleKeepsTheRepositoryRawForTheArgumentVector() + { + var manager = new PowerShell(); + manager.Initialize(); + + if (manager.Status.OperationCallArgs.Count is 0) + return; + + var package = Assert.Single( + PowerShell.ParseInstalledPackages( + ["Devolutions.PowerShell\t1.0.0\tInternal Modules"], + manager + ) + ); + + var options = new InstallOptions { SkipHashCheck = true }; + var parameters = manager.OperationHelper.GetParameters( + package, + options, + OperationType.Update + ); + + int repositoryIndex = parameters.ToList().IndexOf("-Repository"); + Assert.NotEqual(-1, repositoryIndex); + Assert.Equal("Internal Modules", parameters[repositoryIndex + 1]); + } + + [Fact] + public void GetStandaloneParameters_UpdateThroughInstallModuleEscapesAQuotedRepository() + { + var manager = new PowerShell(); + var package = Assert.Single( + PowerShell.ParseInstalledPackages( + ["Devolutions.PowerShell\t1.0.0\tBob's Modules"], + manager + ) + ); + + var options = new InstallOptions { SkipHashCheck = true }; + var parameters = manager.OperationHelper.GetStandaloneParameters( + package, + options, + OperationType.Update + ); + + int repositoryIndex = parameters.ToList().IndexOf("-Repository"); + Assert.NotEqual(-1, repositoryIndex); + Assert.Equal("'Bob''s Modules'", parameters[repositoryIndex + 1]); } [Fact] From a1647c80bbac5c433c1a990a47bf151ce2cf7599 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Thu, 8 Oct 2026 08:23:26 -0400 Subject: [PATCH 5/5] Scope the module routing decision to the attempt that made it --- .../Helpers/PowerShellPkgOperationHelper.cs | 11 ++-- .../PowerShellManagerTests.cs | 61 +++++++++++++++++-- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs index df1a073eae..4ca0946af3 100644 --- a/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.PowerShell/Helpers/PowerShellPkgOperationHelper.cs @@ -42,7 +42,8 @@ bool standalone ]; parameters.AddRange(["-Name", package.Id, "-Confirm:$false", "-Force"]); - package.OverridenOptions.PowerShell_UpdateThroughInstall = updatesThroughInstall; + if (!standalone) + package.OverridenOptions.PowerShell_UpdateThroughInstall = updatesThroughInstall; bool shellInterpreted = standalone || Manager.Status.OperationCallArgs.Count is 0; @@ -133,6 +134,9 @@ int returnCode { string output_string = string.Join("\n", processOutput); + bool routedThroughInstall = package.OverridenOptions.PowerShell_UpdateThroughInstall; + package.OverridenOptions.PowerShell_UpdateThroughInstall = false; + if ( package.OverridenOptions.RunAsAdministrator is not true && ( @@ -156,10 +160,7 @@ package.OverridenOptions.RunAsAdministrator is not true } if ( - ( - operation is OperationType.Install - || package.OverridenOptions.PowerShell_UpdateThroughInstall - ) + (operation is OperationType.Install || routedThroughInstall) && output_string.Contains("CommandAlreadyAvailable") && !package.OverridenOptions.PowerShell_AllowClobber ) diff --git a/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs index ed504e6a5b..59edacde53 100644 --- a/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PowerShellManagerTests.cs @@ -280,10 +280,7 @@ public void GetStandaloneParameters_UpdateThroughInstallModuleQuotesTheRepositor public void GetParameters_UpdateThroughInstallModuleKeepsTheRepositoryRawForTheArgumentVector() { var manager = new PowerShell(); - manager.Initialize(); - - if (manager.Status.OperationCallArgs.Count is 0) - return; + manager.Status.OperationCallArgs = LauncherVector; var package = Assert.Single( PowerShell.ParseInstalledPackages( @@ -304,6 +301,62 @@ public void GetParameters_UpdateThroughInstallModuleKeepsTheRepositoryRawForTheA Assert.Equal("Internal Modules", parameters[repositoryIndex + 1]); } + private static readonly string[] LauncherVector = + [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + @"C:\App\Assets\Utilities\unigetui_ps_operation.ps1", + "tls12", + ]; + + [Fact] + public void GetStandaloneParameters_DoesNotRecordTheRoutingDecisionOnThePackage() + { + var manager = new PowerShell(); + var package = BuildInstalledPackage(manager); + + var options = new InstallOptions { SkipHashCheck = true }; + manager.OperationHelper.GetStandaloneParameters(package, options, OperationType.Update); + + Assert.False(package.OverridenOptions.PowerShell_UpdateThroughInstall); + + var veredict = manager.OperationHelper.GetResult( + package, + OperationType.Update, + ClobberFailureOutput, + 1 + ); + + Assert.Equal(OperationVeredict.Failure, veredict); + Assert.False(package.OverridenOptions.PowerShell_AllowClobber); + } + + [Fact] + public void GetResult_ClearsTheRoutingDecisionSoItCannotLeakIntoALaterOperation() + { + var manager = new PowerShell(); + var package = BuildInstalledPackage(manager); + + var options = new InstallOptions { SkipHashCheck = true }; + manager.OperationHelper.GetParameters(package, options, OperationType.Update); + Assert.True(package.OverridenOptions.PowerShell_UpdateThroughInstall); + + manager.OperationHelper.GetResult(package, OperationType.Update, ["done"], 0); + + Assert.False(package.OverridenOptions.PowerShell_UpdateThroughInstall); + + var veredict = manager.OperationHelper.GetResult( + package, + OperationType.Update, + ClobberFailureOutput, + 1 + ); + + Assert.Equal(OperationVeredict.Failure, veredict); + } + [Fact] public void GetStandaloneParameters_UpdateThroughInstallModuleEscapesAQuotedRepository() {