Skip to content
Merged
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
29 changes: 8 additions & 21 deletions src/Aspire.Cli/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,31 +138,25 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell

await _certificateService.EnsureCertificatesTrustedAsync(_runner, cancellationToken);

var shouldBuildAppHostInExtension = await ShouldBuildAppHostInExtensionAsync(InteractionService, isSingleFileAppHost, cancellationToken);

var watch = !isSingleFileAppHost && (_features.IsFeatureEnabled(KnownFeatures.DefaultWatchEnabled, defaultValue: false) || (isExtensionHost && !startDebugSession));

if (!watch)
{
if (!isSingleFileAppHost || isExtensionHost)
if (!isSingleFileAppHost && !isExtensionHost)
{
var buildOptions = new DotNetCliRunnerInvocationOptions
{
StandardOutputCallback = buildOutputCollector.AppendOutput,
StandardErrorCallback = buildOutputCollector.AppendError,
};

// The extension host will build the app host project itself, so we don't need to do it here if host exists.
if (!shouldBuildAppHostInExtension)
{
var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, InteractionService, effectiveAppHostFile, buildOptions, ExecutionContext.WorkingDirectory, cancellationToken);
var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, InteractionService, effectiveAppHostFile, buildOptions, ExecutionContext.WorkingDirectory, cancellationToken);

if (buildExitCode != 0)
{
InteractionService.DisplayLines(buildOutputCollector.GetLines());
InteractionService.DisplayError(InteractionServiceStrings.ProjectCouldNotBeBuilt);
return ExitCodeConstants.FailedToBuildArtifacts;
}
if (buildExitCode != 0)
{
InteractionService.DisplayLines(buildOutputCollector.GetLines());
InteractionService.DisplayError(InteractionServiceStrings.ProjectCouldNotBeBuilt);
return ExitCodeConstants.FailedToBuildArtifacts;
}
}
}
Expand Down Expand Up @@ -224,7 +218,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
cancellationToken);

// Wait for the backchannel to be established.
var backchannel = await InteractionService.ShowStatusAsync(shouldBuildAppHostInExtension ? InteractionServiceStrings.BuildingAppHost : RunCommandStrings.ConnectingToAppHost, async () => { return await backchannelCompletitionSource.Task.WaitAsync(cancellationToken); });
var backchannel = await InteractionService.ShowStatusAsync(InteractionServiceStrings.BuildingAppHost, async () => { return await backchannelCompletitionSource.Task.WaitAsync(cancellationToken); });

var logFile = GetAppHostLogFile();

Expand Down Expand Up @@ -491,11 +485,4 @@ public void ProcessResourceState(RpcResourceState resourceState, Action<string,
_resourceStates[resourceState.Resource] = resourceState;
}
}

private static async Task<bool> ShouldBuildAppHostInExtensionAsync(IInteractionService interactionService, bool isSingleFileAppHost, CancellationToken cancellationToken)
{
return ExtensionHelper.IsExtensionHost(interactionService, out _, out var extensionBackchannel)
&& await extensionBackchannel.HasCapabilityAsync(KnownCapabilities.DevKit, cancellationToken)
&& !isSingleFileAppHost;
}
}
63 changes: 63 additions & 0 deletions tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,69 @@ public async Task RunCommand_SkipsBuild_WhenExtensionDevKitCapabilityIsAvailable
Assert.False(buildCalled, "Build should be skipped when extension DevKit capability is available.");
}

[Fact]
public async Task RunCommand_SkipsBuild_WhenRunningInExtension()
{
var buildCalled = false;

var extensionBackchannel = new TestExtensionBackchannel();
extensionBackchannel.GetCapabilitiesAsyncCallback = ct => Task.FromResult(Array.Empty<string>());

var appHostBackchannel = new TestAppHostBackchannel();
appHostBackchannel.GetDashboardUrlsAsyncCallback = (ct) => Task.FromResult(new DashboardUrlsState
{
DashboardHealthy = true,
BaseUrlWithLoginToken = "http://localhost/dashboard",
CodespacesUrlWithLoginToken = null
});
appHostBackchannel.GetAppHostLogEntriesAsyncCallback = ReturnLogEntriesUntilCancelledAsync;

var backchannelFactory = (IServiceProvider sp) => appHostBackchannel;

var extensionInteractionServiceFactory = (IServiceProvider sp) => new TestExtensionInteractionService(sp);

var runnerFactory = (IServiceProvider sp) => {
var runner = new TestDotNetCliRunner();
runner.CheckHttpCertificateAsyncCallback = (options, ct) => 0;
runner.BuildAsyncCallback = (projectFile, options, ct) => {
buildCalled = true;
return 0;
};
runner.GetAppHostInformationAsyncCallback = (projectFile, options, ct) => (0, true, VersionHelper.GetDefaultTemplateVersion());
runner.RunAsyncCallback = async (projectFile, watch, noBuild, args, env, backchannelCompletionSource, options, ct) => {
var backchannel = sp.GetRequiredService<IAppHostBackchannel>();
backchannelCompletionSource!.SetResult(backchannel);
await Task.Delay(Timeout.InfiniteTimeSpan, ct);
return 0;
};
return runner;
};

var projectLocatorFactory = (IServiceProvider sp) => new TestProjectLocator();

using var workspace = TemporaryWorkspace.Create(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ProjectLocatorFactory = projectLocatorFactory;
options.AppHostBackchannelFactory = backchannelFactory;
options.DotNetCliRunnerFactory = runnerFactory;
options.ExtensionBackchannelFactory = _ => extensionBackchannel;
options.InteractionServiceFactory = extensionInteractionServiceFactory;
});

var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse("run");

using var cts = new CancellationTokenSource();
var pendingRun = result.InvokeAsync(cancellationToken: cts.Token);
cts.Cancel();
var exitCode = await pendingRun.WaitAsync(CliTestConstants.DefaultTimeout);

Assert.Equal(ExitCodeConstants.Success, exitCode);
Assert.False(buildCalled, "Build should be skipped when running in extension.");
}

[Fact]
public async Task RunCommand_WhenSingleFileAppHostAndDefaultWatchEnabled_DoesNotUseWatchMode()
{
Expand Down
Loading