While developing dotnet-evergreen, I needed to figure out a way to cleanly terminate a process in a cross-platform way without resorting to a (somewhat violent) Process.Kill.

In Ubuntu/macOS this was trivial and worked nicely by just running kill -s SIGINT [ProcessId] as a new process, as documented on termination signals.

On Windows, this was far trickier. I read on Ctrl+C an Ctrl+Break signals, on GenerateConsoleCtrlEvent, looked at how ASP.NET Core hosting handles its application lifetime, found (shocking!) an answer on StackOverflow, and determined that approach I needed was FreeConsole ->
AttachConsole -> GenerateConsoleCtrlEvent then wait for Process.HasExited.

Since this is somewhat involved and I wanted to make it cross-platform and easily reusable, I created the dotnet-stop tool which I can now depend on and invoke from any other dotnet tool. The whole tool fits nicely in a single C# 9 top-level Program.cs.

On Windows, invoking a separate tool for this was unavoidable (as far as I could manage), since I couldn’t re-acquire the console after signaling the external process, which left the calling tool in a broken state (wouldn’t even respond to Ctrl+C at that point). But this was perfectly acceptable for my dotnet-evergreen scenario anyway.

Usage is trivial, with a couple options to tweak how the stopping is done:

dotnet-stop
  Sends the SIGINT (Ctrl+C) signal to a process to gracefully stop it.

Usage:
  dotnet stop [options] <id>

Arguments:
  <id>  ID of the process to stop.

Options:
  -t, --timeout <timeout>  Optional timeout in milliseconds to wait for the process to exit.
  -q, --quiet              Do not display any output. [default: False]
  --version                Show version information
  -?, -h, --help           Show help and usage information