Skip to content

Commit

Permalink
Merge pull request #1170 from dscho/mingw-kill-process
Browse files Browse the repository at this point in the history
Handle Ctrl+C in Git Bash nicely
  • Loading branch information
dscho committed May 30, 2017
2 parents d218178 + fd4860e commit a56c243
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 8 deletions.
74 changes: 66 additions & 8 deletions compat/mingw.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "../strbuf.h"
#include "../run-command.h"
#include "../cache.h"
#include "win32/exit-process.h"

#define HCAST(type, handle) ((type)(intptr_t)handle)

Expand Down Expand Up @@ -1458,10 +1459,44 @@ struct pinfo_t {
static struct pinfo_t *pinfo = NULL;
CRITICAL_SECTION pinfo_cs;

#ifndef SIGRTMAX
#define SIGRTMAX 63
#endif

static void kill_child_processes_on_signal(void)
{
DWORD status;

/*
* Only continue if the process was terminated by a signal, as
* indicated by the exit status (128 + sig_no).
*
* As we are running in an atexit() handler, the exit code has been
* set at this stage by the ExitProcess() function already.
*/
if (!GetExitCodeProcess(GetCurrentProcess(), &status) ||
status <= 128 || status > 128 + SIGRTMAX)
return;

EnterCriticalSection(&pinfo_cs);

while (pinfo) {
struct pinfo_t *info = pinfo;
pinfo = pinfo->next;
if (exit_process(info->proc, status))
/* the handle is still valid in case of error */
CloseHandle(info->proc);
free(info);
}

LeaveCriticalSection(&pinfo_cs);
}

static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaenv,
const char *dir,
int prepend_cmd, int fhin, int fhout, int fherr)
{
static int atexit_handler_initialized;
STARTUPINFOW si;
PROCESS_INFORMATION pi;
struct strbuf args;
Expand All @@ -1471,6 +1506,17 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
HANDLE cons;
const char *strace_env;

if (!atexit_handler_initialized) {
atexit_handler_initialized = 1;
/*
* On Windows, there is no POSIX signaling. Instead, we inject
* a thread calling ExitProcess(128 + sig_no); and that calls
* the *atexit* handlers. Catch this condition and kill child
* processes with the same signal.
*/
atexit(kill_child_processes_on_signal);
}

do_unset_environment_variables();

/* Determine whether or not we are associated to a console */
Expand Down Expand Up @@ -1705,16 +1751,28 @@ int mingw_execvp(const char *cmd, char *const *argv)
int mingw_kill(pid_t pid, int sig)
{
if (pid > 0 && sig == SIGTERM) {
HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pid);

if (TerminateProcess(h, -1)) {
HANDLE h = OpenProcess(PROCESS_CREATE_THREAD |
PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION | PROCESS_VM_WRITE |
PROCESS_VM_READ | PROCESS_TERMINATE,
FALSE, pid);
int ret;

if (h)
ret = exit_process(h, 128 + sig);
else {
h = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (!h) {
errno = err_win_to_posix(GetLastError());
return -1;
}
ret = terminate_process_tree(h, 128 + sig);
}
if (ret) {
errno = err_win_to_posix(GetLastError());
CloseHandle(h);
return 0;
}

errno = err_win_to_posix(GetLastError());
CloseHandle(h);
return -1;
return ret;
} else if (pid > 0 && sig == 0) {
HANDLE h = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if (h) {
Expand Down
164 changes: 164 additions & 0 deletions compat/win32/exit-process.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#ifndef EXIT_PROCESS_H
#define EXIT_PROCESS_H

/*
* This file contains functions to terminate a Win32 process, as gently as
* possible.
*
* At first, we will attempt to inject a thread that calls ExitProcess(). If
* that fails, we will fall back to terminating the entire process tree.
*
* For simplicity, these functions are marked as file-local.
*/

#include <tlhelp32.h>

/*
* Terminates the process corresponding to the process ID and all of its
* directly and indirectly spawned subprocesses.
*
* This way of terminating the processes is not gentle: the processes get
* no chance of cleaning up after themselves (closing file handles, removing
* .lock files, terminating spawned processes (if any), etc).
*/
static int terminate_process_tree(HANDLE main_process, int exit_status)
{
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 entry;
DWORD pids[16384];
int max_len = sizeof(pids) / sizeof(*pids), i, len, ret = 0;
pid_t pid = GetProcessId(main_process);

pids[0] = (DWORD)pid;
len = 1;

/*
* Even if Process32First()/Process32Next() seem to traverse the
* processes in topological order (i.e. parent processes before
* child processes), there is nothing in the Win32 API documentation
* suggesting that this is guaranteed.
*
* Therefore, run through them at least twice and stop when no more
* process IDs were added to the list.
*/
for (;;) {
int orig_len = len;

memset(&entry, 0, sizeof(entry));
entry.dwSize = sizeof(entry);

if (!Process32First(snapshot, &entry))
break;

do {
for (i = len - 1; i >= 0; i--) {
if (pids[i] == entry.th32ProcessID)
break;
if (pids[i] == entry.th32ParentProcessID)
pids[len++] = entry.th32ProcessID;
}
} while (len < max_len && Process32Next(snapshot, &entry));

if (orig_len == len || len >= max_len)
break;
}

for (i = len - 1; i > 0; i--) {
HANDLE process = OpenProcess(PROCESS_TERMINATE, FALSE, pids[i]);

if (process) {
if (!TerminateProcess(process, exit_status))
ret = -1;
CloseHandle(process);
}
}
if (!TerminateProcess(main_process, exit_status))
ret = -1;
CloseHandle(main_process);

return ret;
}

/**
* Determine whether a process runs in the same architecture as the current
* one. That test is required before we assume that GetProcAddress() returns
* a valid address *for the target process*.
*/
static inline int process_architecture_matches_current(HANDLE process)
{
static BOOL current_is_wow = -1;
BOOL is_wow;

if (current_is_wow == -1 &&
!IsWow64Process (GetCurrentProcess(), &current_is_wow))
current_is_wow = -2;
if (current_is_wow == -2)
return 0; /* could not determine current process' WoW-ness */
if (!IsWow64Process (process, &is_wow))
return 0; /* cannot determine */
return is_wow == current_is_wow;
}

/**
* Inject a thread into the given process that runs ExitProcess().
*
* Note: as kernel32.dll is loaded before any process, the other process and
* this process will have ExitProcess() at the same address.
*
* This function expects the process handle to have the access rights for
* CreateRemoteThread(): PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION,
* PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ.
*
* The idea comes from the Dr Dobb's article "A Safer Alternative to
* TerminateProcess()" by Andrew Tucker (July 1, 1999),
* http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547
*
* If this method fails, we fall back to running terminate_process_tree().
*/
static int exit_process(HANDLE process, int exit_code)
{
DWORD code;

if (GetExitCodeProcess(process, &code) && code == STILL_ACTIVE) {
static int initialized;
static LPTHREAD_START_ROUTINE exit_process_address;
PVOID arg = (PVOID)(intptr_t)exit_code;
DWORD thread_id;
HANDLE thread = NULL;

if (!initialized) {
HINSTANCE kernel32 = GetModuleHandle("kernel32");
if (!kernel32)
die("BUG: cannot find kernel32");
exit_process_address = (LPTHREAD_START_ROUTINE)
GetProcAddress(kernel32, "ExitProcess");
initialized = 1;
}
if (!exit_process_address ||
!process_architecture_matches_current(process))
return terminate_process_tree(process, exit_code);

thread = CreateRemoteThread(process, NULL, 0,
exit_process_address,
arg, 0, &thread_id);
if (thread) {
CloseHandle(thread);
/*
* If the process survives for 10 seconds (a completely
* arbitrary value picked from thin air), fall back to
* killing the process tree via TerminateProcess().
*/
if (WaitForSingleObject(process, 10000) ==
WAIT_OBJECT_0) {
CloseHandle(process);
return 0;
}
}

return terminate_process_tree(process, exit_code);
}

return 0;
}

#endif

0 comments on commit a56c243

Please sign in to comment.