Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<bool> waitForExitTask = Task.Run(() => process.WaitForExit(timeoutMilliseconds), cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this use WaitForExitAsync instead, and simplify the code below a little? It's .NET 5+ (not sure what targets this needs to run on).

Could make an extensions method for this. Something like:

public static async Task<bool> WaitForExitAsync(
    this Process process,
    TimeSpan timeout,
    CancellationToken cancellationToken = default)
{
    using var timeoutCts = new CancellationTokenSource(timeout);
    using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);

    try
    {
        await process.WaitForExitAsync(linkedCts.Token);

        return !timeoutCts.IsCancellationRequested;
    }
    catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
    {
        // Timed out.
        return false;
    }
}

Another option would be to register a callback on the cancellation token that performs the kill.

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
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -309,7 +309,7 @@ public void AddScaffolderCommands()
.WithIdentityTextTemplatingStep()
.WithIdentityCodeChangeStep();

AspNetOptions options = new();
AspNetOptions options = await AspNetOptions.CreateAsync(cancellationToken);

if (options.AreAzCliCommandsSuccessful())
{
Expand Down Expand Up @@ -340,5 +340,10 @@ public void AddScaffolderCommands()
.WithEntraIdTextTemplatingStep();
}
}

public void AddScaffolderCommands()
{
//ignore
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,23 +161,24 @@ internal class AspNetOptions
PickerType = InteractivePickerType.YesNo
};

private readonly bool _areAzCliCommandsSuccessful;
private readonly List<string> _usernames = [];
private readonly List<string> _tenants = [];
private readonly List<string> _appIds = [];
private ScaffolderOption<string>? _username = null;
private ScaffolderOption<string>? _tenantId = null;
private ScaffolderOption<string>? _applicationId = null;
private AzureInformation? _azureInformation;

public AspNetOptions()

private AspNetOptions(AzureInformation? azureInformation)
{
_azureInformation = azureInformation;
}

public static async Task<AspNetOptions> CreateAsync(CancellationToken cancellationToken)
{
_areAzCliCommandsSuccessful = AzCliHelper.GetAzureInformation(out List<string> usernames, out List<string> tenants, out List<string> 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<string> Username => _username ??= new()
{
Expand All @@ -186,7 +187,7 @@ public AspNetOptions()
Description = AspnetStrings.Options.Username.Description,
Required = true,
PickerType = InteractivePickerType.CustomPicker,
CustomPickerValues = _usernames
CustomPickerValues = _azureInformation!.Usernames
Copy link
Member

@drewnoakes drewnoakes Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't appear to be safe to suppress these null checks. The previous code would have a default empty collection here if getting Azure info failed. This will have null, and this will throw. Consider something like:

Suggested change
CustomPickerValues = _azureInformation!.Usernames
CustomPickerValues = _azureInformation?.Usernames ?? []

Ditto below two more times.

};


Expand All @@ -197,7 +198,7 @@ public AspNetOptions()
Description = AspnetStrings.Options.TenantId.Description,
Required = true,
PickerType = InteractivePickerType.CustomPicker,
CustomPickerValues = _tenants
CustomPickerValues = _azureInformation!.Tenants
};

public static ScaffolderOption<string> Application => new()
Expand All @@ -216,6 +217,6 @@ public AspNetOptions()
Description = AspnetStrings.Options.SelectApplication.Description,
Required = false,
PickerType = InteractivePickerType.CustomPicker,
CustomPickerValues = _appIds
CustomPickerValues = _azureInformation!.AppIds
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,60 +13,53 @@ internal class AzCliHelper
/// <summary>
/// Gets Azure usernames, tenant IDs, and application IDs using the Azure CLI.
/// </summary>
/// <param name="usernames">the user IDs</param>
/// <param name="tenants">the tenant IDs</param>
/// <param name="appIds">the app IDs</param>
/// <returns>if successful, return true</returns>
public static bool GetAzureInformation(out List<string> usernames, out List<string> tenants, out List<string> appIds)
/// <returns>non null AzureInformation if the commands run successfully</returns>
public static async Task<AzureInformation?> 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<string> usernames, out List<string> tenants))
{
if (GetAzureAppIds(runner, out appIds))
(bool areAppIdSuccessful, List<string> appIds) = await GetAzureAppIdsAsync(runner, cancellationToken);
if (areAppIdSuccessful)
{
return true;
return new AzureInformation(usernames, tenants, appIds);
}
}
}
usernames = [];
tenants = [];
appIds = [];

