Skip to content

aspire run hangs indefinitely when AppHost exits with code 0 (e.g. empty AppHost) #15685

@adamint

Description

@adamint

Description

When running aspire run against an AppHost that exits immediately with exit code 0 (e.g. an empty AppHost with no resources), the CLI hangs permanently displaying "Connecting to apphost..." and never terminates. From the user's perspective, aspire run appears frozen — the only way out is Ctrl+C.

Without --debug, this is what the user sees:

➜  emptyApphost aspire run
🗄  Updated settings file at 'aspire.config.json'.
🔐 Checking certificates...
🛠  Building apphost... apphost.cs
Connecting to apphost...

...and it stays there forever. The process never terminates.

With --debug, the log reveals the AppHost process exits immediately with code 0, but the CLI keeps retrying the backchannel socket connection indefinitely:

[17:16:21] [dbug] DotNetCliExecutionFactory: dotnet process with PID: 45966 has exited with code: 0
...
[17:16:21] [dbug] AppHostCliBackchannel: Connecting to AppHost backchannel at ...cli.sock... (autoReconnect=False)
[17:16:21] [dbug] AppHostCliBackchannel: Connecting to AppHost backchannel at ...cli.sock... (autoReconnect=False)
[17:16:22] [dbug] AppHostCliBackchannel: Connecting to AppHost backchannel at ...cli.sock... (autoReconnect=False)
... (continues indefinitely, every ~50ms then every 1s)

Root Cause

In DotNetCliRunner.StartBackchannelAsync, the SocketException catch that handles process exit only triggers when execution.ExitCode != 0:

catch (SocketException ex) when (execution is not null && execution.HasExited && execution.ExitCode != 0)
{
    // Only handles non-zero exit codes
    logger.LogDebug(ex, "AppHost process has exited. Unable to connect to backchannel at {SocketPath}", socketPath);
    backchannelCompletionSource.SetException(backchannelException);
    return;
}
catch (SocketException ex)
{
    // Falls through to here for exit code 0 — retries forever
    var waitingFor = DateTimeOffset.UtcNow - startTime;
    if (waitingFor > TimeSpan.FromSeconds(10))
    {
        await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
    }
}

When the AppHost exits with code 0, the first catch clause's when filter fails (ExitCode != 0 is false), so it falls through to the generic SocketException catch which retries indefinitely without ever checking execution.HasExited.

Suggested Fix

The when filter on line 172 should check execution.HasExited without restricting to non-zero exit codes. Additionally, the generic SocketException catch should also check if the process has exited to avoid retrying against a dead process:

catch (SocketException ex) when (execution is not null && execution.HasExited)
{
    logger.LogDebug(ex, "AppHost process has exited (exit code {ExitCode}). Unable to connect to backchannel.", execution.ExitCode);
    var backchannelException = new FailedToConnectBackchannelConnection("AppHost process has exited.", ex);
    backchannelCompletionSource.SetException(backchannelException);
    return;
}

Reproduction Steps

  1. Create an empty AppHost file (e.g. apphost.cs):
    var builder = DistributedApplication.CreateBuilder(args);
    builder.Build().Run();
  2. Run aspire run in the directory
  3. Observe the CLI displays "Connecting to apphost..." and hangs permanently

Expected Behavior

The CLI should detect the AppHost has exited (even with exit code 0), stop retrying the backchannel connection, and terminate gracefully.

Actual Behavior

The CLI displays "Connecting to apphost..." and hangs permanently, retrying backchannel socket connections indefinitely (~every 50ms initially, then every 1s after 10 seconds). Requires Ctrl+C to stop.

Environment

  • .NET Aspire CLI 13.3.0-pr.15668
  • macOS
  • .NET 11.0 Preview 1

Metadata

Metadata

Assignees

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions