Skip to content

Commit

Permalink
exit_process.h: fix handling of SIGINT and SIGTERM
Browse files Browse the repository at this point in the history
Handle SIGINT and SIGTERM by injecting into the process
a thread that runs ExitProcess. Use TerminateProcess otherwise.

In both cases, enumerate the entire process tree.

This fixes git-for-windows/git#1219

Signed-off-by: Adam Smith <afsmith92@gmail.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
  • Loading branch information
Adam Smith authored and dscho committed Jan 10, 2018
1 parent 9f53930 commit 53e5c03
Showing 1 changed file with 56 additions and 38 deletions.
94 changes: 56 additions & 38 deletions winsup/cygwin/include/cygwin/exit_process.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,56 @@

#include <tlhelp32.h>

static int
terminate_process_with_remote_thread(HANDLE process, int exit_code)
{
static LPTHREAD_START_ROUTINE exit_process_address;
if (!exit_process_address)
{
HINSTANCE kernel32 = GetModuleHandle ("kernel32");
exit_process_address = (LPTHREAD_START_ROUTINE)
GetProcAddress (kernel32, "ExitProcess");
}
DWORD thread_id;
HANDLE thread = !exit_process_address ? NULL :
CreateRemoteThread (process, NULL, 0, exit_process_address,
(PVOID)exit_code, 0, &thread_id);

if (thread)
{
CloseHandle (thread);
/*
* Wait 10 seconds (arbitrary constant) for the process to
* finish; After that grace period, fall back to terminating
* non-gently.
*/
if (WaitForSingleObject (process, 10000) == WAIT_OBJECT_0)
return 0;
}

return -1;
}

/**
* Terminates the process corresponding to the process ID and all of its
* directly and indirectly spawned subprocesses.
* Terminates the process corresponding to the process ID
*
* This way of terminating the processes is not gentle: the processes get
* no chance of cleaning up after themselves (closing file handles, removing
* This way of terminating the processes is not gentle: the process gets
* no chance of cleaning up after itself (closing file handles, removing
* .lock files, terminating spawned processes (if any), etc).
*/
static int
terminate_process_tree(HANDLE main_process, int exit_code)
terminate_process(HANDLE process, int exit_code)
{
return int(TerminateProcess (process, exit_code));
}

