Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ctrl + C Doesn't Kill Gracefully | Node v6.11.3 - Git Bash #16103

Closed
dark-swordsman opened this issue Oct 9, 2017 · 34 comments
Closed

Ctrl + C Doesn't Kill Gracefully | Node v6.11.3 - Git Bash #16103

dark-swordsman opened this issue Oct 9, 2017 · 34 comments
Labels
process Issues and PRs related to the process subsystem. windows Issues and PRs related to the Windows platform.

Comments

@dark-swordsman
Copy link

dark-swordsman commented Oct 9, 2017

  • Version: v6.11.3
  • Platform: 64-bit Windows 10
  • CLI: Git 2.14.1
  • Express Version: 4.16.1

Referencing #4182 which seems similar (but at the end, it was stated that a new issue should be opened).

Ctrl + C does not kill the server. The resolution to the issue was using:

process.on('SIGINT', function() {
    process.exit();
});

however, when I use it, my server exits immediately. It completely skips app.listen() and exits completely, even when I previously kill the process on the specific port. Without the process.exit() function, it starts up and I can Ctrl + C to exit, but the process still runs in the background. I opened an issue on StackOverflow which hasn't gone anywhere really. This happens across multiple projects, so it's not just one project. I'm pretty sure it started happening when I updated node.

I am able to use netstat -aon to list it, then tskill <pid> to kill it afterwards, but this isn't really a solution.

Here's my code with my express server:

const express = require('express');
const app = express();

var config = require('./config')(process.env.NODE_ENV);
var appPort = process.env.PORT || config.port;

app.listen(appPort, function () {
  console.log('UAction Test Server Running on Port ' + appPort + '!');
});

process.on('SIGINT', function() {
  console.log( "\nGracefully shutting down from SIGINT (Ctrl-C)" );
  // some other closing procedures go here
  process.exit(1);
});

UPDATE
So the server is able to start now with:

process.on('SIGINT', function() {
  console.log( "\nGracefully shutting down from SIGINT (Ctrl-C)" );
  // some other closing procedures go here
  process.exit(1);
});

I added app.use('/', express.static('./src'));, but removing it didn't change anything.

When I do Ctrl + C, the console.log("\nGracefully shutting down from SIGINT (Ctrl-C)" ); doesn't appear, and trying to restart causes the same issue. Just for reference, here's the log:

$ npm start

> express-test@1.0.0 start E:\github\uaction\express-test
> node index.js

events.js:160
      throw er; // Unhandled 'error' event
      ^

Error: listen EADDRINUSE :::1337
    at Object.exports._errnoException (util.js:1020:11)
    at exports._exceptionWithHostPort (util.js:1043:20)
    at Server._listen2 (net.js:1258:14)
    at listen (net.js:1294:10)
    at Server.listen (net.js:1390:5)
    at EventEmitter.listen (E:\github\uaction\express-test\node_modules\express\lib\application.js:618:24)
    at Object.<anonymous> (E:\github\uaction\express-test\index.js:9:5)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)

