File: DevTunnelCliInstallationManager.cs
Web Access
Project: src\src\Aspire.Hosting.DevTunnels\Aspire.Hosting.DevTunnels.csproj (Aspire.Hosting.DevTunnels)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.DevTunnels;
 
internal sealed class DevTunnelCliInstallationManager : RequiredCommandValidator
{
    private readonly IDevTunnelClient _devTunnelClient;
    private readonly IConfiguration _configuration;
    private readonly ILogger _logger;
    private readonly Version _minSupportedVersion;
    private string? _resolvedCommandPath;
 
#pragma warning disable ASPIREINTERACTION001 // Interaction service is experimental.
    public DevTunnelCliInstallationManager(
        IDevTunnelClient devTunnelClient,
        IConfiguration configuration,
        IInteractionService interactionService,
        ILogger<DevTunnelCliInstallationManager> logger)
        : this(devTunnelClient, configuration, interactionService, logger, DevTunnelCli.MinimumSupportedVersion)
    {
 
    }
 
    public DevTunnelCliInstallationManager(
        IDevTunnelClient devTunnelClient,
        IConfiguration configuration,
        IInteractionService interactionService,
        ILogger<DevTunnelCliInstallationManager> logger,
        Version minSupportedVersion)
        : base(interactionService, logger)
#pragma warning restore ASPIREINTERACTION001
    {
        _devTunnelClient = devTunnelClient ?? throw new ArgumentNullException(nameof(devTunnelClient));
        _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _minSupportedVersion = minSupportedVersion ?? throw new ArgumentNullException(nameof(minSupportedVersion));
    }
 
    /// <summary>
    /// Gets the resolved full path to the devtunnel CLI after a successful validation, otherwise <c>null</c>.
    /// </summary>
    public string? ResolvedCommandPath => _resolvedCommandPath;
 
    /// <summary>
    /// Gets a value indicating whether the CLI was found (after calling <see cref="EnsureInstalledAsync"/>).
    /// </summary>
    public bool IsInstalled => _resolvedCommandPath is not null;
 
    /// <summary>
    /// Ensures the devtunnel CLI is installed/available. This method is safe for concurrent callers;
    /// only one validation will run at a time.
    /// </summary>
    /// <throws cref="DistributedApplicationException">Thrown if the devtunnel CLI is not found.</throws>
    public Task EnsureInstalledAsync(CancellationToken cancellationToken = default) => RunAsync(cancellationToken);
 
    protected override string GetCommandPath() => DevTunnelCli.GetCliPath(_configuration);
 
    protected internal override async Task<(bool IsValid, string? ValidationMessage)> OnResolvedAsync(string resolvedCommandPath, CancellationToken cancellationToken)
    {
        // Verify the version is supported
        var version = await _devTunnelClient.GetVersionAsync(_logger, cancellationToken).ConfigureAwait(false);
        if (version < _minSupportedVersion)
        {
            return (false, string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevtunnelCliVersionNotSupported, version, _minSupportedVersion));
        }
        return (true, null);
    }
 
    protected override Task OnValidatedAsync(string resolvedCommandPath, CancellationToken cancellationToken)
    {
        _resolvedCommandPath = resolvedCommandPath;
        return Task.CompletedTask;
    }
 
    protected override string? GetHelpLink() => "https://learn.microsoft.com/azure/developer/dev-tunnels/get-started#install";
}