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
4 changes: 4 additions & 0 deletions src/Languages/lang_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,10 @@
"Automatic updates": "Automatic updates",
"Here you can change UniGetUI's behaviour regarding the following shortcuts. Checking a shortcut will make UniGetUI delete it if if gets created on a future upgrade. Unchecking it will keep the shortcut intact": "Here you can change UniGetUI's behaviour regarding the following shortcuts. Checking a shortcut will make UniGetUI delete it if gets created on a future upgrade. Unchecking it will keep the shortcut intact",
"Agent broker unavailable": "Agent broker unavailable",
"The Devolutions Agent package broker is not running": "The Devolutions Agent package broker is not running",
"UniGetUI could not find the package broker of Devolutions Agent on this computer. Make sure that Devolutions Agent is installed and running, and that its package broker is enabled in the Agent configuration. Versions of Devolutions Agent without a package broker cannot run package operations.": "UniGetUI could not find the package broker of Devolutions Agent on this computer. Make sure that Devolutions Agent is installed and running, and that its package broker is enabled in the Agent configuration. Versions of Devolutions Agent without a package broker cannot run package operations.",
"Devolutions Agent refused this copy of UniGetUI": "Devolutions Agent refused this copy of UniGetUI",
"The package broker is running, but it closed the connection without answering. This usually means that it does not trust this copy of UniGetUI: the Devolutions Agent only accepts UniGetUI installed for all users in a folder that only administrators can modify, such as Program Files. If you are running a development or self-built version, install an official release of UniGetUI and try again.": "The package broker is running, but it closed the connection without answering. This usually means that it does not trust this copy of UniGetUI: the Devolutions Agent only accepts UniGetUI installed for all users in a folder that only administrators can modify, such as Program Files. If you are running a development or self-built version, install an official release of UniGetUI and try again.",
"These options cannot be used through the Devolutions Agent, which will refuse the operation:": "These options cannot be used through the Devolutions Agent, which will refuse the operation:",
"--override and --custom pass arbitrary arguments to the package installer. Through the Devolutions Agent, the installer can run with administrator rights, so your organization's policy may block these arguments.": "--override and --custom pass arbitrary arguments to the package installer. Through the Devolutions Agent, the installer can run with administrator rights, so your organization's policy may block these arguments.",
"Custom arguments are passed to WinGet by the Devolutions Agent, which runs this operation with administrator rights. Your organization's policy may block custom arguments.": "Custom arguments are passed to WinGet by the Devolutions Agent, which runs this operation with administrator rights. Your organization's policy may block custom arguments.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,21 @@ private void ApplyManagementResult(
CoreTools.Translate("Communication with the package broker could not be completed. Verify that Devolutions Agent is installed and running. If the problem persists, check the Agent logs, then refresh."),
InfoBarSeverity.Error);
break;
case BrokerPolicyManagementStatus.BrokerNotRunning:
SetManagementStatus(
BrokerFailureDescriber.DescribeBrokerNotRunning(),
InfoBarSeverity.Error);
break;
case BrokerPolicyManagementStatus.ClientRejected:
SetManagementStatus(
BrokerFailureDescriber.DescribeClientRejected(),
InfoBarSeverity.Error);
break;
case BrokerPolicyManagementStatus.ClientNotAuthorized:
SetManagementStatus(
BrokerFailureDescriber.DescribeClientNotAuthorized(),
InfoBarSeverity.Error);
break;
case BrokerPolicyManagementStatus.Unsupported:
SetManagementStatus(
CoreTools.Translate("Policy management is unsupported"),
Expand Down Expand Up @@ -815,6 +830,9 @@ private void AnnounceManagementStatus()
: AutomationLiveSetting.Polite);
}

private void SetManagementStatus(BrokerFailureDescription description, InfoBarSeverity severity) =>
SetManagementStatus(description.Title, description.Message, severity);

