Skip to content

Commit

Permalink
Emulate GenerateConsoleCtrlEvent() upon Ctrl+C
Browse files Browse the repository at this point in the history
This patch is heavily inspired by the Git for Windows' strategy in
handling Ctrl+C.

When a process is terminated via TerminateProcess(), it has no chance to
do anything in the way of cleaning up. This is particularly noticeable
when a lengthy Git for Windows process tries to update Git's index file
and leaves behind an index.lock file. Git's idea is to remove the stale
index.lock file in that case, using the signal and atexit handlers
available in Linux. But those signal handlers never run.

Note: this is not an issue for MSYS2 processes because MSYS2 emulates
Unix' signal system accurately, both for the process sending the kill
signal and the process receiving it. Win32 processes do not have such a
signal handler, though, instead MSYS2 shuts them down via
`TerminateProcess()`.

For a while, Git for Windows tried to use a gentler method, described in
the Dr Dobb's article "A Safer Alternative to TerminateProcess()" by
Andrew Tucker (July 1, 1999),
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547

Essentially, we injected a new thread into the running process that does
nothing else than running the ExitProcess() function.

However, this was still not in line with the way CMD handles Ctrl+C: it
gives processes a chance to do something upon Ctrl+C by calling
SetConsoleCtrlHandler(), and ExitProcess() simply never calls that
handler.

So for a while we tried to handle SIGINT/SIGTERM by attaching to the
console of the command to interrupt, and generating the very same event
as CMD does via GenerateConsoleCtrlEvent().

This method *still* was not correct, though, as it would interrupt
*every* process attached to that Console, not just the process (and its
children) that we wanted to signal. A symptom was that hitting Ctrl+C
while `git log` was shown in the pager would interrupt *the pager*.

The method we settled on is to emulate what GenerateConsoleCtrlEvent()
does, but on a process by process basis: inject a remote thread and call
the (private) function kernel32!CtrlRoutine.

To obtain said function's address, we use the dbghelp API to generate a
stack trace from a handler configured via SetConsoleCtrlHandler() and
triggered via GenerateConsoleCtrlEvent(). To avoid killing each and all
processes attached to the same Console as the MSYS2 runtime, we modify
the cygwin-console-helper to optionally print the address of
kernel32!CtrlRoutine to stdout, and then spawn it with a new Console.

Note that this also opens the door to handling 32-bit process from a
64-bit MSYS2 runtime and vice versa, by letting the MSYS2 runtime look
for the cygwin-console-helper.exe of the "other architecture" in a
specific place (we choose /usr/libexec/, as it seems to be the
convention for helper .exe files that are not intended for public
consumption).

The 32-bit helper implicitly links to libgcc_s_dw2.dll and
libwinpthread-1.dll, so to avoid cluttering /usr/libexec/, we look for
the helped of the "other" architecture in the corresponding mingw32/ or
mingw64/ subdirectory.

Among other bugs, this strategy to handle Ctrl+C fixes the MSYS2 side of
the bug where interrupting `git clone https://...` would send the
spawned-off `git remote-https` process into the background instead of
interrupting it, i.e. the clone would continue and its progress would be
reported mercilessly to the console window without the user being able
to do anything about it (short of firing up the task manager and killing
the appropriate task manually).

Note that this special-handling is only necessary when *MSYS2* handles
the Ctrl+C event, e.g. when interrupting a process started from within
MinTTY or any other non-cmd-based terminal emulator. If the process was
started from within `cmd.exe`'s terminal window, child processes are
already killed appropriately upon Ctrl+C, by `cmd.exe` itself.

Also, we can't trust the processes to end it's subprocesses upon receiving
Ctrl+C. For example, `pip.exe` from `python-pip` doesn't kill the python
it lauches (it tries to but fails), and I noticed that in cmd it kills python
also correctly, which mean we should kill all the process using
`exit_process_tree`.

Co-authored-by: Naveen M K <naveen@syrusdark.website>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
  • Loading branch information
dscho and naveen521kk committed Feb 27, 2024
1 parent 5b98304 commit 35a3691
Show file tree
Hide file tree
Showing 2 changed files with 384 additions and 4 deletions.
24 changes: 20 additions & 4 deletions winsup/cygwin/exceptions.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ details. */
#include "exception.h"
#include "posix_timer.h"
#include "gcc_seh.h"
#include "cygwin/exit_process.h"

/* Define macros for CPU-agnostic register access. The _CX_foo
macros are for access into CONTEXT, the _MC_foo ones for access into
Expand Down Expand Up @@ -1598,10 +1599,25 @@ sigpacket::process ()
dosig:
if (have_execed)
{
sigproc_printf ("terminating captive process");
if (::cygheap->ctty)
::cygheap->ctty->cleanup_before_exit ();
TerminateProcess (ch_spawn, sigExeced = si.si_signo);
switch (si.si_signo)
{
case SIGUSR1:
case SIGUSR2:
case SIGCONT:
case SIGSTOP:
case SIGTSTP:
case SIGTTIN:
case SIGTTOU:
system_printf ("Suppressing signal %d to win32 process (pid %u)",
(int)si.si_signo, (unsigned int)GetProcessId(ch_spawn));
goto done;
default:
sigproc_printf ("terminating captive process");
if (::cygheap->ctty)
::cygheap->ctty->cleanup_before_exit ();
rc = exit_process_tree (ch_spawn, 128 + (sigExeced = si.si_signo));
goto done;
}
}
/* Dispatch to the appropriate function. */
sigproc_printf ("signal %d, signal handler %p", si.si_signo, handler);
Expand Down
Loading

0 comments on commit 35a3691

Please sign in to comment.