Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Languages/lang_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1072,5 +1072,8 @@
"UniGetUI is running in portable mode and started with empty settings. Settings from a previous installation were found at {0}.": "UniGetUI is running in portable mode and started with empty settings. Settings from a previous installation were found at {0}.",
"Settings imported": "Settings imported",
"{0} file(s) were copied. Restart UniGetUI to apply them.": "{0} file(s) were copied. Restart UniGetUI to apply them.",
"Could not import settings": "Could not import settings"
"Could not import settings": "Could not import settings",
"The installer for {package} does not match the hash in its manifest": "The installer for {package} does not match the hash in its manifest",
"The package manifest is likely out of date. WinGet cannot skip this check while running as administrator.": "The package manifest is likely out of date. WinGet cannot skip this check while running as administrator.",
"The package manifest is likely out of date. Skipping this check requires WinGet's InstallerHashOverride administrator setting.": "The package manifest is likely out of date. Skipping this check requires WinGet's InstallerHashOverride administrator setting."
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ public static (bool AsAdmin, bool Interactive, bool SkipHash) GetRetryModes(Oper
var options = LoadOptions(record);
bool asAdmin = manager.Capabilities.CanRunAsAdmin && !options.RunAsAdministrator;
bool interactive = manager.Capabilities.CanRunInteractively && !options.InteractiveInstallation;
bool skipHash = manager.Capabilities.CanSkipIntegrityChecks && !options.SkipHashCheck
bool skipHash = PackageOperation.CanRetrySkippingIntegrityChecks(
manager, options, (OperationType)record.Role,
record.RanElevated ?? (CoreTools.IsAdministrator() || options.RunAsAdministrator))
&& record.Role != (int)OperationType.Uninstall;
Comment thread
GabrielDuf marked this conversation as resolved.
return (asAdmin, interactive, skipHash);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,8 @@ private void RebuildMenu(OperationStatus status)
OpMenu.Items.Add(Item("Retry interactively", "interactive.svg", true,
() => Operation.Retry(AbstractOperation.RetryMode.Retry_Interactive)));

if (!pkgOp.Options.SkipHashCheck && caps.CanSkipIntegrityChecks)
if (PackageOperation.CanRetrySkippingIntegrityChecks(
pkgOp.Package.Manager, pkgOp.Options, pkgOp.Role, pkgOp.WillRunElevated))
OpMenu.Items.Add(Item("Retry skipping integrity checks", "checksum.svg", true,
() => Operation.Retry(AbstractOperation.RetryMode.Retry_SkipIntegrity)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ private Control BuildRetryButton(AbstractOperation operation)
retryOptions.Add(MenuItem(CoreTools.Translate("Retry interactively"),
() => { operation.Retry(AbstractOperation.RetryMode.Retry_Interactive); Close(); }));

if (!pkgOp.Options.SkipHashCheck && caps.CanSkipIntegrityChecks)
if (PackageOperation.CanRetrySkippingIntegrityChecks(
pkgOp.Package.Manager, pkgOp.Options, pkgOp.Role, pkgOp.WillRunElevated))
retryOptions.Add(MenuItem(CoreTools.Translate("Retry skipping integrity checks"),
() => { operation.Retry(AbstractOperation.RetryMode.Retry_SkipIntegrity); Close(); }));
}
Expand Down
8 changes: 6 additions & 2 deletions src/UniGetUI.Interface.IpcApi/IpcOperationApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,12 @@ private static IReadOnlyList<string> GetRetryModes(AbstractOperation operation)
}

if (
!packageOperation.Options.SkipHashCheck
&& packageOperation.Package.Manager.Capabilities.CanSkipIntegrityChecks
PackageOperation.CanRetrySkippingIntegrityChecks(
packageOperation.Package.Manager,
packageOperation.Options,
packageOperation.Role,
packageOperation.WillRunElevated
)
)
{
retryModes.Add("retry-no-hash-check");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ operation is OperationType.Uninstall
return OperationVeredict.AutoRetry;
}

if (uintCode is 0x8A150011)
{ // TODO: Integrity failed
if (ReportedInstallerHashMismatch(returnCode))
{
return OperationVeredict.Failure;
}

Expand Down Expand Up @@ -430,6 +430,9 @@ private static void RecordUpgradeAttempt(string id, string version)
);
}

internal static bool ReportedInstallerHashMismatch(int returnCode) =>
(uint)returnCode is 0x8A150011;

internal bool ReportedUpdateNotApplicable(
IReadOnlyList<string> processOutput,
int returnCode
Expand Down
6 changes: 6 additions & 0 deletions src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ int returnCode
&& helper.ReportedUpdateNotApplicable(processOutput, returnCode);
}

public bool ReportedInstallerHashMismatch(int returnCode) =>
WinGetPkgOperationHelper.ReportedInstallerHashMismatch(returnCode);