/**
* Terminates the process corresponding to the process ID and all of its
* directly and indirectly spawned subprocesses using the provided
* terminate callback function
*/
static int
terminate_process_tree(HANDLE main_process, int exit_code, int (*terminate)(HANDLE, int))
{
HANDLE snapshot = CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 entry;
Expand All @@ -46,6 +86,7 @@ terminate_process_tree(HANDLE main_process, int exit_code)
for (;;)
{
int orig_len = len;
pid_t cyg_pid;

memset (&entry, 0, sizeof (entry));
entry.dwSize = sizeof (entry);
Expand All @@ -57,6 +98,12 @@ terminate_process_tree(HANDLE main_process, int exit_code)
{
for (i = len - 1; i >= 0; i--)
{
cyg_pid = cygwin_winpid_to_pid(entry.th32ProcessID);
if (cyg_pid > -1)
{
kill(cyg_pid, exit_code);
continue;
}
if (pids[i] == entry.th32ProcessID)
break;
if (pids[i] == entry.th32ParentProcessID)
Expand All @@ -76,7 +123,7 @@ terminate_process_tree(HANDLE main_process, int exit_code)

if (process)
{
if (!TerminateProcess (process, exit_code))
if (!(*terminate) (process, exit_code))
ret = -1;
CloseHandle (process);
}
Expand Down Expand Up @@ -129,39 +176,10 @@ exit_process(HANDLE process, int exit_code)

if (GetExitCodeProcess (process, &code) && code == STILL_ACTIVE)
{
/*
* We cannot determine the address of ExitProcess() for a process
* that does not match the current architecture (e.g. for a 32-bit
* process when we're running in 64-bit mode).
*/
if (process_architecture_matches_current (process))
{
static LPTHREAD_START_ROUTINE exit_process_address;
if (!exit_process_address)
{
HINSTANCE kernel32 = GetModuleHandle ("kernel32");
exit_process_address = (LPTHREAD_START_ROUTINE)
GetProcAddress (kernel32, "ExitProcess");
}
DWORD thread_id;
HANDLE thread = !exit_process_address ? NULL :
CreateRemoteThread (process, NULL, 0, exit_process_address,
(PVOID)exit_code, 0, &thread_id);

if (thread)
{
CloseHandle (thread);
/*
* Wait 10 seconds (arbitrary constant) for the process to
* finish; After that grace period, fall back to terminating
* non-gently.
*/
if (WaitForSingleObject (process, 10000) == WAIT_OBJECT_0)
return 0;
}
}
if (process_architecture_matches_current(process) && (exit_code == SIGINT || exit_code == SIGTERM))
return terminate_process_tree (process, exit_code, terminate_process_with_remote_thread);

return terminate_process_tree (process, exit_code);
return terminate_process_tree (process, exit_code, terminate_process);
}

return -1;
Expand Down

16 comments on commit 53e5c03

@afsmith92
Copy link

Choose a reason for hiding this comment

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

@dscho Unfortunately after further investigation it seems the change I added doesn't fix the node ctrl+c issue.

All of the child processes are killed, but processes still can't respond to SIGINT or SIGKILL. It seems that line 179 always evaluates to false, and TerminateProcess is always used. It looks like after this line exit_code === SIGINT and exit_code === SIGTERM always evaluates to false. If I force the use of terminate_process_with_remote_thread. The node child processes don't get closed and this cleanup code doesn't run either:

process.on('SIGTERM', function() {
  console.log( "\nresponding to SIGTERM" );
  // other cleanup code
  process.exit(1);
});

process.on('SIGINT', function() {
  console.log( "\nresponding to SIGINT" );
  // other cleanup code
  process.exit(1);
});

I'm continuing to investigate. I'll see if I can make a change to terminate_process_with_remote_thread that resolves the issue.

@dscho
Copy link
Member

@dscho dscho commented on 53e5c03 Jan 11, 2018

Choose a reason for hiding this comment

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

The kill utility should not be involved at all, so that line 179 should not be hit at all...

@afsmith92
Copy link

Choose a reason for hiding this comment

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

Where is exit_process called when responding to ctrl+c? If it's passing the exit code in the same way e.g. sig + 128 then the exit_code == SIGINT check will fail.

@dscho
Copy link
Member

@dscho dscho commented on 53e5c03 Jan 12, 2018

Choose a reason for hiding this comment

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

It's called via signal() in exceptions.cc. AFAICT you call exit_process() correctly there...

@dscho
Copy link
Member

@dscho dscho commented on 53e5c03 Jan 12, 2018

Choose a reason for hiding this comment

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

Oh no. I was mistaken!

exit_process (ch_spawn, 128 + (sigExeced = si.si_signo));

@dscho
Copy link
Member

@dscho dscho commented on 53e5c03 Jan 12, 2018

Choose a reason for hiding this comment

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

Maybe we should pass the real code instead, and let exit_process() decide whether to add 128 or not?

@dscho
Copy link
Member

@dscho dscho commented on 53e5c03 Jan 12, 2018

Choose a reason for hiding this comment

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

I made up my mind and think that testing exit_code & 0x7f might be better. My proposed fix: dscho@fix-ctrl-c-again

What do you think?

@dscho
Copy link
Member

@dscho dscho commented on 53e5c03 Jan 12, 2018

Choose a reason for hiding this comment

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

I tested this with Node v6.10.2 as shipped in MSYS2 and with this script:

#!/usr/bin/env node

process.on('SIGTERM', function() {
  console.log( "\nresponding to SIGTERM" );
  // other cleanup code
  process.exit(1);
});

process.on('SIGINT', function() {
  console.log( "\nresponding to SIGINT" );
  // other cleanup code
  process.exit(1);
});

var http = require('http');

http.createServer(function (request, response) {
    response.writeHead(200, {
        'Content-Type': 'text/plain',
        'Access-Control-Allow-Origin' : '*'
    });
    response.end('Hello World\n');
}).listen(1337);

This is the "screenshot":

$ node handle-ctrl-c.js

responding to SIGINT

So I call this success and will push my fix ;-)

@dscho
Copy link
Member

@dscho dscho commented on 53e5c03 Jan 12, 2018

Choose a reason for hiding this comment

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

Actually... After testing a bit further, it looks like the new code path is not hit in that case, and I cannot get it to work with kill.exe. Will keep digging, even if I do not really have any time to spend on this ;-)

@afsmith92
Copy link

@afsmith92 afsmith92 commented on 53e5c03 Jan 12, 2018

Choose a reason for hiding this comment

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

@dscho I'm continuing to investigate as well. Would it be best to revert the patch at this point?

Maybe we should pass the real code instead, and let exit_process() decide whether to add 128 or not?

That sounds reasonable to me.

@afsmith92
Copy link

Choose a reason for hiding this comment

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

Also, I found a different implementation of the safe ExitProcess in this thread -- I've been messing with this.

@dscho
Copy link
Member

@dscho dscho commented on 53e5c03 Jan 12, 2018

Choose a reason for hiding this comment

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

After chasing down this rabbit hole, I finally have something that really seems to work: dscho@fix-ctrl-c-again

@afsmith92 would you mind testing this? It also requires kill.exe to be rebuilt (cd ../utils && make kill.exe after you built msys0.dll), as it has to spawn kill.exe process so that that one can attach to the Console of the target process and send the Ctrl event.

@afsmith92
Copy link

Choose a reason for hiding this comment

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

Absolutely. I'll have it tested within the next half hour.

@afsmith92
Copy link

Choose a reason for hiding this comment

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

Still compiling..

@afsmith92
Copy link

@afsmith92 afsmith92 commented on 53e5c03 Jan 13, 2018

Choose a reason for hiding this comment

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

Works perfectly. Tested in a PortableGit with node v6.11.2. Killed all child processes and was able to handle SIGINT:

process.on('SIGINT', function() {
  console.log( "\nresponding to SIGINT" );
  // other cleanup code
  process.exit(1);
});

logged responding to SIGINT to the console after ctrl +c

Thank you so much for taking the time to figure this out, and I'm sorry for getting you dragged into this issue. I was only following the issue on git-for-windows -- not the issue logged to node -- and I wasn't aware of the issues with SIGINT and SIGTERM handling (until you tagged me in the node issue) and thus only tested for the child processes issue described in the git-for-windows issue.

I'll keep an eye on the issues list for this repo, and I'll jump in if I see anything I think I can help with in the future.

@dscho
Copy link
Member

@dscho dscho commented on 53e5c03 Jan 13, 2018

Choose a reason for hiding this comment

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

No worries! You helped. That means a lot to me. And thanks for verifying my latest attempt at a fix!

Please sign in to comment.