Skip to content

WithDebugSupport argument rewriting is order-sensitive and mutates the app model #18929

Description

@afscrome

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

WithDebugSupport takes an optional argsCallback that integrations use to strip the tool entrypoint (cargo run … --, go run <pkg>, python -m <mod>) when an IDE debugs the produced binary directly. It applies that callback by registering an ordinary WithArgs callback on the resource:

https://github.com/microsoft/aspire/blob/main/src/Aspire.Hosting/ResourceBuilderExtensions.cs#L4778

if (argsCallback is not null && builder is IResourceBuilder<IResourceWithArgs> resourceWithArgs)
{
    resourceWithArgs.WithArgs(ctx =>
    {
        if (resourceWithArgs.Resource.SupportsDebugging(...) && ReferenceEquals(activeAnnotation, supportsDebuggingAnnotation))
        {
            argsCallback(ctx);
        }
    });
}

This creates three problems.

1. A silent registration-order requirement. CommandLineArgsCallbackAnnotation callbacks run in registration order, each mutating one shared list (ResourceExtensions.cs#L282). Because the debug callback removes items, it only works if registered after the callback that adds the entrypoint prefix. Nothing enforces or documents this. Registered earlier it sees an empty list, hits its own guard, and silently no-ops — leaking the entrypoint tokens to the debugged binary.

This was hit for real in Aspire.Hosting.Rust (#18906): the binary launched as myapp.exe run -- --login … and failed with error: unexpected argument 'run' found. Go (L91 / L185) and Python carry the same unwritten contract and happen to be ordered correctly. No tests cover it.

2. The API's shape invites the more dangerous position. A "rewrite arguments for debugging" hook reads as post-processing, so the intuitive place to register it is last. But last is strictly worse: the callback would then have to decide which -- is the integration's separator and which is a user argument. WithArgs("--", "--login", "user") is exactly that ambiguity. It currently works only because it runs before user arguments exist — the opposite of what the name suggests. Conversely, an integration that genuinely needs the final argument list has no correct position at all.

3. It mutates the app model, degrading non-debug paths. The rewrite applies to the resource's arguments generally, leaving them valid only for IDE launch. SupportsDebuggingAnnotation.RewritesArgumentsForDebugging exists purely to compensate, by suppressing Process fallback:

https://github.com/microsoft/aspire/blob/main/src/Aspire.Hosting/Dcp/ExecutableCreator.cs#L289

// A Process fallback runs the DCP Executable Spec's command and args "as is". When the debug
// support rewrites the resource's arguments for debugging ... those args are valid only for
// IDE launch, so a Process fallback would run a broken command. Skip the fallback in that case.
if (!supportsDebuggingAnnotation.RewritesArgumentsForDebugging)
{
    exe.Spec.FallbackExecutionTypes = [ExecutionType.Process];
}

Also at L203 and L454. So any resource using an argsCallback permanently loses Process fallback — a capability sacrificed to work around the design.

Describe the solution you'd like

Compose the command line per execution mode instead of subtracting from it.

The information needed is already modelled separately. Aspire.Hosting.Rust keeps cargo arguments in their own annotation and already declares them structurally in the launch configuration:

return new RustLaunchConfiguration
{
    Cargo = new RustCargoLaunchTarget { Args = ["build", .. cargoArgs] }
};

It then also concatenates run <cargo args> -- into the command line and textually re-derives that prefix to delete it. The structured form is discarded and reconstructed by string scanning. Go (build_flags, program) and Python (module, program_path) have the same split available in their launch configurations.

If a resource can express "tool invocation" and "program arguments" as distinct concepts, the command line is just assembled differently per mode:

  • Process: cargo run <cargoArgs> -- <programArgs>
  • IDE: <programArgs> only, with the tool arguments carried in the launch configuration where they already live

That removes the ordering requirement, the -- ambiguity, and the need for RewritesArgumentsForDebugging and its fallback suppression — and the app model stays truthful about what the resource runs.

A smaller intermediate step, if the full change is too invasive: keep argsCallback but store it on SupportsDebuggingAnnotation rather than registering it as a WithArgs callback, and apply it in ExecutableCreator when populating the IDE launch arguments. That alone fixes the ordering trap and gives the callback the fully-evaluated list.

One thing worth confirming first: the DCP Executable spec has a single args field (Executable.cs#L28), and launch configurations carry build/launch information rather than program arguments. Retaining both the rewritten and un-rewritten forms may need a DCP-side change.

Additional context

Karol Zadora-Przylecki (@karolz-ms) — flagging because this may interact with #18918. That PR makes the debugging APIs public so language integrations can live outside Aspire.Hosting and third-party language support becomes possible. If WithDebugSupport ships publicly in its current shape, the registration-order requirement becomes a public contract that external integration authors have to know about but can't discover — and the failure mode is silent, producing a broken command line at debug time rather than an error at build time. Worth deciding whether to reshape the API before it's public, or at minimum document the ordering requirement on the argsCallback parameter, whose XML doc currently says only:

Optional callback to add or modify command line arguments when running in an extension host. Useful if the entrypoint is usually provided as an argument to the resource executable.

Affected integrations: Aspire.Hosting.Rust, Aspire.Hosting.Go, Aspire.Hosting.Python. Aspire.Hosting.JavaScript passes no argsCallback and is unaffected.

Suggested follow-up regardless of approach: tests covering WithDebugSupport registered both before and after the entrypoint WithArgs callback — currently no test would catch the regression.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area-app-modelIssues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplicationarea-polyglotIssues related to polyglot apphostsarea-vscodetriage:bot-seenAspire triage bot has seen this issue

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions