File: AzureCliCredentialWithAzNoUpdateWrapper.cs
Web Access
Project: src\src\Microsoft.DotNet.ArcadeAzureIntegration\Microsoft.DotNet.ArcadeAzureIntegration.csproj (Microsoft.DotNet.ArcadeAzureIntegration)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#if !NET472_OR_GREATER
 
#nullable enable
 
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;
 
namespace Microsoft.DotNet.ArcadeAzureIntegration;
 
 
// This class is an workaround for disable az cli auto update mechanism which cause timeout when waiting for
// console input in case of new version of az available
// - this wrppper will run "az config set auto-upgrade.enable=no" only once before first call to az for acquiring the token
public class AzureCliCredentialWithAzNoUpdateWrapper : TokenCredential
{
    private readonly AzureCliCredential _azureCliCredential;
 
    public AzureCliCredentialWithAzNoUpdateWrapper(AzureCliCredential azureCliCredential)
    {
        _azureCliCredential = azureCliCredential;
    }
 
    public static string? EnvProgramFilesX86 => GetNonEmptyStringOrNull(Environment.GetEnvironmentVariable("ProgramFiles(x86)"));
    public static string? EnvProgramFiles => GetNonEmptyStringOrNull(Environment.GetEnvironmentVariable("ProgramFiles"));
    public static string? EnvPath => GetNonEmptyStringOrNull(Environment.GetEnvironmentVariable("PATH"));
 
    private static readonly string DefaultPathWindows = $"{EnvProgramFilesX86}\\Microsoft SDKs\\Azure\\CLI2\\wbin;{EnvProgramFiles}\\Microsoft SDKs\\Azure\\CLI2\\wbin";
    private static readonly string DefaultWorkingDirWindows = Environment.GetFolderPath(Environment.SpecialFolder.System);
    private const string DefaultPathNonWindows = "/usr/bin:/usr/local/bin";
    private const string DefaultWorkingDirNonWindows = "/bin/";
    private static readonly string DefaultPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultPathWindows : DefaultPathNonWindows;
    private static readonly string DefaultWorkingDir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultWorkingDirWindows : DefaultWorkingDirNonWindows;
 
    private static string? GetNonEmptyStringOrNull(string? str)
    {
        return !string.IsNullOrEmpty(str) ? str : null;
    }
 
    private static SemaphoreSlim _azCliInitSemaphore = new SemaphoreSlim(1, 1);
    private static bool _azCliInitialized = false;
 
    private async Task SetUpAzAsync()
    {
        await _azCliInitSemaphore.WaitAsync();
        try
        {
            if (_azCliInitialized) return;
 
            string fileName;
            string argument;
            string command = $"az config set auto-upgrade.enable=no";
 
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");
                argument = $"/d /c \"{command}\"";
            }
            else
            {
                fileName = "/bin/sh";
                argument = $"-c \"{command}\"";
            }
 
            string path = !string.IsNullOrEmpty(EnvPath) ? EnvPath : DefaultPath;
            var processInfo = new ProcessStartInfo
            {
                FileName = fileName,
                Arguments = argument,
                UseShellExecute = false,
                ErrorDialog = false,
                CreateNoWindow = true,
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                WorkingDirectory = DefaultWorkingDir,
                Environment = { { "PATH", path } }
            };
 
            using Process? process = Process.Start(processInfo);
            if (process == null)
            {
                throw new InvalidOperationException("Failed to start process to disable auto update of Azure CLI");
            }
 
            using var tokenSource = new CancellationTokenSource();
            tokenSource.CancelAfter(TimeSpan.FromSeconds(30));
            await process.WaitForExitAsync(tokenSource.Token);
 
            if (!process.HasExited)
            {
                // try clean up the process if it is still running after timeout
                try { process.Kill(); } catch { /* ignore this excpetion */}
                throw new InvalidOperationException("Could not finish az config command to disable auto update on time");
            }
 
            process.StandardInput.Close();
            process.Close();
        }
        catch (Exception e)
        {
            // silent catch with direct console output as this is not a critical error
            Console.WriteLine($"Warning - Disable auto update of Azure CLI failed: {e.Message}");
        }
        finally
        {
            _azCliInitialized = true;
            _azCliInitSemaphore.Release();
        }
    }
 
    public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken = default)
    {
        if (!_azCliInitialized)
        {
            SetUpAzAsync().Wait();
        }
        return _azureCliCredential.GetToken(requestContext, cancellationToken);
    }
 
    public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default)
    {
        if (!_azCliInitialized)
        {
            await SetUpAzAsync();
        }
        return await _azureCliCredential.GetTokenAsync(requestContext, cancellationToken);
    }
}
 
#endif