Skip to content

Launch an apphost directly instead of through the dotnet muxer - #34

Open
nevse wants to merge 1 commit into
MattParkerDev:mainfrom
nevse:fix/launch-apphost-without-the-muxer
Open

Launch an apphost directly instead of through the dotnet muxer#34
nevse wants to merge 1 commit into
MattParkerDev:mainfrom
nevse:fix/launch-apphost-without-the-muxer

Conversation

@nevse

@nevse nevse commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #33.

PerformLaunch and the runInTerminal request both hardcode the muxer:

FileName = "dotnet",
ArgumentList = { launchInfo.Program },
Arguments = ["dotnet", launchInfo.Program, ..launchInfo.Arguments],

That is right for a managed assembly and wrong for an apphost, which is the only artefact a self-contained or single-file publish produces.

It is worse than a failed launch. dotnet <apphost> does not fail fast - the muxer starts, fails to resolve the path as a command, and the debugger attaches to the muxer rather than to the program. The session looks healthy, and the top frame is:

dotnet.dll!Microsoft.DotNet.Cli.Telemetry.ExternalTelemetryProperties.GetLibcRelease()

LaunchInfo.GetCommandLine() now decides: a .dll goes through dotnet, anything else is run directly. Both call sites use it.

Tests

LaunchTests launches DebuggableConsoleApp and stops at a breakpoint, once through the assembly and once through the apphost - the framework-dependent build produces both, so nothing new has to be published. On main the apphost case attaches to the muxer and never hits the breakpoint.

There was no launch coverage before, so this needed a little scaffolding:

  • GetRunningDebugProtocolHostForLaunchInProc does not start a debuggee, and gives the first stop its own TaskCompletionSource. A launch binds its breakpoints and hits one within milliseconds, and the shared TcsContainer only holds the latest event, so the stop was getting dropped.
  • WithConfigurationDoneRequestAsync bounds the request. The launch happens inside configurationDone, so a program the debugger cannot start leaves it unanswered - without a bound the whole run hangs instead of failing one test.
  • LaunchedDebuggeeKiller kills what the launch started. Dispose releases the Process object without killing the process, and disconnect with terminateDebuggee: true does not return at all (below), so the test cannot hand cleanup back to the adapter.

Unrelated, found on the way

disconnect with terminateDebuggee: true, sent while stopped at a breakpoint, never returns - 3 of 3 on main. With terminateDebuggee: false the same sequence disconnects cleanly, 2 of 2. It looks like the same lock as #32, but deterministic rather than 1-in-12, so it may be the easier way in. I have not touched it here.

Verified on macOS arm64 with .NET 10.

I agree to the terms of contributing as stated here

@MattParkerDev
MattParkerDev force-pushed the fix/launch-apphost-without-the-muxer branch from 75af418 to b2f4395 Compare August 11, 2026 12:45
The muxer cannot execute an apphost, which is the only artefact a
self-contained or single-file publish produces. Worse than failing, it
starts the muxer itself, so the debugger attaches to dotnet rather than
to the program.

LaunchInfo.GetCommandLine decides: a .dll goes through dotnet, anything
else runs directly. Both PerformLaunch and the runInTerminal request use
it.

Covers launch with tests for the first time - one through the assembly,
one through the apphost.

fix startinfo in launch request
@MattParkerDev
MattParkerDev force-pushed the fix/launch-apphost-without-the-muxer branch from ca7a5e1 to efe7104 Compare August 11, 2026 13:27
@MattParkerDev

Copy link
Copy Markdown
Owner

FYI I've committed the fix to main separately, and will review the new tests that remain in this PR when I get a chance.

@nevse

nevse commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for rebasing it. The tests are byte-identical to what I ran, and they pass against your version, 2 of 2 — the apphost one still fails on the muxer path, so it pins the behaviour rather than just exercising it.

One small thing about normalising program at the request site: LaunchInfo.Program is now the host rather than the program, and the runInTerminal title reads it — Title = $"{Path.GetFileName(launchInfo.Program)} [DEBUG]". So a .dll launch in a terminal is now titled dotnet [DEBUG] instead of MyApp.dll [DEBUG].

Unrelated, while I was running the suite: EvalTests.SharpDbgCli_EvaluationRequest_Returns fails here on 66db871, clean checkout. GenericTypeWithStaticField<DateTime>.IntValue (EvalTests.cs:186, added in dbdb45f) gives error: CIL execution failed at IL_0000 (ldsfld): COMException: Unexpected HRESULT has been returned from a call to a COM component. instead of 4. The line above it with <int[]> passes, so a reference-type argument is fine and the corelib value type is not. Everything else passes, 33 of 34 — macOS arm64, .NET 10. No idea whether it is green on Windows, and nothing in CI runs the suite, so flagging in case.

@MattParkerDev

Copy link
Copy Markdown
Owner

One small thing about normalising program at the request site: LaunchInfo.Program is now the host rather than the program, and the runInTerminal title reads it — Title = $"{Path.GetFileName(launchInfo.Program)} [DEBUG]". So a .dll launch in a terminal is now titled dotnet [DEBUG] instead of MyApp.dll [DEBUG].

Fixed in 5c2552f :)

Ah, thanks for reporting that EvalTests failure, looks like a macos/unix specific failure - I am primarily on Windows. I shall investigate and fix :) I probably should add some CI for running the tests on all platforms at some point too 😅

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Launch always goes through the dotnet muxer, so a self-contained app cannot be launched

2 participants