diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/AzCliRunner.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/AzCliRunner.cs index 85e6461ba..b3103173d 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/AzCliRunner.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/AzCliRunner.cs @@ -16,7 +16,7 @@ public static AzCliRunner Create(string? commandName = null) return new AzCliRunner(commandName); } - public int RunAzCli(string arguments, out string? stdOut, out string? stdErr) + public async Task<(int ExitCode, string? StdOut, string? StdErr)> RunAzCliAsync(string arguments, CancellationToken cancellationToken) { using var outStream = new ProcessOutputStreamReader(); using var errStream = new ProcessOutputStreamReader(); @@ -42,19 +42,30 @@ public int RunAzCli(string arguments, out string? stdOut, out string? stdErr) } catch (Exception ex) { - stdOut = string.Empty; - stdErr = ex.Message; - return -1; + return (-1, string.Empty, ex.Message); } - var taskOut = outStream.BeginRead(process.StandardOutput); - var taskErr = errStream.BeginRead(process.StandardError); + Task taskOut = outStream.BeginRead(process.StandardOutput); + Task taskErr = errStream.BeginRead(process.StandardError); - // Wait for process to exit with a timeout (e.g., 30 seconds) + // Wait for process to exit with a timeout (e.g., 30 seconds) or cancellation const int timeoutMilliseconds = 30000; - bool exited = process.WaitForExit(timeoutMilliseconds); + Task waitForExitTask = Task.Run(() => process.WaitForExit(timeoutMilliseconds), cancellationToken); + try + { + await Task.WhenAny(waitForExitTask, Task.Delay(Timeout.Infinite, cancellationToken)); + } + catch (OperationCanceledException) + { + try + { + process.Kill(entireProcessTree: true); + } + catch { } + throw; + } - if (!exited) + if (!waitForExitTask.Result) { try { @@ -66,13 +77,12 @@ public int RunAzCli(string arguments, out string? stdOut, out string? stdErr) } } - taskOut.Wait(); - taskErr.Wait(); + await Task.WhenAll(taskOut, taskErr); - stdOut = outStream.CapturedOutput?.Trim(); - stdErr = errStream.CapturedOutput; + string? stdOut = outStream.CapturedOutput?.Trim(); + string? stdErr = errStream.CapturedOutput; - return process.ExitCode; + return (process.ExitCode, stdOut, stdErr); } internal ProcessStartInfo _psi; diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/AspNetCommandService.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/AspNetCommandService.cs index a10435d53..eda9df909 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/AspNetCommandService.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/AspNetCommandService.cs @@ -44,7 +44,7 @@ public Type[] GetScaffoldSteps() ]; } - public void AddScaffolderCommands() + public async Task AddScaffolderCommandsAsync(CancellationToken cancellationToken) { _builder.AddScaffolder(ScaffolderCatagory.AspNet, AspnetStrings.Blazor.Empty) .WithDisplayName(AspnetStrings.Blazor.EmptyDisplayName) @@ -309,7 +309,7 @@ public void AddScaffolderCommands() .WithIdentityTextTemplatingStep() .WithIdentityCodeChangeStep(); - AspNetOptions options = new(); + AspNetOptions options = await AspNetOptions.CreateAsync(cancellationToken); if (options.AreAzCliCommandsSuccessful()) { @@ -340,5 +340,10 @@ public void AddScaffolderCommands() .WithEntraIdTextTemplatingStep(); } } + + public void AddScaffolderCommands() + { + //ignore + } } } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Commands/AspNetOptions.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Commands/AspNetOptions.cs index a9ddba5b6..b086398c1 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Commands/AspNetOptions.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Commands/AspNetOptions.cs @@ -161,23 +161,24 @@ internal class AspNetOptions PickerType = InteractivePickerType.YesNo }; - private readonly bool _areAzCliCommandsSuccessful; - private readonly List _usernames = []; - private readonly List _tenants = []; - private readonly List _appIds = []; private ScaffolderOption? _username = null; private ScaffolderOption? _tenantId = null; private ScaffolderOption? _applicationId = null; + private AzureInformation? _azureInformation; - public AspNetOptions() + + private AspNetOptions(AzureInformation? azureInformation) + { + _azureInformation = azureInformation; + } + + public static async Task CreateAsync(CancellationToken cancellationToken) { - _areAzCliCommandsSuccessful = AzCliHelper.GetAzureInformation(out List usernames, out List tenants, out List appIds); - _usernames = usernames; - _tenants = tenants; - _appIds = appIds; + AzureInformation? azureInfo = await AzCliHelper.GetAzureInformationAsync(cancellationToken); + return new AspNetOptions(azureInfo); } - public bool AreAzCliCommandsSuccessful() => _areAzCliCommandsSuccessful; + public bool AreAzCliCommandsSuccessful() => _azureInformation is not null; public ScaffolderOption Username => _username ??= new() { @@ -186,7 +187,7 @@ public AspNetOptions() Description = AspnetStrings.Options.Username.Description, Required = true, PickerType = InteractivePickerType.CustomPicker, - CustomPickerValues = _usernames + CustomPickerValues = _azureInformation!.Usernames }; @@ -197,7 +198,7 @@ public AspNetOptions() Description = AspnetStrings.Options.TenantId.Description, Required = true, PickerType = InteractivePickerType.CustomPicker, - CustomPickerValues = _tenants + CustomPickerValues = _azureInformation!.Tenants }; public static ScaffolderOption Application => new() @@ -216,6 +217,6 @@ public AspNetOptions() Description = AspnetStrings.Options.SelectApplication.Description, Required = false, PickerType = InteractivePickerType.CustomPicker, - CustomPickerValues = _appIds + CustomPickerValues = _azureInformation!.AppIds }; } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Helpers/AzCliHelper.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Helpers/AzCliHelper.cs index 8deca82d8..93cc31a71 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Helpers/AzCliHelper.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Helpers/AzCliHelper.cs @@ -13,60 +13,53 @@ internal class AzCliHelper /// /// Gets Azure usernames, tenant IDs, and application IDs using the Azure CLI. /// - /// the user IDs - /// the tenant IDs - /// the app IDs - /// if successful, return true - public static bool GetAzureInformation(out List usernames, out List tenants, out List appIds) + /// non null AzureInformation if the commands run successfully + public static async Task GetAzureInformationAsync(CancellationToken cancellationToken) { // Create a runner to execute the 'az account list' command with json output format var runner = AzCliRunner.Create(); - - if (EnsureUserIsLoggedIn(runner, out string? output) && !string.IsNullOrEmpty(output)) + (bool isUserLoggedIn, string? output) = await EnsureUserIsLoggedInAsync(runner, cancellationToken); + if (isUserLoggedIn && !string.IsNullOrEmpty(output)) { - if (GetAzureUsernamesAndTenatIds(runner, output, out usernames, out tenants)) + if (GetAzureUsernamesAndTenatIds(runner, output, out List usernames, out List tenants)) { - if (GetAzureAppIds(runner, out appIds)) + (bool areAppIdSuccessful, List appIds) = await GetAzureAppIdsAsync(runner, cancellationToken); + if (areAppIdSuccessful) { - return true; + return new AzureInformation(usernames, tenants, appIds); } } } - usernames = []; - tenants = []; - appIds = []; - return false; + return null; } /// /// Ensures the user is logged into Azure CLI. If not logged in, it will prompt for login. /// /// the az cli runner - /// the CLI output if available - /// if successful, return true - private static bool EnsureUserIsLoggedIn(AzCliRunner runner, out string? output) + /// the cancellation token + /// if successful, return true and the output if applicable + private static async Task<(bool success, string? output)> EnsureUserIsLoggedInAsync(AzCliRunner runner, CancellationToken cancellationToken) { try { - int exitCode = runner.RunAzCli("account list --output json", out var stdOut, out var stdErr); + (int exitCode, string? stdOut, string? stdErr) = await runner.RunAzCliAsync("account list --output json", cancellationToken); if (stdOut is not null) { var result = StringUtil.ConvertStringToArray(stdOut); if (result.Length is 0) { - exitCode = runner.RunAzCli("login", out stdOut, out stdErr); + (exitCode, stdOut, stdErr) = await runner.RunAzCliAsync("login", cancellationToken); } } - output = stdOut; - return exitCode == 0 && string.IsNullOrEmpty(stdErr); + return (exitCode == 0 && string.IsNullOrEmpty(stdErr), stdOut); } catch (Exception ex) { - output = null; AnsiConsole.WriteLine($"Error checking Azure login status: {ex.Message}"); - return false; + return (false, null); } } @@ -132,15 +125,14 @@ private static bool GetAzureUsernamesAndTenatIds(AzCliRunner runner, string outp /// Gets Azure application IDs using the Azure CLI. /// /// the az cli runner - /// the appIds - /// if successful, returns true - private static bool GetAzureAppIds(AzCliRunner runner, out List appIds) + /// the cancellation token + /// if successful, returns true with the appIds if retrieved + private static async Task<(bool, List appIds)> GetAzureAppIdsAsync(AzCliRunner runner, CancellationToken cancellationToken) { try { - - appIds = []; - var exitCode = runner.RunAzCli("ad app list --output json", out string? stdOut, out string? stdErr); + List appIds = []; + (int exitCode, string? stdOut, string? stdErr) = await runner.RunAzCliAsync("ad app list --output json", cancellationToken); if (exitCode == 0 && !string.IsNullOrEmpty(stdOut)) { @@ -166,7 +158,7 @@ private static bool GetAzureAppIds(AzCliRunner runner, out List appIds) } } } - return true; + return (true, appIds); } } @@ -177,10 +169,9 @@ private static bool GetAzureAppIds(AzCliRunner runner, out List appIds) } catch (Exception ex) { - appIds = []; // Handle any exceptions, like az CLI not being installed AnsiConsole.WriteLine($"Error getting Azure apps: {ex.Message}"); } - return false; + return (false, []); } } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Helpers/AzureAccountInformation.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Helpers/AzureAccountInformation.cs new file mode 100644 index 000000000..88f752377 --- /dev/null +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Helpers/AzureAccountInformation.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Tools.Scaffold.AspNet.Helpers; + +internal record AzureInformation(List Usernames, List Tenants, List AppIds); diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/UpdateAppAuthorizationStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/UpdateAppAuthorizationStep.cs index 50126143b..b32192ef8 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/UpdateAppAuthorizationStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/UpdateAppAuthorizationStep.cs @@ -28,11 +28,11 @@ public UpdateAppAuthorizationStep(ILogger logger, IF _telemetryService = telemetryService; } - public override Task ExecuteAsync(ScaffolderContext context, CancellationToken cancellationToken = default) + public override async Task ExecuteAsync(ScaffolderContext context, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(ClientId)) { - return Task.FromResult(false); + return false; } if (AutoConfigureLocalUrls) @@ -59,16 +59,16 @@ public override Task ExecuteAsync(ScaffolderContext context, CancellationT command += $" --public-client-redirect-uris {spaUrisJson}"; } - var exitCode = runner.RunAzCli(command, out var stdOut, out var stdErr); + (int exitCode, string? stdOut, string? stdErr) = await runner.RunAzCliAsync(command, cancellationToken); if (exitCode != 0 || !string.IsNullOrEmpty(stdErr)) { _logger.LogError($"Failed to update app registration: {stdErr}"); - return Task.FromResult(false); + return false; } _logger.LogInformation($"Updated App registration with ID token configuration and redirect URIs"); - return Task.FromResult(true); + return true; } private void ConfigureLocalRedirectUris(string projectPath) diff --git a/src/dotnet-scaffolding/dotnet-scaffold/Aspire/AspireCommandService.cs b/src/dotnet-scaffolding/dotnet-scaffold/Aspire/AspireCommandService.cs index e2744e0a5..cb91bb529 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/Aspire/AspireCommandService.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/Aspire/AspireCommandService.cs @@ -74,4 +74,10 @@ public void AddScaffolderCommands() .WithStorageAddPackageSteps() .WithStorageCodeModificationSteps(); } + + public Task AddScaffolderCommandsAsync(CancellationToken cancellationToken) + { + //ignore + throw new NotImplementedException(); + } } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/Command/ICommandService.cs b/src/dotnet-scaffolding/dotnet-scaffold/Command/ICommandService.cs index e67d52d8c..42cc6a80f 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/Command/ICommandService.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/Command/ICommandService.cs @@ -7,6 +7,8 @@ internal interface ICommandService { void AddScaffolderCommands(); + Task AddScaffolderCommandsAsync(CancellationToken cancellationToken); + Type[] GetScaffoldSteps(); } } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/Program.cs b/src/dotnet-scaffolding/dotnet-scaffold/Program.cs index 9daa58848..d1ef27ca1 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/Program.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/Program.cs @@ -15,6 +15,12 @@ IScaffoldRunnerBuilder builder = Host.CreateScaffoldBuilder(); +using var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (sender, e) => { + e.Cancel = true; + cts.Cancel(); +}; + ConfigureServices(builder.Services); ConfigureSharedSteps(builder.Services); @@ -23,11 +29,13 @@ Option nonInteractiveOption = nonInteractiveScaffoldOption.ToCliOption(); AspireCommandService aspireCommandService = new(builder); + +//aspire command adding does not need to be async aspireCommandService.AddScaffolderCommands(); ConfigureCommandSteps(builder.Services, aspireCommandService); AspNetCommandService aspNetCommandService = new(builder); -aspNetCommandService.AddScaffolderCommands(); +await aspNetCommandService.AddScaffolderCommandsAsync(cts.Token); ConfigureCommandSteps(builder.Services, aspNetCommandService); IScaffoldRunner runner = builder.Build();