return false;
return null;
}

/// <summary>
/// Ensures the user is logged into Azure CLI. If not logged in, it will prompt for login.
/// </summary>
/// <param name="runner">the az cli runner</param>
/// <param name="output">the CLI output if available</param>
/// <returns>if successful, return true</returns>
private static bool EnsureUserIsLoggedIn(AzCliRunner runner, out string? output)
/// <param name="cancellationToken">the cancellation token</param>
/// <returns>if successful, return true and the output if applicable</returns>
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)
Comment on lines 51 to 52
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR, but this seems to allocate an array when all it really wants to know is that there was at least one value. If stdOut is huge and there are many items, this might be wasteful.

{
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);
}
}

Expand Down Expand Up @@ -132,15 +125,14 @@ private static bool GetAzureUsernamesAndTenatIds(AzCliRunner runner, string outp
/// Gets Azure application IDs using the Azure CLI.
/// </summary>
/// <param name="runner">the az cli runner</param>
/// <param name="appIds"> the appIds</param>
/// <returns>if successful, returns true</returns>
private static bool GetAzureAppIds(AzCliRunner runner, out List<string> appIds)
/// <param name="cancellationToken">the cancellation token</param>
/// <returns>if successful, returns true with the appIds if retrieved</returns>
private static async Task<(bool, List<string> appIds)> GetAzureAppIdsAsync(AzCliRunner runner, CancellationToken cancellationToken)
{
try
{

appIds = [];
var exitCode = runner.RunAzCli("ad app list --output json", out string? stdOut, out string? stdErr);
List<string> appIds = [];
(int exitCode, string? stdOut, string? stdErr) = await runner.RunAzCliAsync("ad app list --output json", cancellationToken);

if (exitCode == 0 && !string.IsNullOrEmpty(stdOut))
{
Expand All @@ -166,7 +158,7 @@ private static bool GetAzureAppIds(AzCliRunner runner, out List<string> appIds)
}
}
}
return true;
return (true, appIds);
}
}

Expand All @@ -177,10 +169,9 @@ private static bool GetAzureAppIds(AzCliRunner runner, out List<string> 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, []);
}
}
Original file line number Diff line number Diff line change
@@ -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<string> Usernames, List<string> Tenants, List<string> AppIds);
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ public UpdateAppAuthorizationStep(ILogger<UpdateAppAuthorizationStep> logger, IF
_telemetryService = telemetryService;
}

public override Task<bool> ExecuteAsync(ScaffolderContext context, CancellationToken cancellationToken = default)
public override async Task<bool> ExecuteAsync(ScaffolderContext context, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(ClientId))
{
return Task.FromResult(false);
return false;
}

if (AutoConfigureLocalUrls)
Expand All @@ -59,16 +59,16 @@ public override Task<bool> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,10 @@ public void AddScaffolderCommands()
.WithStorageAddPackageSteps()
.WithStorageCodeModificationSteps();
}

public Task AddScaffolderCommandsAsync(CancellationToken cancellationToken)
{
//ignore
throw new NotImplementedException();
Comment on lines +80 to +81
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not obvious to me why this is safe to ignore. Can it be described in the comment?

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ internal interface ICommandService
{
void AddScaffolderCommands();

Task AddScaffolderCommandsAsync(CancellationToken cancellationToken);

Type[] GetScaffoldSteps();
}
}
10 changes: 9 additions & 1 deletion src/dotnet-scaffolding/dotnet-scaffold/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -23,11 +29,13 @@
Option nonInteractiveOption = nonInteractiveScaffoldOption.ToCliOption();

AspireCommandService aspireCommandService = new(builder);

//aspire command adding does not need to be async
aspireCommandService.AddScaffolderCommands();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding ScaffolderComamnds twice. on L28 and L32

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one is aspire and the other is aspnet commands, they contain different scaffolders

ConfigureCommandSteps(builder.Services, aspireCommandService);

AspNetCommandService aspNetCommandService = new(builder);
aspNetCommandService.AddScaffolderCommands();
await aspNetCommandService.AddScaffolderCommandsAsync(cts.Token);
ConfigureCommandSteps(builder.Services, aspNetCommandService);

IScaffoldRunner runner = builder.Build();
Expand Down