Skip to content

Commit 4fff8a1

Browse files
aspire-repo-bot[bot]adamintCopilot
authored
[release/13.3] Use plain dotnet run for extension Run Without Debugging (#16803)
* Use plain dotnet run for extension Run Without Debugging * Honor IDE-advertised SupportedLaunchConfigurations for project resources ExtensionUtils.SupportsDebugging treated 'project' launch type as implicitly supported by every IDE, even when the IDE explicitly advertised a SupportedLaunchConfigurations list that did not include 'project'. This caused project resources to fail to start in VS Code when the C# extension was not installed: the AppHost routed them to the extension via DCP, and the extension returned 400 UnsupportedLaunchConfiguration because it has no project debugger registered without C#. Now the implicit-project rule only applies when the IDE did not send DEBUG_SESSION_INFO at all (the Visual Studio scenario). When the IDE sent an explicit list, honor it for every launch type including 'project' — IDEs that can launch project resources must advertise 'project' in their list. The VS Code extension already does this correctly when C# is installed. Resources whose launch type is not in the advertised list now fall to ExecutionType.Process, so the AppHost spawns dotnet itself and the resource starts (without a debugger attached, which is correct since the IDE has no debugger to attach). * Remove NoExtensionLaunch flag DotNetAppHostProject only needed NoExtensionLaunch to prevent the extension from launching the apphost during pipeline (publish/exec) flows. Since the extension launch path now uses plain 'dotnet run' for Run Without Debugging, the flag is unused — pipeline commands already take a different code path. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Adam Ratzman <adam@adamratzman.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 9821098 commit 4fff8a1

6 files changed

Lines changed: 91 additions & 35 deletions

File tree

playground/ProjectResourceExtensions/ProjectResourceExtensions.AppHost/AppHost.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
var funcApp = builder.AddAzureFunctionsProject<Projects.AzureFunctionsService>("azure-functions-service")
99
.WithExternalHttpEndpoints();
1010

11-
// Bug #15606/#15647 repro: A standard project resource should always get
12-
// IDE execution when DEBUG_SESSION_PORT is set, even if the extension
13-
// advertises SupportedLaunchConfigurations that don't include "project".
11+
// Bug #15606/#15647 repro: A standard project resource should get
12+
// IDE execution when DEBUG_SESSION_PORT is set and the extension
13+
// advertises SupportedLaunchConfigurations that include "project"
14+
// (or omits the list entirely, e.g. Visual Studio).
1415
builder.AddProject<Projects.StandardService>("standard-service")
1516
.WithReference(funcApp)
1617
.WaitFor(funcApp);

src/Aspire.Cli/DotNet/DotNetCliRunner.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ internal sealed class ProcessInvocationOptions
5151

5252
public bool NoLaunchProfile { get; set; }
5353
public bool StartDebugSession { get; set; }
54-
public bool NoExtensionLaunch { get; set; }
5554
public bool Debug { get; set; }
5655

5756
/// <summary>
@@ -119,7 +118,6 @@ private async Task<int> ExecuteAsync(
119118
{
120119
if (ExtensionHelper.IsExtensionHost(interactionService, out var extensionInteractionService, out var extensionBackchannel)
121120
&& projectFile is not null
122-
&& !options.NoExtensionLaunch
123121
&& await extensionBackchannel.HasCapabilityAsync(KnownCapabilities.Project, cancellationToken))
124122
{
125123
await extensionInteractionService.LaunchAppHostAsync(

src/Aspire.Cli/Projects/DotNetAppHostProject.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ public async Task<int> RunAsync(AppHostProjectContext context, CancellationToken
259259
throw;
260260
}
261261

262-
var watch = !isSingleFileAppHost && (_features.IsFeatureEnabled(KnownFeatures.DefaultWatchEnabled, defaultValue: false) || (isExtensionHost && !context.StartDebugSession));
262+
var watch = !isSingleFileAppHost && _features.IsFeatureEnabled(KnownFeatures.DefaultWatchEnabled, defaultValue: false);
263263

264264
try
265265
{
@@ -338,7 +338,8 @@ public async Task<int> RunAsync(AppHostProjectContext context, CancellationToken
338338
// Start the apphost - the runner will signal the backchannel when ready
339339
try
340340
{
341-
// noBuild: true if either watch mode is off (we already built above) or --no-build was passed
341+
// noBuild: true if watch mode is off (we already built above), or if --no-build was explicitly requested.
342+
// dotnet watch does not support --no-build, so watch + context.NoBuild is invalid and will fail in the runner.
342343
// noRestore: only relevant when noBuild is false (since --no-build implies --no-restore)
343344
var noBuild = !watch || context.NoBuild;
344345
return await _runner.RunAsync(
@@ -525,10 +526,7 @@ public async Task<int> PublishAsync(PublishContext context, CancellationToken ca
525526
StandardOutputCallback = runOutputCollector.AppendOutput,
526527
StandardErrorCallback = runOutputCollector.AppendError,
527528
NoLaunchProfile = true,
528-
StartDebugSession = context.StartDebugSession,
529-
// When not starting a debug session, prevent DotNetCliRunner from delegating the
530-
// apphost launch to the extension — pipeline commands should run the apphost directly.
531-
NoExtensionLaunch = !context.StartDebugSession,
529+
StartDebugSession = context.StartDebugSession
532530
};
533531

534532
if (isSingleFileAppHost)

src/Aspire.Hosting/Utils/ExtensionUtils.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,20 @@ public static bool SupportsDebugging(this IResource builder, IConfiguration conf
2424
return false;
2525
}
2626

27-
// Per DCP IDE execution spec, project launch configuration support is implicit.
28-
// Custom launch types (for example, azure-functions) must be explicitly advertised.
29-
return supportsDebuggingAnnotation.LaunchConfigurationType == "project"
30-
|| (supportedLaunchConfigurations is not null && supportedLaunchConfigurations.Contains(supportsDebuggingAnnotation.LaunchConfigurationType));
27+
// When the IDE did not send DEBUG_SESSION_INFO (e.g. Visual Studio), fall back to the
28+
// legacy rule that "project" launch configuration support is implicit. VS launches all
29+
// project resources natively without advertising a capability list.
30+
if (supportedLaunchConfigurations is null)
31+
{
32+
return supportsDebuggingAnnotation.LaunchConfigurationType == "project";
33+
}
34+
35+
// The IDE advertised an explicit capability list — honor it for every type, including
36+
// "project". An IDE that can launch project resources must include "project" in its list
37+
// (the VS Code extension does this when the C# extension is installed). Treating "project"
38+
// as implicitly supported here would route resources to an IDE that cannot launch them
39+
// and leave them stuck.
40+
return supportedLaunchConfigurations.Contains(supportsDebuggingAnnotation.LaunchConfigurationType);
3141
}
3242

3343
private static string[]? GetSupportedLaunchConfigurations(IConfiguration configuration)

tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,6 @@ public async Task RunCommand_Builds_WhenExtensionHasBuildDotnetUsingCliCapabilit
714714

715715
using var provider = services.BuildServiceProvider();
716716
var command = provider.GetRequiredService<RootCommand>();
717-
// Pass --start-debug-session to avoid watch mode (which skips build)
718717
var result = command.Parse("run --start-debug-session");
719718

720719
using var cts = new CancellationTokenSource();
@@ -730,6 +729,60 @@ public async Task RunCommand_Builds_WhenExtensionHasBuildDotnetUsingCliCapabilit
730729
Assert.True(buildCalled, "Build should be called when extension has build-dotnet-using-cli capability.");
731730
}
732731

732+
[Fact]
733+
public async Task RunCommand_WhenExtensionNoDebugBuildFails_DoesNotRunAppHost()
734+
{
735+
var buildCalled = false;
736+
var runCalled = false;
737+
738+
var extensionBackchannel = new TestExtensionBackchannel();
739+
extensionBackchannel.HasCapabilityAsyncCallback = (capability, ct) => Task.FromResult(capability == KnownCapabilities.BuildDotnetUsingCli);
740+
741+
var extensionInteractionServiceFactory = (IServiceProvider sp) => new TestExtensionInteractionService(sp);
742+
743+
var runnerFactory = (IServiceProvider sp) =>
744+
{
745+
var runner = new TestDotNetCliRunner();
746+
runner.BuildAsyncCallback = (projectFile, noRestore, options, ct) =>
747+
{
748+
buildCalled = true;
749+
return 1;
750+
};
751+
runner.GetAppHostInformationAsyncCallback = (projectFile, options, ct) => (0, true, VersionHelper.GetDefaultTemplateVersion());
752+
runner.RunAsyncCallback = (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, ct) =>
753+
{
754+
runCalled = true;
755+
return Task.FromResult(0);
756+
};
757+
return runner;
758+
};
759+
760+
var projectLocatorFactory = (IServiceProvider sp) => new TestProjectLocator();
761+
762+
using var workspace = TemporaryWorkspace.Create(outputHelper);
763+
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
764+
{
765+
options.ProjectLocatorFactory = projectLocatorFactory;
766+
options.DotNetCliRunnerFactory = runnerFactory;
767+
options.ExtensionBackchannelFactory = _ => extensionBackchannel;
768+
options.InteractionServiceFactory = extensionInteractionServiceFactory;
769+
options.ConfigurationCallback += config =>
770+
{
771+
config["ASPIRE_EXTENSION_DEBUG_SESSION_ID"] = "test-session-id";
772+
};
773+
});
774+
775+
using var provider = services.BuildServiceProvider();
776+
var command = provider.GetRequiredService<RootCommand>();
777+
var result = command.Parse("run");
778+
779+
var exitCode = await result.InvokeAsync().DefaultTimeout();
780+
781+
Assert.Equal(ExitCodeConstants.FailedToBuildArtifacts, exitCode);
782+
Assert.True(buildCalled, "Build should be called before launching the AppHost in extension no-debug mode.");
783+
Assert.False(runCalled, "AppHost should not be launched when the pre-build fails.");
784+
}
785+
733786
[Fact]
734787
public async Task RunCommand_WhenSingleFileAppHostAndDefaultWatchEnabled_DoesNotUseWatchMode()
735788
{

tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,11 +2193,13 @@ public async Task ProjectExecutable_DebugSessionInfoWithNullSupportedLaunchConfi
21932193
}
21942194

21952195
[Fact]
2196-
public async Task ProjectExecutable_DebugSessionInfoWithoutProjectStillDefaultsToProjectSupport()
2196+
public async Task ProjectExecutable_DebugSessionInfoWithoutProjectFallsBackToProcess()
21972197
{
2198-
// Bug #15606/#15647: VS Code extension sends SupportedLaunchConfigurations=["azure-functions"]
2199-
// (not including "project"). Standard project resources should still get IDE execution because
2200-
// "project" launch support is implicit in DCP.
2198+
// When the IDE explicitly advertises a SupportedLaunchConfigurations list that does NOT
2199+
// include "project", honor it: the IDE cannot launch project resources, so we must run
2200+
// them as a Process from the AppHost. The VS Code extension behaves this way when the
2201+
// C# extension is not installed; routing project resources to the extension in that case
2202+
// would result in them never starting (the extension returns 400 UnsupportedLaunchConfiguration).
22012203
var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions
22022204
{
22032205
AssemblyName = typeof(DistributedApplicationTests).Assembly.FullName
@@ -2228,11 +2230,7 @@ public async Task ProjectExecutable_DebugSessionInfoWithoutProjectStillDefaultsT
22282230
await appExecutor.RunApplicationAsync();
22292231

22302232
var exe = Assert.Single(kubernetesService.CreatedResources.OfType<Executable>(), e => e.AppModelResourceName == "ServiceA");
2231-
Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType);
2232-
2233-
Assert.True(exe.TryGetAnnotationAsObjectList<ProjectLaunchConfiguration>(Executable.LaunchConfigurationsAnnotation, out var launchConfigs));
2234-
Assert.Single(launchConfigs);
2235-
Assert.Equal("project", launchConfigs[0].Type);
2233+
Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType);
22362234
}
22372235

22382236
[Fact]
@@ -2440,10 +2438,11 @@ public async Task StandardAndCustomProjects_VSScenario_BothRunInIde()
24402438
[Fact]
24412439
public async Task StandardAndCustomProjects_VSCodeScenario_BothRunInIde()
24422440
{
2443-
// Combined VS Code scenario for bugs #15606/#15647 and class library projects:
2444-
// VS Code extension sends SupportedLaunchConfigurations=["azure-functions"] (not "project").
2445-
// A standard project (type "project") should still get IDE (implicit support).
2446-
// A project with "azure-functions" annotation should also get IDE (explicit match).
2441+
// Combined VS Code scenario for class library projects:
2442+
// VS Code extension sends SupportedLaunchConfigurations=["azure-functions"] (without "project").
2443+
// A standard project (type "project") falls to Process execution because the IDE explicitly
2444+
// did not advertise project support — the AppHost spawns dotnet itself.
2445+
// A project with "azure-functions" annotation gets IDE (explicit match).
24472446
var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions
24482447
{
24492448
AssemblyName = typeof(DistributedApplicationTests).Assembly.FullName
@@ -2484,14 +2483,11 @@ public async Task StandardAndCustomProjects_VSCodeScenario_BothRunInIde()
24842483
var dcpExes = kubernetesService.CreatedResources.OfType<Executable>().ToList();
24852484
Assert.Equal(2, dcpExes.Count);
24862485

2487-
// Standard project: IDE via implicit "project" support (bug #15606/#15647 fix)
2486+
// Standard project: Process execution because the IDE did not advertise "project" support.
24882487
var standardExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "standard-project");
2489-
Assert.Equal(ExecutionType.IDE, standardExe.Spec.ExecutionType);
2490-
Assert.True(standardExe.TryGetAnnotationAsObjectList<ProjectLaunchConfiguration>(Executable.LaunchConfigurationsAnnotation, out var standardConfigs));
2491-
Assert.Single(standardConfigs);
2492-
Assert.Equal("project", standardConfigs[0].Type);
2488+
Assert.Equal(ExecutionType.Process, standardExe.Spec.ExecutionType);
24932489

2494-
// Azure Functions project: IDE via explicit "azure-functions" support
2490+
// Azure Functions project: IDE via explicit "azure-functions" support.
24952491
var functionsExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "functions-project");
24962492
Assert.Equal(ExecutionType.IDE, functionsExe.Spec.ExecutionType);
24972493
}

0 commit comments

Comments
 (0)