| File: ApplicationModel\DebugSupportExtensions.cs | Web Access |
| Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Aspire.Hosting.Dcp; using Aspire.Hosting.Dcp.Model; using Microsoft.Extensions.Configuration; namespace Aspire.Hosting.ApplicationModel; /// <summary> /// Extension methods for inspecting whether a resource will be launched by an IDE or extension host /// for debugging rather than started as a plain process by Aspire. /// </summary> [Experimental("ASPIREEXTENSION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static class DebugSupportExtensions { /// <summary> /// Determines whether the resource will be launched by the IDE for debugging in the current session. /// </summary> /// <param name="resource">The resource to inspect.</param> /// <param name="configuration">The app host configuration, used to detect the active debug session and its capabilities.</param> /// <param name="supportsDebuggingAnnotation">When this method returns <see langword="true"/>, the annotation describing how the resource is launched.</param> /// <returns><see langword="true"/> when the IDE owns launching this resource; otherwise <see langword="false"/>.</returns> /// <remarks> /// <para> /// Integrations use this to decide whether to build a runnable command line for the resource. When the /// IDE launches the resource, arguments such as <c>dotnet run --project …</c> or <c>go run …</c> are /// supplied by the IDE instead and must not be produced by the integration. /// </para> /// <para> /// A resource is only considered debuggable when it carries a <see cref="SupportsDebuggingAnnotation"/>, /// a debug session is active, the resource is not forced to process execution, it does not have a /// persistent lifetime, and the IDE advertised support for the annotation's launch configuration type. /// </para> /// <para> /// Exception: when the active debug session did not advertise any launch configuration types at all /// (for example Visual Studio, which does not send a capability list), a resource whose launch /// configuration type is <see cref="KnownLaunchConfigurationTypes.Project"/> is treated as implicitly /// supported rather than falling back to plain process execution. /// </para> /// </remarks> [AspireExportIgnore(Reason = "Debug support inspection is a local .NET helper and is not part of the ATS surface.")] public static bool SupportsDebugging(this IResource resource, IConfiguration configuration, [NotNullWhen(true)] out SupportsDebuggingAnnotation? supportsDebuggingAnnotation) { ArgumentNullException.ThrowIfNull(resource); ArgumentNullException.ThrowIfNull(configuration); var supportedLaunchConfigurations = GetSupportedLaunchConfigurations(configuration); if (!resource.TryGetLastAnnotation(out supportsDebuggingAnnotation) || string.IsNullOrEmpty(configuration[DcpExecutor.DebugSessionPortVar]) || resource.HasAnnotationOfType<ForceProcessExecutionAnnotation>() || resource.HasPersistentLifetime()) { return false; } // When the IDE did not send DEBUG_SESSION_INFO (e.g. Visual Studio), fall back to the // legacy rule that "project" launch configuration support is implicit. VS launches all // project resources natively without advertising a capability list. if (supportedLaunchConfigurations is null) { return supportsDebuggingAnnotation.LaunchConfigurationType == KnownLaunchConfigurationTypes.Project; } // The IDE advertised an explicit capability list — honor it for every type, including // "project". An IDE that can launch project resources must include "project" in its list // (the VS Code extension does this when the C# extension is installed). Treating "project" // as implicitly supported here would route resources to an IDE that cannot launch them // and leave them stuck. return supportedLaunchConfigurations.Contains(supportsDebuggingAnnotation.LaunchConfigurationType); } /// <summary> /// Determines whether the launch configuration performs the resource's tool invocation itself, meaning the /// resource's launch tool arguments must not also be passed to the launched program. /// </summary> /// <param name="resource">The resource to inspect.</param> /// <param name="supportsDebuggingAnnotation">The launch configuration annotation to compare.</param> /// <returns><see langword="true"/> when the launch configuration supplies the tool invocation; otherwise, <see langword="false"/>.</returns> /// <remarks> /// This is <see langword="false"/> for a resource whose launch tool arguments declare no owning launch /// configuration type, because such a prefix is always passed to the program. /// </remarks> [AspireExportIgnore(Reason = "Debug support inspection is a local .NET helper and is not part of the ATS surface.")] public static bool HasLaunchToolArgsOwnedBy(this IResource resource, SupportsDebuggingAnnotation supportsDebuggingAnnotation) { ArgumentNullException.ThrowIfNull(resource); ArgumentNullException.ThrowIfNull(supportsDebuggingAnnotation); return resource.TryGetLastAnnotation<LaunchToolArgsCallbackAnnotation>(out var launchToolAnnotation) && launchToolAnnotation.OwningLaunchConfigurationType is string owner && string.Equals(owner, supportsDebuggingAnnotation.LaunchConfigurationType, StringComparison.Ordinal); } /// <summary> /// Creates the launch configuration that this resource sends to the IDE for the given launch mode. /// </summary> /// <param name="resource">The resource to inspect. It must carry a <see cref="SupportsDebuggingAnnotation"/>.</param> /// <param name="mode">The launch mode, one of the values on <see cref="ExecutableLaunchMode"/>.</param> /// <param name="cancellationToken">A token to cancel the operation.</param> /// <returns>The launch configuration, typically an <see cref="ExecutableLaunchConfiguration"/>.</returns> /// <exception cref="ArgumentNullException"><paramref name="resource"/> or <paramref name="mode"/> is <see langword="null"/>.</exception> /// <exception cref="InvalidOperationException">The resource does not declare debug launch support.</exception> /// <remarks> /// <para> /// Launch configuration is created by invoking the producer callback passed to a /// <c>WithDebugSupport</c> overload on <see cref="ResourceBuilderExtensions"/>, which owns the complete /// configuration; Aspire serializes the result as-is. The configuration is produced fresh on each call. /// Aspire may call the producer several times for the same resource. /// </para> /// <para> /// This overload does not resolve the resource's environment variables. A context-based producer receives /// an empty environment. Aspire's executable creation path supplies the resolved environment through the /// context-aware overload. /// </para> /// <para> /// This describes the launch configuration itself, not whether one is going to be used. Depending on how /// the application is started, or how a resource is configured, Aspire may or may not run the resource under /// a debugger. Use <see cref="SupportsDebugging"/> to test for that. /// </para> /// </remarks> [AspireExportIgnore(Reason = "Debug support inspection is a local .NET helper and is not part of the ATS surface.")] public static Task<object> CreateLaunchConfigurationAsync( this IResource resource, string mode, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(resource); ArgumentNullException.ThrowIfNull(mode); return resource.CreateLaunchConfigurationAsync( new LaunchConfigurationCallbackContext( mode, resource, new Dictionary<string, string>(), cancellationToken)); } /// <summary> /// Creates the launch configuration that this resource sends to the IDE using a callback context. /// </summary> /// <param name="resource">The resource to inspect. It must carry a <see cref="SupportsDebuggingAnnotation"/>.</param> /// <param name="context">The callback context containing the resolved environment and launch data.</param> /// <returns>The launch configuration, typically an <see cref="ExecutableLaunchConfiguration"/>.</returns> /// <exception cref="ArgumentException"><paramref name="context"/> belongs to a different resource.</exception> /// <exception cref="InvalidOperationException">The resource does not declare debug launch support.</exception> /// <remarks> /// <para> /// Launch configuration is created by invoking the producer callback passed to /// <see cref="ResourceBuilderExtensions.WithDebugSupport{T, TLaunchConfiguration}(IResourceBuilder{T}, Func{LaunchConfigurationCallbackContext, Task{TLaunchConfiguration}}, string)"/>, /// which owns the complete configuration; Aspire serializes the result as-is. /// </para> /// <para> /// This method never resolves environment variables. Aspire creates <paramref name="context"/> /// when the active debug-support annotation is producing a launch configuration for an executable creation. /// </para> /// <para> /// This overload is internal because only Aspire constructs callback contexts containing resolved environment /// variables. Use the public overload when inspecting a launch configuration outside executable creation. /// </para> /// </remarks> internal static Task<object> CreateLaunchConfigurationAsync( this IResource resource, LaunchConfigurationCallbackContext context) { ArgumentNullException.ThrowIfNull(resource); ArgumentNullException.ThrowIfNull(context); if (!ReferenceEquals(resource, context.Resource)) { throw new ArgumentException( $"The launch configuration callback context belongs to resource '{context.Resource.Name}', " + $"but launch configuration was requested for resource '{resource.Name}'.", nameof(context)); } if (!resource.TryGetLastAnnotation<SupportsDebuggingAnnotation>(out var supportsDebuggingAnnotation)) { throw new InvalidOperationException( $"Resource '{resource.Name}' does not declare debug launch support. " + $"Call {nameof(ResourceBuilderExtensions.WithDebugSupport)} on the resource first. " + $"Note that it only adds the annotation in run mode."); } return supportsDebuggingAnnotation.LaunchConfigurationProducer(context); } private static string[]? GetSupportedLaunchConfigurations(IConfiguration configuration) { try { if (configuration[KnownConfigNames.DebugSessionInfo] is { } debugSessionInfoJson && JsonSerializer.Deserialize<RunSessionInfo>(debugSessionInfoJson) is { } debugSessionInfo) { return debugSessionInfo.SupportedLaunchConfigurations; } } catch (JsonException) { } return null; } }