public bool HonorsIntegrityCheckSkipWhenElevated =>
SelectedCliToolKind is WinGetCliToolKind.BundledPinget;

protected override IReadOnlyList<Package> FindPackages_UnSafe(string query)
{
return WinGetHelper.Instance.FindPackages_UnSafe(query);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ public sealed class OperationHistoryRecord
public string OptionsJson { get; set; } = "";
/// <summary>Process exit code, when the operation ran a process (null otherwise).</summary>
public int? ExitCode { get; set; }
/// <summary>
/// Whether the operation actually ran elevated, for package operations. Null on records
/// written before this was tracked, and for operations that never ran a process.
/// </summary>
public bool? RanElevated { get; set; }
/// <summary>Short human-readable reason, derived from the last error line (mainly for failures).</summary>
public string FailureSummary { get; set; } = "";
public List<OperationHistoryOutputLine> Output { get; set; } = [];
Expand Down Expand Up @@ -93,6 +98,7 @@ public static OperationHistoryRecord FromOperation(AbstractOperation op, string
_ => pop.Package.VersionString,
};
record.OptionsJson = pop.Options.AsJsonString();
record.RanElevated = pop.WillRunElevated;
Comment thread
GabrielDuf marked this conversation as resolved.
break;
case DownloadOperation dop:
record.PackageId = dop.Package.Id;
Expand Down
75 changes: 73 additions & 2 deletions src/UniGetUI.PackageEngine.Operations/PackageOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,43 @@
!Settings.Get(Settings.K.ProhibitElevation)
&& (Package.OverridenOptions.RunAsAdministrator is true || Options.RunAsAdministrator);

private volatile int _ranElevated = -1;

public virtual bool WillRunElevated =>
_ranElevated switch
{
1 => true,
0 => false,
_ => CoreTools.IsAdministrator() || RequiresAdminRights(),
};

public static bool CanRetrySkippingIntegrityChecks(
IPackageManager manager,
InstallOptions options,
OperationType role,
bool willRunElevated
)
{
if (!manager.Capabilities.CanSkipIntegrityChecks || options.SkipHashCheck)
return false;

return IntegrityCheckSkipIsHonored(manager, role, willRunElevated);
}

private static bool IntegrityCheckSkipIsHonored(
IPackageManager manager,
OperationType role,
bool willRunElevated
)
{
#if WINDOWS
if (manager is WinGet winget)
return role is not OperationType.Uninstall
&& (!willRunElevated || winget.HonorsIntegrityCheckSkipWhenElevated);
#endif
return true;
}

protected override void ApplyRetryAction(string retryMode)
{
switch (retryMode)
Expand Down Expand Up @@ -242,6 +279,8 @@
process.StartInfo.StandardOutputEncoding = Package.Manager.OutputEncoding;
process.StartInfo.StandardErrorEncoding = Package.Manager.OutputEncoding;

_ranElevated = IsAdmin ? 1 : 0;

ApplyCapabilities(
IsAdmin,
Options.InteractiveInstallation,
Expand Down Expand Up @@ -311,6 +350,7 @@
Package.Manager.OperationHelper.ApplyElevationRequirements(Package, Options, Role);

bool requestElevated = RequiresAdminRights();
_ranElevated = requestElevated ? 1 : 0;
using var client = CreateBrokerClient(requestElevated);

// Check broker availability. Brokered operations must not fall back to local
Expand Down Expand Up @@ -900,13 +940,17 @@
ReturnCode
);

if (veredict is OperationVeredict.Failure && Role is OperationType.Update)
ExplainNotApplicableUpdate(Output, ReturnCode);
if (veredict is OperationVeredict.Failure)
{
if (Role is OperationType.Update)
ExplainNotApplicableUpdate(Output, ReturnCode);
ExplainInstallerHashMismatch(ReturnCode);
}

return Task.FromResult(veredict);
}

private void ExplainNotApplicableUpdate(List<string> output, int returnCode)

Check warning on line 953 in src/UniGetUI.PackageEngine.Operations/PackageOperations.cs

View workflow job for this annotation

GitHub Actions / Linux (Avalonia)

Member 'ExplainNotApplicableUpdate' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 953 in src/UniGetUI.PackageEngine.Operations/PackageOperations.cs

View workflow job for this annotation

GitHub Actions / Linux (NativeAOT)

Member 'ExplainNotApplicableUpdate' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 953 in src/UniGetUI.PackageEngine.Operations/PackageOperations.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ExplainNotApplicableUpdate' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)
{
#if WINDOWS
if (Package.Manager is not WinGet winget)
Expand All @@ -922,6 +966,33 @@
#endif
}

private void ExplainInstallerHashMismatch(int returnCode)

Check warning on line 969 in src/UniGetUI.PackageEngine.Operations/PackageOperations.cs

View workflow job for this annotation

GitHub Actions / Linux (Avalonia)

Member 'ExplainInstallerHashMismatch' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 969 in src/UniGetUI.PackageEngine.Operations/PackageOperations.cs

View workflow job for this annotation

GitHub Actions / Linux (NativeAOT)

Member 'ExplainInstallerHashMismatch' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 969 in src/UniGetUI.PackageEngine.Operations/PackageOperations.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Member 'ExplainInstallerHashMismatch' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)
{
#if WINDOWS
if (Package.Manager is not WinGet winget)
return;

if (!winget.ReportedInstallerHashMismatch(returnCode))
return;

Metadata.FailureMessage = CoreTools.Translate(
"The installer for {package} does not match the hash in its manifest",
new Dictionary<string, object?> { { "package", Package.Name } }
);

Line(
WillRunElevated && !winget.HonorsIntegrityCheckSkipWhenElevated
? CoreTools.Translate(
"The package manifest is likely out of date. WinGet cannot skip this check while running as administrator."
)
: CoreTools.Translate(
"The package manifest is likely out of date. Skipping this check requires WinGet's InstallerHashOverride administrator setting."
),
LineType.Error
);
#endif
}