npm ERR! Windows_NT 10.0.15063
npm ERR! argv "C:\\Program Files\\nodejs\\node.exe" "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js" "start"
npm ERR! node v6.11.3
npm ERR! npm  v3.10.10
npm ERR! code ELIFECYCLE
npm ERR! express-test@1.0.0 start: `node index.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the express-test@1.0.0 start script 'node index.js'.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the express-test package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     node index.js
npm ERR! You can get information on how to open an issue for this project with:
npm ERR!     npm bugs express-test
npm ERR! Or if that isn't available, you can get their info via:
npm ERR!     npm owner ls express-test
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR!     E:\github\uaction\express-test\npm-debug.log
@refack refack added the windows Issues and PRs related to the Windows platform. label Oct 9, 2017
@refack
Copy link
Contributor

refack commented Oct 9, 2017

Hi @dark-swordsman,

I can't repro. Is there a specific way that you start the process?

@refack refack added the process Issues and PRs related to the process subsystem. label Oct 9, 2017
@dark-swordsman
Copy link
Author

dark-swordsman commented Oct 9, 2017

I start the process like any other. The package.json looks at "start": "node index.js". I use npm start to start the process. The process then starts with the following console messages:

$ npm start

> express-test@1.0.0 start E:\github\uaction\express-test
> node index.js

UAction Test Server Running on Port 1337!

I usually do Ctrl + C to exit the process, which usually gracefully kills it and it doesn't run in the background. What ends up happening is the command line returns to it's previous state:

$ npm start

> express-test@1.0.0 start E:\github\uaction\express-test
> node index.js

UAction Test Server Running on Port 1337!


DarkSwordsman@FUJIWARA_TOFU_SHOP MINGW64 /e/github/uaction/express-test (phonegap-testing)
$

Doing Ctrl + C without any process running results in a ^C being printed, which is normal behavior.

Then from there, doing npm start again (which I usually do and have done forever) results in a EADDRINUSE error (detailed above in OP).

netstat -aon shows the localhost process running with a PID (which is random every time):

$ netstat -aon

Active Connections

  Proto  Local Address          Foreign Address        State           PID
  TCP    0.0.0.0:135            0.0.0.0:0              LISTENING       1100
  TCP    0.0.0.0:445            0.0.0.0:0              LISTENING       4
  TCP    0.0.0.0:1337           0.0.0.0:0              LISTENING       8012

then tskill <pid> is used to kill it.

I did find this in the documentation, which may be relevant, but I am not sure:

"SIGTERM and SIGINT have default handlers on non-Windows platforms that resets the terminal mode before exiting with code 128 + signal number. If one of these signals has a listener installed, its default behavior will be removed (Node.js will no longer exit)."

According to that, I am not sure what I may have installed... unless it may be firebase tools? I will uninstall them temporarily and see if that fixes it, since I also changed that recently.

UPDATE
Uninstalling firebase, firebase-tools, phonegap, and cordova did not fix the issue.

@refack
Copy link
Contributor

refack commented Oct 9, 2017

UPDATE
Uninstalling firebase, firebase-tools, phonegap, and cordova did not fix the issue.

By my guess it wouldn't be a general CLI tool but rather something that traps keystrokes like ComEnu or clink.
There are several players in you scenario:

  1. minnty
  2. bash
  3. npm
  4. node
  5. conhost
  6. possibly other keystroke traps

I'll try to reproduce so I can investigate, but the simplest answer I can give you, don't launch node from mintty (Ref: #16104)

@dark-swordsman
Copy link
Author

dark-swordsman commented Oct 9, 2017

I just tried using Node.js command prompt (A CMD with Node: "Your environment has been set up for using Node.js 6.11.3 (x64) and npm.") and it was able to successfully kill and restart the process. It also confirms if I want to Terminate batch job:

E:\GitHub\uaction\express-test>npm start

> express-test@1.0.0 start E:\GitHub\uaction\express-test
> node index.js

UAction Test Server Running on Port 1337!
Terminate batch job (Y/N)? Y

E:\GitHub\uaction\express-test>npm start

> express-test@1.0.0 start E:\GitHub\uaction\express-test
> node index.js

UAction Test Server Running on Port 1337!
Terminate batch job (Y/N)? Y

E:\GitHub\uaction\express-test>

So I can confirm that this is an issue with (at least) MinTTY, hopefully not with Git Bash completely.

I will continue to find anything that would interfere with Bash (I may start with just a clean re-install; perhaps different settings) since I would prefer not to work in CMD... Lol

EDIT: I want to mention I had Git version 2.14.1 and am going to install version 2.14.2.2 which is the latest from Git.

UPDATE: Updating to Git version 2.14.2.2 did not work. I used the following settings:

- Use Git from Git Bash only
- Use the OpenSSL Library
- Checkout Windows-style, commit Unix-style line endings
- Use MinTTY (the deafult terminal of MSYS2)

- Enable file system caching
- Enable Git Credential Manager
- Disable symbolic links

I will come back and reinstall changing from MinTTY to CMD.

@dark-swordsman
Copy link
Author

dark-swordsman commented Oct 9, 2017

The issue was MinTTY. I don't know why, but changing to the Default Console Window fixed the issue.

EDIT: I referenced this issue in git-for-windows/git/issues/1219 which seems to be the most relevant issue. @dscho said something about MSYS2 being modified since v2.13.1 (which is what MinTTY uses) that causes this.

Here are the settings:

- Use Git from Git Bash only
- Use the OpenSSL Library
- Checkout Windows-style, commit Unix-style line endings
- Use Default Console Window (CMD)

- Enable file system caching
- Enable Git Credential Manager
- Disable symbolic links

Here's it gracefully closing and restarting:

DarkSwordsman@FUJIWARA_TOFU_SHOP MINGW64 /e/github/uaction/express-test (phonegap-testing)
$ npm start

> express-test@1.0.0 start E:\github\uaction\express-test
> node index.js

UAction Test Server Running on Port 1337!


DarkSwordsman@FUJIWARA_TOFU_SHOP MINGW64 /e/github/uaction/express-test (phonegap-testing)
$ npm start

> express-test@1.0.0 start E:\github\uaction\express-test
> node index.js

UAction Test Server Running on Port 1337!


DarkSwordsman@FUJIWARA_TOFU_SHOP MINGW64 /e/github/uaction/express-test (phonegap-testing)
$

Everyone tells me Mac is better, and I'm starting to believe that more and more everyday, but I really don't want to fork over thousands when I already have a great working computer.

Might try ElementaryOS though. 🤔

Sorry for cluttering Node with another issue, but hopefully this will help being here since many people may think it's Node's fault like I did.

@refack
Copy link
Contributor

refack commented Oct 9, 2017

Everyone tells me Mac is better, and I'm starting to believe that more and more everyday, but I really don't want to fork over thousands when I already have a great working computer.

There are a few of us who develop node on Windows, so it's not a completely uncharted territory. Personally I use ComEmu to run most of my shells; "Git bash", cmd, and "WSL bash", and I don't have zombie process problems.
I believe the problem you were experiencing was the shell was disconnecting from the process, but the process didn't receive the signal (similar in a sense to nohup). You could try running winpty npm start, I'm not sure but it might make sure the process dies if the tty streams are closed.

@dark-swordsman
Copy link
Author

@refack doing that, I get an error:

DarkSwordsman@FUJIWARA_TOFU_SHOP MINGW64 /e/github/uaction/express-test (phonegap-testing)
$ winpty npm start
winpty: error: cannot start 'npm': Not found in PATH

I have never used or heard of winpty before, and searching for a solution to that error didn't return anything.

@danikane
Copy link

danikane commented Oct 18, 2017

  • git version 2.14.2.windows.3
  • node version v6.11.4
  • Windows 10 x64

Hi all, so I experienced the same issue lately and as indicated here zombie processes were present when launching node in the git bash terminal MinTTY - launched from the default Git Bash shortcut and from the context menu on a folder - located in C:\Program Files\Git
So I decided to use bash.exe located in C:\Program Files\Git\bin and voilà- no more zombie processes!
Then I modified the shortcut and the windows shell context menu and no more nuisances.

See the screenshot below:
image

Hope this helps.

@KiritoCheng
Copy link

I also met the same problem, this is the latest git bash bug, if back to the previous version can kill the process node

@martinmckenna
Copy link

chiming in here to say I'm having the same issue.

Switching away from MinTTY fixed the issue

@hottredpen
Copy link

i has same problem ,but my colleague use old git-bash version not the problem , so I use the git-bash version 2.8.1 now :)

@dark-swordsman
Copy link
Author

@KiritoCheng @martinmckenna @hottredpen I suggest you guys post that in the actual issue with a little more information. git-for-windows/git#1219

I'm sure it will help dscho pinpoint the issue a little more.

@dscho
Copy link

dscho commented Nov 14, 2017

I'm sure it will help dscho pinpoint the issue a little more.

I am pretty certain that I know what the culprit is. It is time that is lacking.

Essentially, this code needs to be replaced with code that injects the thread in the entire process tree, i.e. reuses the enumeration in terminate_process_tree() even for the gentle case.

But it also needs to send a MSYS2/Cygwin signal instead if any of the encountered PIDs is actually referring to an MSYS2/Cygwin process. But only for that arm of the process tree.

Not an easy project. And as long as I am the only one to answer questions on the bug tracker like "why does my terminal not show any input anymore after I forget the - in ls | vim -?", it is unlikely that I will have the time to tackle involved projects such as overhauling the Ctrl+C handling in Git for Windows' Bash.

Of course, if one (or more) of you gentle people could give me a hand with those more mundane tickets. Then. Then I could...

@SamanthaAdrichem
Copy link

SamanthaAdrichem commented Dec 6, 2017

I also have this problem on windows. It starts occuring when i have an npm script with && in it
like a && b

then the CTRL + C command is never send to the b script.

$ npm -v
5.5.1

$ node --version
v8.9.1

@sedulous-mortal
Copy link

I was able to get all my node processes to die directly from the Git Bash shell on Windows 10 by typing taskkill -F -IM node.exe - this ends all the node processes on my computer at once. I found I could also use taskkill //F //IM node.exe. Not sure why both - and // work in this context. Hope this helps!

@SamanthaAdrichem
Copy link

SamanthaAdrichem commented Dec 19, 2017

We're just running it in phpstorm now instead of git bash. phpstorm does propperly kill the process for it kills the terminal.

But thnx though!

@dscho
Copy link

dscho commented Jan 11, 2018

Thanks to an excellent contribution by the excellent @afsmith92, the next Git for Windows version will come with a proper fix.

You can even get it early by downloading https://bintray.com/git-for-windows/pacman/download_file?file_path=x86_64%2Fmsys2-runtime-2.9.0-6-x86_64.pkg.tar.xz and unpacking its usr\bin\msys-2.0.dll over your existing Git's.

@afsmith92
Copy link

After further testing, it turns out my change didn't end up fixing the issue, but @dscho generously stepped in and took the time to resolve it.

@giovannipds
Copy link

giovannipds commented Jan 31, 2018

It's occurring to me too, node v8.9.3, Git for Windows 2.16.1 (x64), Windows 10 (x64), using ConEmu by cmder. Tried what @dscho suggested but it didn't solve it. Tried to update Node to v8.9.4 too, same thing... It "fixes" for a while when I restart.

@ktsangop
Copy link

ktsangop commented Feb 1, 2018

@dscho and @afsmith92 thank you for your effort! I can confirm that it works for my setup :

  • Git for windows version 2.15.1.2
  • Windows 7 x64

@38leinaD
Copy link

38leinaD commented Apr 4, 2018

I am facing the same issue for Gradle instead of nodejs:

Git for windows version 2.16.2
Windows 7 x64

Fixed when applying the patch from @dscho .
Should i create a seperate issue for it? It seems to be the same/related problem.

@dscho
Copy link

dscho commented Apr 4, 2018

Fixed when applying the patch from @dscho

Then it should be fixed in Git for Windows v2.17.0, which was released yesterday...

@msdevstep
Copy link

I can confirm that v2.17.0 resolved the issue after installing over the top of my existing installation. Thanks @dscho

@38leinaD
Copy link

38leinaD commented Apr 6, 2018

@dscho I collegue of mine and me has the same result:
"./gradlew tasks" or similar gradle commands do not exit on Ctrl+C

It does not work for us on 2.17.0. It only works again when copying in the dll from here afterwars: https://bintray.com/git-for-windows/pacman/download_file?file_path=x86_64%2Fmsys2-runtime-2.9.0-6-x86_64.pkg.tar.xz

Git for windows version 2.17.0
Windows 7 x64

@dscho
Copy link

dscho commented Apr 6, 2018

It does not work for us on 2.17.0

There is exactly one way how it can work. But there are gazillions of ways for it not to work. Would you care to impart some details?

@38leinaD
Copy link

@dscho what kind of information would help?
To summarize:

Git for windows version 2.17.0
Windows 7 x64
java version "1.8.0_161"
Java(TM) SE Runtime Environment (build 1.8.0_161-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.161-b12, mixed mode)

curl -O https://gradle.org/next-steps/?version=4.6&format=bin
unzip gradle-4.6-bin.zip
mkdir testapp
cd testapp/
../gradle-4.6/bin/gradle init --type java-application
../gradle-4.6/bin/gradle clean build // press CTRL+C

This task takes a few seconds to download depdenencies the first time; so just any example where you cannot kill the process.

Now i apply your msys-2.0.dll from this thread and try it again.

../gradle-4.6/bin/gradle clean build // press CTRL+C
Now it works.

@dscho
Copy link

dscho commented Apr 11, 2018

Now i apply your msys-2.0.dll from this thread and try it again.

../gradle-4.6/bin/gradle clean build // press CTRL+C
Now it works.

I guess the culprit is that I had to change this again because it caused more problems. Ctrl+C is really a vexing problem, and whatever way we do it, it seems to break somebody's scenario.

So currently we call GenerateConsoleCtrlEvent() because that was the only way node.js programs could react to the signal. But then your Java example seems to fail to interrupt? What seems to work for you is the thing we did before, where we inserted a remote thread calling ExitProcess() (and we had to enumerate the process tree to do it correctly), but the problem with that was that the SIGINT could not be caught because there was none.

Damn if you do, damn if you don't.

dscho added a commit to git-for-windows/msys2-runtime that referenced this issue Apr 23, 2018
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
dscho added a commit to git-for-windows/MSYS2-packages that referenced this issue Apr 23, 2018
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
git-for-windows/msys2-runtime@c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
git-for-windows/msys2-runtime@e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
git-for-windows/msys2-runtime@53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
git-for-windows/msys2-runtime@ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
@dscho
Copy link

dscho commented Apr 23, 2018

Okay, so I spent way too many hours on trying to fix this, but I have something to show: The newest Git for Windows snapshot at https://wingit.blob.core.windows.net/files/index.html should fix this. Please test.

@codynova
Copy link

@dscho Thanks for your hard work on this, it's been plaguing me forever. I actually had a script just to call taskkill on node. I've tested the snapshot (commit e6ee2f786d) on Windows 10 and Node v9.8 - it's working like a charm! 😁

@38leinaD
Copy link

38leinaD commented May 2, 2018

@dscho For the gradle-scenario described above it does not work. the process does not terminate on ctrl-c.

dscho added a commit to git-for-windows/msys2-runtime that referenced this issue Nov 9, 2018
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
@steranka
Copy link

steranka commented Nov 24, 2018

For the record, this happens with cygwin as well.

$ cygcheck --version
cygcheck (msys) 2.6.0
System Checker for Msys
Copyright (C) 1998 - 2016 Cygwin Authors

I searched and found this command helps me know what process to kill:

wmic process where "name like '%node%'" get processid,commandline

Found from https://superuser.com/questions/1003921/how-to-show-full-command-line-of-all-processes-in-windows

@dscho
Copy link

dscho commented Nov 26, 2018

For the record, this happens with cygwin as well.

$ cygcheck --version
cygcheck (msys) 2.6.0
System Checker for Msys
Copyright (C) 1998 - 2016 Cygwin Authors

That looks more like MSYS than Cygwin. (And a pretty old one at that.)

@ozyman42
Copy link

Does anyone know of a workaround for the Mintty shell that ships with MSYS2? I don't want to give up MSYS2 or use git bash, because I love tmux and pacman too much.

dscho added a commit to git-for-windows/msys2-runtime that referenced this issue Feb 16, 2019
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
dscho added a commit to git-for-windows/msys2-runtime that referenced this issue Feb 20, 2019
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
dscho added a commit to git-for-windows/msys2-runtime that referenced this issue Feb 20, 2019
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
git-for-windows-ci pushed a commit to git-for-windows/msys2-runtime that referenced this issue Mar 5, 2019
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
git-for-windows-ci pushed a commit to git-for-windows/msys2-runtime that referenced this issue Mar 9, 2019
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
git-for-windows-ci pushed a commit to git-for-windows/msys2-runtime that referenced this issue Mar 16, 2019
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
git-for-windows-ci pushed a commit to git-for-windows/msys2-runtime that referenced this issue Mar 31, 2019
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
git-for-windows-ci pushed a commit to git-for-windows/msys2-runtime that referenced this issue Apr 6, 2019
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
@neddongo
Copy link

neddongo commented Feb 21, 2021

So what about those of us who want to use mintty? We're just SOL?
This literally doesn't happen with any other program but Node, so why do you blame the terminal?

naveen521kk pushed a commit to naveen521kk/msys2-runtime that referenced this issue Mar 15, 2021
This thing again...

Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is
sent to the foreground process and its child processes. This signal can
be intercepted by installing a signal handler for this specific signal.
On Windows, there is no precise equivalent for this system.

Instead, the Ctrl+C is translated by the current ConHost (i.e. the
container running the Console processes) to a ConsoleCtrl event that is
sent to all processes attached to that Console. If any of these
processes installed a handler via SetConsoleCtrlHandler(), they can
intercept that event (and avoid exiting or doing some cleanup work).

On Linux and macOS (and every Unix flavor, really), processes can also
be killed via the `kill` executable, which really just sends a signal to
the process, typically SIGTERM. Processes can intercept that signal,
too. To force processes to terminate, without giving them any chance to
prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on
Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used,
but it only kills one process (unlike SIGKILL, which is sent also to the
child processes).

In Git for Windows, we struggled with emulating SIGINT, SIGTERM and
SIGKILL handling essentially since the beginning of the efforts to port
Git to Windows.

At least the SIGINT part of the problem becomes a lot worse when using a
terminal window other than cmd.exe's: as long as using cmd.exe (AKA
"ConHost"), Ctrl+C is handled entirely outside of our code. But with the
big jump from v1.x to v2.x, Git for Windows not only switched from MSys
to MSYS2, but also started using MinTTY as the default terminal window,
which uses the MSYS2 runtime-provided pseudo terminals (inherited from
Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin).
When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by
our code.

The original code to handle Ctrl+C comes straight from Cygwin. It simply
ignores the entire conundrum for non-Cygwin processes and simply calls
TerminateProcess() on them, leaving spawned child processes running.

The first attempt at fixing "the Ctrl+C problem" (with the symptom that
interrupting `git clone ...` would not stop the actual download of the
Git objects that was still running in a child process) was
git-for-windows/msys2-runtime@c4ba4e3357f. It
simply enumerated all the processes' process IDs and parent process IDs
and extracted the tree of (possibly transitive) child processes of the
process to kill, then called TerminateProcess() on them.

This solved the problem with interrupting `git clone`, but it did not
address the problem that Git typically wants to "clean up" when being
interrupted. In particular, Git installs atexit() and signal handlers to
remove .lock files. The most common symptom was that a stale
.git/index.lock file was still present after interrupting a Git process.

Based on the idea presented in Dr Dobb's Journal in the article "A Safer
Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999)
http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
we changed our handling to inject a remote thread calling ExitProcess()
first, and fall back to TerminateProcess() the process tree instead:
git-for-windows/msys2-runtime@e9cb332976c

This change was a little misguided in hindsight, as it only called
TerminateProcess() on the process tree, but expected the atexit()
handler of Git to take care of the child processes when killing the
process via the remote ExitProcess() method.

Therefore, we changed the strategy once again, to inject ExitProcess()
threads into the child processes of the process to kill, too:
git-for-windows/msys2-runtime@53e5c0313e1

(That commit also tries to handle Cygwin process among the child
processes by sending Cygwin signals, but unfortunately that part of the
commit was buggy.)

This worked well for Git processes. However, Git Bash is used in all
kinds of circumstances, including launching Maven, or node.js scripts
that want to intercept SIGINT. Naturally, these callees have no idea
that Git for Windows injects an ExitProcess() with exit code 130
(corresponding to 0x100 + SIGINT). Therefore, they never "got" the
signal.

So what is it that happens when ConHost generates a ConsoleCtrl event?
This question was asked and answered in the excellent blog post at:
http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880

Essentially, the same happens as what we did with ExitProcess(): a
remote thread gets injected, with the event type as parameter. Of course
it is not ExitProcess() that is called, but CtrlRoutine(). This function
lives in kernel32.dll, too, but it is not exported, i.e.
GetProcAddress() won't find it. The trick proposed in the blog post (to
send a test ConsoleCtrl event to the process itself, using a special
handler that then inspects the stack trace to figure out the address of
the caller) does not work for us, however: it would send a
CTRL_BREAK_EVENT to *all* processes attached to the same Console,
essentially killing MinTTY.

But could we make this still work somehow? Yes, we could. We came up
with yet another trick up our sleeves: instead of determining the
address of kernel32!CtrlRoutine in our own process, we spawn a new one,
with a new Console, to avoid killing MinTTY. To do that, we need a
helper .exe, of course, which we put into /usr/libexec/. If this helper
is not found, we fall back to the previous methods of injecting
ExitProcess() or calling TerminateProcess().

This method (to spawn a helper .exe) has a further incidental benefit:
by compiling 32-bit *and* 64-bit helpers and providing them as
getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit
processes in a 64-bit Git for Windows. Sadly not vice versa: calling
CreateRemoteThread() on a 64-bit process from a 32-bit process seems to
fail all the time (and require a lot of assembly hackery to fix that I
am not really willing to include in Git for Windows' MSYS2 runtime).

The current method was implemented in this commit:
git-for-windows/msys2-runtime@ca6188a7520

This is the hopeful final fix for
git-for-windows/git#1491,
git-for-windows/git#1470,
git-for-windows/git#1248,
git-for-windows/git#1239,
git-for-windows/git#227,
git-for-windows/git#1553,
nodejs/node#16103, and plenty other tickets
that petered out mostly due to a willingness of community members to
leave all the hard work to a single, already overworked person.

This fix also partially helps
git-for-windows/git#1629 (only partially
because the user wanted to quit the pager using Ctrl+C, which is not the
intended consequence of a Ctrl+C: it should stop the Git process, but
not the pager).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
process Issues and PRs related to the process subsystem. windows Issues and PRs related to the Windows platform.
Projects
None yet
Development

No branches or pull requests