Skip to content

[dotnet run/watch] gracefully handle Ctrl+C for Windows (WinForms, WPF, MAUI)#53127

Merged
tmat merged 8 commits intorelease/10.0.3xxfrom
dev/peppers/ctrl-c-windows-apps
Mar 6, 2026
Merged

[dotnet run/watch] gracefully handle Ctrl+C for Windows (WinForms, WPF, MAUI)#53127
tmat merged 8 commits intorelease/10.0.3xxfrom
dev/peppers/ctrl-c-windows-apps

Conversation

@jonathanpeppers
Copy link
Member

Fixes: #52473

There are really two concerns here: dotnet run and dotnet watch.

dotnet run

For dotnet run, we want to ensure that Ctrl+C is handled gracefully for Windows applications (WinForms, WPF, MAUI) that do not handle Ctrl+C. In these cases, the UI framework disables the console. As a result, when a user tries to stop the application using Ctrl+C: nothing happens!

So, when OutputType is WinExe, we will use Process.CloseMainWindow() to close the application instead of sending a Ctrl+C signal. This should work for nearly all Windows applications, unless there is a modal dialog preventing CloseMainWindow() from working.

dotnet watch

For dotnet watch, the behavior is similar, but with the issue that dotnet watch launches dotnet run as a "process group". When doing this, default Ctrl+C handling is lost.

To restore Ctrl+C handling from inside dotnet run, we can call:

SetConsoleCtrlHandler(null, false);

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleCtrlHandler(Delegate? handler, bool add);

When running inside a process group, this restores the default Ctrl+C handling, and is a no-op otherwise.

The only reason this works for console apps, is the $DOTNET_STARTUP_HOOKS assembly does this work instead, allowing dotnet watch to signal Ctrl+C to the grandchild process instead of dotnet run. Since Windows applications (WinForms, WPF, MAUI) ignore Ctrl+C signals, the grandchild process does nothing.

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Enables handling of Ctrl+C in a process where it was disabled.
//
// If a process is launched with CREATE_NEW_PROCESS_GROUP flag
// it allows the parent process to send Ctrl+C event to the child process,
// but also disables Ctrl+C handlers.
//
// "If the HandlerRoutine parameter is NULL, a TRUE value causes the calling process to ignore CTRL+C input,
// and a FALSE value restores normal processing of CTRL+C input.
// This attribute of ignoring or processing CTRL+C is inherited by child processes."
if (SetConsoleCtrlHandler(null, false))

I don't think we should remove the SetConsoleCtrlHandler() call from the startup hook, as it allows Ctrl+C to work for console applications.

…F, MAUI)

Fixes: #52473

There are really two concerns here: `dotnet run` and `dotnet watch`.

~~ dotnet run ~~

For `dotnet run`, we want to ensure that Ctrl+C is handled gracefully
for Windows applications (WinForms, WPF, MAUI) that do not handle
Ctrl+C. In these cases, the UI framework disables the console. As a
result, when a user tries to stop the application using Ctrl+C:
nothing happens!

So, when `OutputType` is `WinExe`, we will use
`Process.CloseMainWindow()` to close the application instead of
sending a Ctrl+C signal. This should work for nearly all Windows
applications, unless there is a modal dialog preventing
`CloseMainWindow()` from working.

~~ dotnet watch ~~

For `dotnet watch`, the behavior is similar, but with the issue that
`dotnet watch` launches `dotnet run` as a "process group". When doing
this, default Ctrl+C handling is lost.

To restore Ctrl+C handling from inside `dotnet run`, we can call:

    SetConsoleCtrlHandler(null, false);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool SetConsoleCtrlHandler(Delegate? handler, bool add);

When running *inside* a process group, this restores the default Ctrl+C
handling, and is a no-op otherwise.

The only reason this works for console apps, is the
`$DOTNET_STARTUP_HOOKS` assembly does this work instead, allowing
`dotnet watch` to signal Ctrl+C to the grandchild process instead of
`dotnet run`. Since Windows applications (WinForms, WPF, MAUI) ignore
Ctrl+C signals, the grandchild process does nothing.

https://github.com/dotnet/sdk/blob/f284d9f49aec157d49233ab5785cbcf78ed1ea58/src/BuiltInTools/HotReloadAgent.Host/StartupHook.cs#L104-L116

I don't think we should remove the `SetConsoleCtrlHandler()` call from
the startup hook, as it allows Ctrl+C to work for console applications.
Copilot AI review requested due to automatic review settings February 23, 2026 20:12
@jonathanpeppers jonathanpeppers requested review from a team and tmat as code owners February 23, 2026 20:12
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves Ctrl+C handling for Windows GUI apps (WinForms/WPF/MAUI) launched via dotnet run and dotnet watch, where Ctrl+C may otherwise be ignored due to console behavior/process-group semantics.

Changes:

  • Plumbs MSBuild OutputType into dotnet run so WinExe apps can be terminated via Process.CloseMainWindow() on Ctrl+C.
  • Re-enables Windows Ctrl+C handling in dotnet run when launched in a new process group (e.g., by dotnet watch).
  • Adds a WinForms WinExe test asset and new tests covering graceful termination paths.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
test/dotnet.Tests/CommandTests/Run/GivenDotnetRunIsInterrupted.cs Adds a Windows-only dotnet run interruption test for WinExe apps.
test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs Adds a Windows watch test validating graceful termination for WinExe apps.
test/TestAssets/TestProjects/WatchWinExeApp/WatchWinExeApp.csproj New WinExe WinForms test project used by run/watch tests.
test/TestAssets/TestProjects/WatchWinExeApp/Program.cs New WinForms app that reports whether it closed “gracefully”.
src/Cli/dotnet/Commands/Run/RunProperties.cs Adds OutputType to run properties retrieved from MSBuild.
src/Cli/dotnet/Commands/Run/RunCommand.cs Enables CloseMainWindow termination behavior for OutputType=WinExe.
src/Cli/Microsoft.DotNet.Cli.Utils/ProcessReaper.cs Re-enables Windows Ctrl+C handling and adds CloseMainWindow-on-Ctrl+C option.
src/Cli/Microsoft.DotNet.Cli.Utils/Command.cs Passes CloseMainWindow option into ProcessReaper.

jonathanpeppers and others added 2 commits February 23, 2026 14:27
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@tmat tmat merged commit 7080cc0 into release/10.0.3xx Mar 6, 2026
28 checks passed
@tmat tmat deleted the dev/peppers/ctrl-c-windows-apps branch March 6, 2026 19:39
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.

3 participants