private void SetManagementStatus(string title, string message, InfoBarSeverity severity)
{
ManagementStatus.Title = title;
Expand Down
27 changes: 23 additions & 4 deletions src/UniGetUI.PackageEngine.AgentBroker/BrokerFailureDescriber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,12 @@ .. issues.Select(issue => "• " + issue),
/// </summary>
public static BrokerFailureDescription? DescribeAccessFailure(BrokerClientException exception)
{
ErrorCode? code = exception.BrokerError?.Code;
if (exception.StatusCode is 401 || code is ErrorCode.Unauthorized or ErrorCode.Unauthenticated)
if (BrokerPolicyFailure.IsClientNotAuthenticated(exception))
{
return DescribeClientNotAuthorized();
}

if (code is ErrorCode.AdministratorRequired)
if (exception.BrokerError?.Code is ErrorCode.AdministratorRequired)
{
return new(
CoreTools.Translate("Administrator rights are required"),
Expand All @@ -76,12 +75,32 @@ .. issues.Select(issue => "• " + issue),
DescribeAccessFailure(exception)
?? (exception.StatusCode is 403 ? DescribeClientNotAuthorized() : null);

private static BrokerFailureDescription DescribeClientNotAuthorized() =>
public static BrokerFailureDescription DescribeClientNotAuthorized() =>
new(
CoreTools.Translate("UniGetUI is not authorized to use the Devolutions Agent"),
CoreTools.Translate(
"The Devolutions Agent only accepts requests from signed, unmodified copies of UniGetUI. If you are running a development or self-built version, install an official release of UniGetUI and try again."));

public static BrokerFailureDescription DescribeBrokerNotRunning() =>
new(
CoreTools.Translate("The Devolutions Agent package broker is not running"),
CoreTools.Translate(
"UniGetUI could not find the package broker of Devolutions Agent on this computer. Make sure that Devolutions Agent is installed and running, and that its package broker is enabled in the Agent configuration. Versions of Devolutions Agent without a package broker cannot run package operations."));

public static BrokerFailureDescription DescribeClientRejected() =>
new(
CoreTools.Translate("Devolutions Agent refused this copy of UniGetUI"),
CoreTools.Translate(
"The package broker is running, but it closed the connection without answering. This usually means that it does not trust this copy of UniGetUI: the Devolutions Agent only accepts UniGetUI installed for all users in a folder that only administrators can modify, such as Program Files. If you are running a development or self-built version, install an official release of UniGetUI and try again."));

public static BrokerFailureDescription? DescribeUnreachable(BrokerUnreachableCause cause) =>
cause switch
{
BrokerUnreachableCause.NotListening => DescribeBrokerNotRunning(),
BrokerUnreachableCause.ConnectionClosed => DescribeClientRejected(),
_ => null,
};

/// <summary>Explains a failed broker request.</summary>
public static BrokerFailureDescription Describe(BrokerClientException exception)
{
Expand Down
4 changes: 4 additions & 0 deletions src/UniGetUI.PackageEngine.AgentBroker/BrokerPolicyFailure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ exception.Kind is BrokerClientErrorKind.BrokerUnavailable or BrokerClientErrorKi
&& exception.StatusCode is null
&& exception.BrokerError is null);

public static bool IsClientNotAuthenticated(BrokerClientException exception) =>
exception.StatusCode is 401
|| exception.BrokerError?.Code is ErrorCode.Unauthorized or ErrorCode.Unauthenticated;

public static bool IsAccessDenied(BrokerClientException exception) =>
exception.BrokerError is { } error
&& (exception.StatusCode is 401 or 403
Expand Down
72 changes: 72 additions & 0 deletions src/UniGetUI.PackageEngine.AgentBroker/BrokerReachability.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Devolutions.Now.Policy.Api;
using Devolutions.Now.Policy.Client;
using UniGetUI.PackageEngine.AgentBroker.PolicyWriteElevation;

namespace UniGetUI.PackageEngine.AgentBroker;

public enum BrokerUnreachableCause
{
Unknown,
NotListening,
ConnectionClosed,
}

public static class BrokerReachability
{
private const int DiagnosticResponseBodyBytes = 65536;

public static bool BrokerPipeExists()
{
if (!OperatingSystem.IsWindows())
return true;

try
{
return Directory.EnumerateFiles(@"\\.\pipe\", BrokerApi.DefaultPipeName).Any();
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return true;
}
}

public static BrokerUnreachableCause Diagnose(BrokerClientException exception, Func<bool> brokerPipeExists)
{
if (!brokerPipeExists())
return BrokerUnreachableCause.NotListening;

return exception.InnerException is BrokerConnectionClosedException
? BrokerUnreachableCause.ConnectionClosed
: BrokerUnreachableCause.Unknown;
}

public static Task<BrokerUnreachableCause> DiagnoseAsync(CancellationToken cancellationToken) =>
OperatingSystem.IsWindows()
? DiagnoseAsync(BrokerPipeExists, CreateDiagnosticClient, cancellationToken)
: Task.FromResult(BrokerUnreachableCause.Unknown);

internal static async Task<BrokerUnreachableCause> DiagnoseAsync(
Func<bool> brokerPipeExists,
Func<BrokerClient> clientFactory,
CancellationToken cancellationToken)
{
if (!brokerPipeExists())
return BrokerUnreachableCause.NotListening;

try
{
using BrokerClient client = clientFactory();
await client.GetHealth(cancellationToken).ConfigureAwait(false);
return BrokerUnreachableCause.Unknown;
}
catch (BrokerClientException ex)
{
return Diagnose(ex, brokerPipeExists);
}
}

private static BrokerClient CreateDiagnosticClient() =>
BrokerClientFactory.Create(
Elevation.Standard,
new BoundedNamedPipeBrokerTransport(DiagnosticResponseBodyBytes));
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public enum BrokerPolicyManagementStatus
/// <summary>The management snapshot was retrieved and passed validation.</summary>
Retrieved,
AgentUnavailable,
BrokerNotRunning,
ClientRejected,
ClientNotAuthorized,
/// <summary>Ordinary 404 (or an explicit UnsupportedEndpoint error code): the Agent is older and does not support this endpoint.</summary>
Unsupported,
AccessDenied,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ public sealed partial class BrokerPolicyManagementService : IBrokerPolicyManagem
{
private readonly Func<BrokerClient> _clientFactory;
private readonly Func<bool> _isWindows;
private readonly Func<bool>? _brokerPipeExists;

public BrokerPolicyManagementService()
: this(CreateStandardClient, OperatingSystem.IsWindows)
: this(CreateStandardClient, OperatingSystem.IsWindows, BrokerReachability.BrokerPipeExists)
{
}

Expand All @@ -41,10 +42,14 @@ private static BrokerClient CreateStandardClient() =>
ApiElevation.Standard,
new BoundedNamedPipeBrokerTransport(BrokerPolicyManagementLimits.MaxResponseBodyBytes));

public BrokerPolicyManagementService(Func<BrokerClient> clientFactory, Func<bool>? isWindows = null)
public BrokerPolicyManagementService(
Func<BrokerClient> clientFactory,
Func<bool>? isWindows = null,
Func<bool>? brokerPipeExists = null)
{
_clientFactory = clientFactory;
_isWindows = isWindows ?? OperatingSystem.IsWindows;
_brokerPipeExists = brokerPipeExists;
}

public async Task<BrokerPolicyManagementResult> GetManagementAsync(CancellationToken cancellationToken)
Expand Down Expand Up @@ -176,7 +181,7 @@ private static bool IsResponseVersion(string? value)
// per the policy management contract. This follows the BrokerPolicyInspector.MapFailure precedent:
// BrokerClientErrorKind.BrokerError collapses every structured broker error into one kind, so
// disambiguation must happen via StatusCode/BrokerError.Code first.
private static BrokerPolicyManagementStatus MapManagementFailure(BrokerClientException ex)
private BrokerPolicyManagementStatus MapManagementFailure(BrokerClientException ex)
{
if (ex.StatusCode == 404
|| ex.BrokerError?.Code is ErrorCode.NotFound or ErrorCode.UnsupportedEndpoint)
Expand All @@ -186,7 +191,9 @@ private static BrokerPolicyManagementStatus MapManagementFailure(BrokerClientExc

if (BrokerPolicyFailure.IsAccessDenied(ex))
{
return BrokerPolicyManagementStatus.AccessDenied;
return BrokerPolicyFailure.IsClientNotAuthenticated(ex)
? BrokerPolicyManagementStatus.ClientNotAuthorized
: BrokerPolicyManagementStatus.AccessDenied;
}

switch (ex.BrokerError?.Code)
Expand All @@ -201,8 +208,7 @@ private static BrokerPolicyManagementStatus MapManagementFailure(BrokerClientExc

return ex.Kind switch
{
_ when BrokerPolicyFailure.IsTransportUnavailable(ex) =>
BrokerPolicyManagementStatus.AgentUnavailable,
_ when BrokerPolicyFailure.IsTransportUnavailable(ex) => MapUnreachable(ex),
BrokerClientErrorKind.EmptyResponse or BrokerClientErrorKind.InvalidResponse =>
BrokerPolicyManagementStatus.InvalidResponse,
BrokerClientErrorKind.BrokerError when ex.BrokerError is not null =>
Expand All @@ -211,6 +217,16 @@ _ when BrokerPolicyFailure.IsTransportUnavailable(ex) =>
};
}

private BrokerPolicyManagementStatus MapUnreachable(BrokerClientException ex) =>
_brokerPipeExists is null
? BrokerPolicyManagementStatus.AgentUnavailable
: BrokerReachability.Diagnose(ex, _brokerPipeExists) switch
{
BrokerUnreachableCause.NotListening => BrokerPolicyManagementStatus.BrokerNotRunning,
BrokerUnreachableCause.ConnectionClosed => BrokerPolicyManagementStatus.ClientRejected,
_ => BrokerPolicyManagementStatus.AgentUnavailable,
};

private static BrokerPolicyValidationStatus MapValidationFailure(BrokerClientException ex)
{
if (ex.StatusCode == 404
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,19 @@ public async Task<BrokerTransportResponse> Send(
await pipe.ConnectAsync(connectCancellation.Token).ConfigureAwait(false);
}

await WriteRequestAsync(pipe, request, cancellationToken).ConfigureAwait(false);
try
{
await WriteRequestAsync(pipe, request, cancellationToken).ConfigureAwait(false);
}
catch (IOException ex)
{
throw BrokerFailure(
BrokerClientErrorKind.BrokerUnavailable,
$"The package broker closed the connection before accepting the request for {request.Path}.",
request.Path,
new BrokerConnectionClosedException(ex.Message, ex));
}

using CancellationTokenSource readCancellation =
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
readCancellation.CancelAfter(ReadTimeoutMilliseconds);
Expand Down Expand Up @@ -137,10 +149,12 @@ internal static async Task<BrokerTransportResponse> ReadResponseAsync(
.ConfigureAwait(false);
if (read == 0)
{
string message = $"The package broker disconnected before sending a complete response for {path}.";
throw BrokerFailure(
incompleteResponseKind,
$"The package broker disconnected before sending a complete response for {path}.",
path);
message,
path,
totalRead == 0 ? new BrokerConnectionClosedException(message) : null);
}

totalRead += read;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace UniGetUI.PackageEngine.AgentBroker.PolicyWriteElevation;

internal sealed class BrokerConnectionClosedException(string message, Exception? innerException = null)
: IOException(message, innerException);
Loading
Loading