private static bool IsWinGetManager(IPackageManager manager)
{
#if WINDOWS
Expand Down
76 changes: 76 additions & 0 deletions src/UniGetUI.PackageEngine.Tests/OperationHistoryTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using UniGetUI.PackageEngine.Enums;
using UniGetUI.PackageEngine.Interfaces;
using UniGetUI.PackageEngine.Operations;
using UniGetUI.PackageEngine.Operations.History;
using UniGetUI.PackageEngine.Serializable;
Expand Down Expand Up @@ -107,6 +108,7 @@ public void PersistsToDiskAndReloads()
var record = Record("persisted");
record.Output.Add(new OperationHistoryOutputLine { Text = "line one", Type = "Information" });
record.Output.Add(new OperationHistoryOutputLine { Text = "boom", Type = "Error" });
record.RanElevated = true;
OperationHistoryStore.Add(record);

// Drop the in-memory cache so the next read must deserialize the file.
Expand All @@ -118,8 +120,51 @@ public void PersistsToDiskAndReloads()
Assert.Equal(2, reloaded.Output.Count);
Assert.Equal("boom", reloaded.Output[1].Text);
Assert.Equal("Error", reloaded.Output[1].Type);
Assert.True(reloaded.RanElevated);
}

[Fact]
public void PersistsANonElevatedRunAsFalseRatherThanUnknown()
{
var record = Record("standard");
record.RanElevated = false;
OperationHistoryStore.Add(record);
OperationHistoryStore.InvalidateCache();

var reloaded = OperationHistoryStore.Get("standard");
Assert.NotNull(reloaded);
Assert.False(reloaded!.RanElevated);
}

[Fact]
public void RecordsWrittenBeforeElevationWasTrackedReloadAsUnknown()
{
File.WriteAllText(_tempFile, LegacyRecordJson);
OperationHistoryStore.InvalidateCache();

var reloaded = OperationHistoryStore.Get("legacy");
Assert.NotNull(reloaded);
Assert.Equal("Contoso.Legacy", reloaded!.PackageId);
Assert.Null(reloaded.RanElevated);
}

private const string LegacyRecordJson = """
[
{
"Id": "legacy",
"Kind": "install-package",
"Role": 0,
"PackageId": "Contoso.Legacy",
"PackageName": "Contoso Legacy",
"ManagerName": "winget",
"SourceName": "winget",
"Status": "succeeded",
"TimestampUtc": "2026-01-01T00:00:00.0000000Z",
"Output": []
}
]
""";

[Fact]
public void CapsAtMaxEntries()
{
Expand Down Expand Up @@ -223,6 +268,37 @@ public void FromOperation_Install_CapturesKindRoleAndVersion()
Assert.Equal(OperationHistoryRecord.StatusSucceeded, record.Status);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void FromOperation_CapturesTheElevationTheOperationRanWith(bool elevated)
{
var manager = new PackageManagerBuilder().WithName("Scoop").Build();
var package = new PackageBuilder()
.WithManager(manager)
.WithId("Contoso.Tool")
.WithVersion("1.2.3")
.Build();

using var op = new ElevationStubInstallOperation(package, new InstallOptions(), elevated);
var record = OperationHistoryRecord.FromOperation(op, OperationHistoryRecord.StatusSucceeded);

Assert.Equal(elevated, record.RanElevated);
}

private sealed class ElevationStubInstallOperation : InstallPackageOperation
{
private readonly bool _elevated;

public ElevationStubInstallOperation(IPackage package, InstallOptions options, bool elevated)
: base(package, options, IgnoreParallelInstalls: true)
{
_elevated = elevated;
}

public override bool WillRunElevated => _elevated;
}

// The package a Discover install starts from carries the feed's LATEST version, while the
// user may have pinned an older one in the install options. Recording the package version
// then claims a version that was never installed - and the retry-from-history flow rebuilds
Expand Down
Loading
Loading