From ef517b19882f88ddbd83cfdfac9dec28cf82540d Mon Sep 17 00:00:00 2001 From: James Johnston <johnstonj.public@codenest.com> Date: Thu, 2 Jul 2015 05:19:12 +0000 Subject: [PATCH] Process: Added initial support for process groups. kwsysProcess_SetOption now allows you to specify a new kwsysProcess_Option_CreateProcessGroup option, which creates the process in a new process group (Windows/UNIX) and a new session (UNIX). Child process groups receive signals separately from their parents. This allowed for the introduction of the new kwsysProcess_Interrupt function, which allows one to safely request the child process in its own group to terminate. The Ctrl+C handler also manually sends that signal to child process groups, since it's no longer automatic. Change-Id: Id0a420ad65f1b1c1d299ac0eb95fbb8b50a52409 --- Process.h.in | 131 ++++++++++++++++------------ ProcessUNIX.c | 230 ++++++++++++++++++++++++++++++++++++++++++++----- ProcessWin32.c | 123 ++++++++++++++++++++++---- 3 files changed, 394 insertions(+), 90 deletions(-) diff --git a/Process.h.in b/Process.h.in index e35939f..c5ebc97 100644 --- a/Process.h.in +++ b/Process.h.in @@ -23,58 +23,60 @@ # define kwsysEXPORT @KWSYS_NAMESPACE@_EXPORT #endif #if !@KWSYS_NAMESPACE@_NAME_IS_KWSYS -# define kwsysProcess kwsys_ns(Process) -# define kwsysProcess_s kwsys_ns(Process_s) -# define kwsysProcess_New kwsys_ns(Process_New) -# define kwsysProcess_Delete kwsys_ns(Process_Delete) -# define kwsysProcess_SetCommand kwsys_ns(Process_SetCommand) -# define kwsysProcess_AddCommand kwsys_ns(Process_AddCommand) -# define kwsysProcess_SetTimeout kwsys_ns(Process_SetTimeout) -# define kwsysProcess_SetWorkingDirectory kwsys_ns(Process_SetWorkingDirectory) -# define kwsysProcess_SetPipeFile kwsys_ns(Process_SetPipeFile) -# define kwsysProcess_SetPipeNative kwsys_ns(Process_SetPipeNative) -# define kwsysProcess_SetPipeShared kwsys_ns(Process_SetPipeShared) -# define kwsysProcess_Option_Detach kwsys_ns(Process_Option_Detach) -# define kwsysProcess_Option_HideWindow kwsys_ns(Process_Option_HideWindow) -# define kwsysProcess_Option_MergeOutput kwsys_ns(Process_Option_MergeOutput) -# define kwsysProcess_Option_Verbatim kwsys_ns(Process_Option_Verbatim) -# define kwsysProcess_GetOption kwsys_ns(Process_GetOption) -# define kwsysProcess_SetOption kwsys_ns(Process_SetOption) -# define kwsysProcess_Option_e kwsys_ns(Process_Option_e) -# define kwsysProcess_State_Starting kwsys_ns(Process_State_Starting) -# define kwsysProcess_State_Error kwsys_ns(Process_State_Error) -# define kwsysProcess_State_Exception kwsys_ns(Process_State_Exception) -# define kwsysProcess_State_Executing kwsys_ns(Process_State_Executing) -# define kwsysProcess_State_Exited kwsys_ns(Process_State_Exited) -# define kwsysProcess_State_Expired kwsys_ns(Process_State_Expired) -# define kwsysProcess_State_Killed kwsys_ns(Process_State_Killed) -# define kwsysProcess_State_Disowned kwsys_ns(Process_State_Disowned) -# define kwsysProcess_GetState kwsys_ns(Process_GetState) -# define kwsysProcess_State_e kwsys_ns(Process_State_e) -# define kwsysProcess_Exception_None kwsys_ns(Process_Exception_None) -# define kwsysProcess_Exception_Fault kwsys_ns(Process_Exception_Fault) -# define kwsysProcess_Exception_Illegal kwsys_ns(Process_Exception_Illegal) -# define kwsysProcess_Exception_Interrupt kwsys_ns(Process_Exception_Interrupt) -# define kwsysProcess_Exception_Numerical kwsys_ns(Process_Exception_Numerical) -# define kwsysProcess_Exception_Other kwsys_ns(Process_Exception_Other) -# define kwsysProcess_GetExitException kwsys_ns(Process_GetExitException) -# define kwsysProcess_Exception_e kwsys_ns(Process_Exception_e) -# define kwsysProcess_GetExitCode kwsys_ns(Process_GetExitCode) -# define kwsysProcess_GetExitValue kwsys_ns(Process_GetExitValue) -# define kwsysProcess_GetErrorString kwsys_ns(Process_GetErrorString) -# define kwsysProcess_GetExceptionString kwsys_ns(Process_GetExceptionString) -# define kwsysProcess_Execute kwsys_ns(Process_Execute) -# define kwsysProcess_Disown kwsys_ns(Process_Disown) -# define kwsysProcess_WaitForData kwsys_ns(Process_WaitForData) -# define kwsysProcess_Pipes_e kwsys_ns(Process_Pipes_e) -# define kwsysProcess_Pipe_None kwsys_ns(Process_Pipe_None) -# define kwsysProcess_Pipe_STDIN kwsys_ns(Process_Pipe_STDIN) -# define kwsysProcess_Pipe_STDOUT kwsys_ns(Process_Pipe_STDOUT) -# define kwsysProcess_Pipe_STDERR kwsys_ns(Process_Pipe_STDERR) -# define kwsysProcess_Pipe_Timeout kwsys_ns(Process_Pipe_Timeout) -# define kwsysProcess_Pipe_Handle kwsys_ns(Process_Pipe_Handle) -# define kwsysProcess_WaitForExit kwsys_ns(Process_WaitForExit) -# define kwsysProcess_Kill kwsys_ns(Process_Kill) +# define kwsysProcess kwsys_ns(Process) +# define kwsysProcess_s kwsys_ns(Process_s) +# define kwsysProcess_New kwsys_ns(Process_New) +# define kwsysProcess_Delete kwsys_ns(Process_Delete) +# define kwsysProcess_SetCommand kwsys_ns(Process_SetCommand) +# define kwsysProcess_AddCommand kwsys_ns(Process_AddCommand) +# define kwsysProcess_SetTimeout kwsys_ns(Process_SetTimeout) +# define kwsysProcess_SetWorkingDirectory kwsys_ns(Process_SetWorkingDirectory) +# define kwsysProcess_SetPipeFile kwsys_ns(Process_SetPipeFile) +# define kwsysProcess_SetPipeNative kwsys_ns(Process_SetPipeNative) +# define kwsysProcess_SetPipeShared kwsys_ns(Process_SetPipeShared) +# define kwsysProcess_Option_Detach kwsys_ns(Process_Option_Detach) +# define kwsysProcess_Option_HideWindow kwsys_ns(Process_Option_HideWindow) +# define kwsysProcess_Option_MergeOutput kwsys_ns(Process_Option_MergeOutput) +# define kwsysProcess_Option_Verbatim kwsys_ns(Process_Option_Verbatim) +# define kwsysProcess_Option_CreateProcessGroup kwsys_ns(Process_Option_CreateProcessGroup) +# define kwsysProcess_GetOption kwsys_ns(Process_GetOption) +# define kwsysProcess_SetOption kwsys_ns(Process_SetOption) +# define kwsysProcess_Option_e kwsys_ns(Process_Option_e) +# define kwsysProcess_State_Starting kwsys_ns(Process_State_Starting) +# define kwsysProcess_State_Error kwsys_ns(Process_State_Error) +# define kwsysProcess_State_Exception kwsys_ns(Process_State_Exception) +# define kwsysProcess_State_Executing kwsys_ns(Process_State_Executing) +# define kwsysProcess_State_Exited kwsys_ns(Process_State_Exited) +# define kwsysProcess_State_Expired kwsys_ns(Process_State_Expired) +# define kwsysProcess_State_Killed kwsys_ns(Process_State_Killed) +# define kwsysProcess_State_Disowned kwsys_ns(Process_State_Disowned) +# define kwsysProcess_GetState kwsys_ns(Process_GetState) +# define kwsysProcess_State_e kwsys_ns(Process_State_e) +# define kwsysProcess_Exception_None kwsys_ns(Process_Exception_None) +# define kwsysProcess_Exception_Fault kwsys_ns(Process_Exception_Fault) +# define kwsysProcess_Exception_Illegal kwsys_ns(Process_Exception_Illegal) +# define kwsysProcess_Exception_Interrupt kwsys_ns(Process_Exception_Interrupt) +# define kwsysProcess_Exception_Numerical kwsys_ns(Process_Exception_Numerical) +# define kwsysProcess_Exception_Other kwsys_ns(Process_Exception_Other) +# define kwsysProcess_GetExitException kwsys_ns(Process_GetExitException) +# define kwsysProcess_Exception_e kwsys_ns(Process_Exception_e) +# define kwsysProcess_GetExitCode kwsys_ns(Process_GetExitCode) +# define kwsysProcess_GetExitValue kwsys_ns(Process_GetExitValue) +# define kwsysProcess_GetErrorString kwsys_ns(Process_GetErrorString) +# define kwsysProcess_GetExceptionString kwsys_ns(Process_GetExceptionString) +# define kwsysProcess_Execute kwsys_ns(Process_Execute) +# define kwsysProcess_Disown kwsys_ns(Process_Disown) +# define kwsysProcess_WaitForData kwsys_ns(Process_WaitForData) +# define kwsysProcess_Pipes_e kwsys_ns(Process_Pipes_e) +# define kwsysProcess_Pipe_None kwsys_ns(Process_Pipe_None) +# define kwsysProcess_Pipe_STDIN kwsys_ns(Process_Pipe_STDIN) +# define kwsysProcess_Pipe_STDOUT kwsys_ns(Process_Pipe_STDOUT) +# define kwsysProcess_Pipe_STDERR kwsys_ns(Process_Pipe_STDERR) +# define kwsysProcess_Pipe_Timeout kwsys_ns(Process_Pipe_Timeout) +# define kwsysProcess_Pipe_Handle kwsys_ns(Process_Pipe_Handle) +# define kwsysProcess_WaitForExit kwsys_ns(Process_WaitForExit) +# define kwsysProcess_Interrupt kwsys_ns(Process_Interrupt) +# define kwsysProcess_Kill kwsys_ns(Process_Kill) #endif #if defined(__cplusplus) @@ -199,6 +201,15 @@ kwsysEXPORT void kwsysProcess_SetPipeNative(kwsysProcess* cp, int pipe, * and ignore the rest of the arguments. * 0 = No (default) * 1 = Yes + * + * kwsysProcess_Option_CreateProcessGroup = Whether to place the process in a + * new process group. This is + * useful if you want to send Ctrl+C + * to the process. On UNIX, also + * places the process in a new + * session. + * 0 = No (default) + * 1 = Yes */ kwsysEXPORT int kwsysProcess_GetOption(kwsysProcess* cp, int optionId); kwsysEXPORT void kwsysProcess_SetOption(kwsysProcess* cp, int optionId, @@ -208,7 +219,8 @@ enum kwsysProcess_Option_e kwsysProcess_Option_HideWindow, kwsysProcess_Option_Detach, kwsysProcess_Option_MergeOutput, - kwsysProcess_Option_Verbatim + kwsysProcess_Option_Verbatim, + kwsysProcess_Option_CreateProcessGroup }; /** @@ -362,6 +374,17 @@ enum kwsysProcess_Pipes_e */ kwsysEXPORT int kwsysProcess_WaitForExit(kwsysProcess* cp, double* timeout); +/** + * Interrupt the process group for the child process that is currently + * running by sending it the appropriate operating-system specific signal. + * The caller should call WaitForExit after this returns to wait for the + * child to terminate. + * + * WARNING: If you didn't specify kwsysProcess_Option_CreateProcessGroup, + * you will interrupt your own process group. + */ +kwsysEXPORT void kwsysProcess_Interrupt(kwsysProcess* cp); + /** * Forcefully terminate the child process that is currently running. * The caller should call WaitForExit after this returns to wait for @@ -394,6 +417,7 @@ kwsysEXPORT void kwsysProcess_Kill(kwsysProcess* cp); # undef kwsysProcess_Option_HideWindow # undef kwsysProcess_Option_MergeOutput # undef kwsysProcess_Option_Verbatim +# undef kwsysProcess_Option_CreateProcessGroup # undef kwsysProcess_GetOption # undef kwsysProcess_SetOption # undef kwsysProcess_Option_e @@ -430,6 +454,7 @@ kwsysEXPORT void kwsysProcess_Kill(kwsysProcess* cp); # undef kwsysProcess_Pipe_Timeout # undef kwsysProcess_Pipe_Handle # undef kwsysProcess_WaitForExit +# undef kwsysProcess_Interrupt # undef kwsysProcess_Kill # endif #endif diff --git a/ProcessUNIX.c b/ProcessUNIX.c index 8c2b2e4..9b6edfc 100644 --- a/ProcessUNIX.c +++ b/ProcessUNIX.c @@ -151,6 +151,7 @@ typedef struct kwsysProcessCreateInformation_s } kwsysProcessCreateInformation; /*--------------------------------------------------------------------------*/ +static void kwsysProcessVolatileFree(volatile void* p); static int kwsysProcessInitialize(kwsysProcess* cp); static void kwsysProcessCleanup(kwsysProcess* cp, int error); static void kwsysProcessCleanupDescriptor(int* pfd); @@ -197,7 +198,7 @@ struct kwsysProcess_s { /* The command lines to execute. */ char*** Commands; - int NumberOfCommands; + volatile int NumberOfCommands; /* Descriptors for the read ends of the child's output pipes and the signal pipe. */ @@ -213,8 +214,10 @@ struct kwsysProcess_s /* Buffer for pipe data. */ char PipeBuffer[KWSYSPE_PIPE_BUFFER_SIZE]; - /* Process IDs returned by the calls to fork. */ - pid_t* ForkPIDs; + /* Process IDs returned by the calls to fork. Everything is volatile + because the signal handler accesses them. You must be very careful + when reaping PIDs or modifying this array to avoid race conditions. */ + volatile pid_t* volatile ForkPIDs; /* Flag for whether the children were terminated by a faild select. */ int SelectError; @@ -237,6 +240,9 @@ struct kwsysProcess_s /* Whether to merge stdout/stderr of the child. */ int MergeOutput; + /* Whether to create the process in a new process group. */ + volatile sig_atomic_t CreateProcessGroup; + /* Time at which the child started. Negative for no timeout. */ kwsysProcessTime StartTime; @@ -257,8 +263,9 @@ struct kwsysProcess_s /* The number of children still executing. */ int CommandsLeft; - /* The current status of the child process. */ - int State; + /* The current status of the child process. Must be atomic because + the signal handler checks this to avoid a race. */ + volatile sig_atomic_t State; /* The exceptional behavior that terminated the child process, if * any. */ @@ -271,7 +278,7 @@ struct kwsysProcess_s int ExitValue; /* Whether the process was killed. */ - int Killed; + volatile sig_atomic_t Killed; /* Buffer for error message in case of failure. */ char ErrorMessage[KWSYSPE_PIPE_BUFFER_SIZE+1]; @@ -649,6 +656,8 @@ int kwsysProcess_GetOption(kwsysProcess* cp, int optionId) case kwsysProcess_Option_Detach: return cp->OptionDetach; case kwsysProcess_Option_MergeOutput: return cp->MergeOutput; case kwsysProcess_Option_Verbatim: return cp->Verbatim; + case kwsysProcess_Option_CreateProcessGroup: + return cp->CreateProcessGroup; default: return 0; } } @@ -666,6 +675,8 @@ void kwsysProcess_SetOption(kwsysProcess* cp, int optionId, int value) case kwsysProcess_Option_Detach: cp->OptionDetach = value; break; case kwsysProcess_Option_MergeOutput: cp->MergeOutput = value; break; case kwsysProcess_Option_Verbatim: cp->Verbatim = value; break; + case kwsysProcess_Option_CreateProcessGroup: + cp->CreateProcessGroup = value; break; default: break; } } @@ -1489,6 +1500,45 @@ int kwsysProcess_WaitForExit(kwsysProcess* cp, double* userTimeout) return 1; } +/*--------------------------------------------------------------------------*/ +void kwsysProcess_Interrupt(kwsysProcess* cp) +{ + int i; + /* Make sure we are executing a process. */ + if(!cp || cp->State != kwsysProcess_State_Executing || cp->TimeoutExpired || + cp->Killed) + { + return; + } + + /* Interrupt the children. */ + if (cp->CreateProcessGroup) + { + if(cp->ForkPIDs) + { + for(i=0; i < cp->NumberOfCommands; ++i) + { + /* Make sure the PID is still valid. */ + if(cp->ForkPIDs[i]) + { + /* The user created a process group for this process. The group ID + is the process ID for the original process in the group. */ + kill(-cp->ForkPIDs[i], SIGINT); + } + } + } + } + else + { + /* No process group was created. Kill our own process group. + NOTE: While one could argue that we could call kill(cp->ForkPIDs[i], + SIGINT) as a way to still interrupt the process even though it's not in + a special group, this is not an option on Windows. Therefore, we kill + the current process group for consistency with Windows. */ + kill(0, SIGINT); + } +} + /*--------------------------------------------------------------------------*/ void kwsysProcess_Kill(kwsysProcess* cp) { @@ -1538,11 +1588,29 @@ void kwsysProcess_Kill(kwsysProcess* cp) cp->CommandsLeft = 0; } +/*--------------------------------------------------------------------------*/ +/* Call the free() function with a pointer to volatile without causing + compiler warnings. */ +static void kwsysProcessVolatileFree(volatile void* p) +{ + /* clang has made it impossible to free memory that points to volatile + without first using special pragmas to disable a warning... */ +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wcast-qual" +#endif + free((void*)p); /* The cast will silence most compilers, but not clang. */ +#if defined(__clang__) +# pragma clang diagnostic pop +#endif +} + /*--------------------------------------------------------------------------*/ /* Initialize a process control structure for kwsysProcess_Execute. */ static int kwsysProcessInitialize(kwsysProcess* cp) { int i; + volatile pid_t* oldForkPIDs; for(i=0; i < KWSYSPE_PIPE_COUNT; ++i) { cp->PipeReadEnds[i] = -1; @@ -1571,16 +1639,21 @@ static int kwsysProcessInitialize(kwsysProcess* cp) cp->ErrorMessage[0] = 0; strcpy(cp->ExitExceptionString, "No exception"); - if(cp->ForkPIDs) + oldForkPIDs = cp->ForkPIDs; + cp->ForkPIDs = (volatile pid_t*)malloc( + sizeof(volatile pid_t)*(size_t)(cp->NumberOfCommands)); + if(oldForkPIDs) { - free(cp->ForkPIDs); + kwsysProcessVolatileFree(oldForkPIDs); } - cp->ForkPIDs = (pid_t*)malloc(sizeof(pid_t)*(size_t)(cp->NumberOfCommands)); if(!cp->ForkPIDs) { return 0; } - memset(cp->ForkPIDs, 0, sizeof(pid_t)*(size_t)(cp->NumberOfCommands)); + for(i=0; i < cp->NumberOfCommands; ++i) + { + cp->ForkPIDs[i] = 0; /* can't use memset due to volatile */ + } if(cp->CommandExitCodes) { @@ -1671,7 +1744,7 @@ static void kwsysProcessCleanup(kwsysProcess* cp, int error) /* Free memory. */ if(cp->ForkPIDs) { - free(cp->ForkPIDs); + kwsysProcessVolatileFree(cp->ForkPIDs); cp->ForkPIDs = 0; } if(cp->RealWorkingDirectory) @@ -1758,15 +1831,49 @@ int decc$set_child_standard_streams(int fd1, int fd2, int fd3); static int kwsysProcessCreate(kwsysProcess* cp, int prIndex, kwsysProcessCreateInformation* si) { + sigset_t mask, old_mask; + int pgidPipe[2]; + char tmp; + ssize_t readRes; + /* Create the error reporting pipe. */ if(pipe(si->ErrorPipe) < 0) { return 0; } - /* Set close-on-exec flag on the error pipe's write end. */ - if(fcntl(si->ErrorPipe[1], F_SETFD, FD_CLOEXEC) < 0) + /* Create a pipe for detecting that the child process has created a process + group and session. */ + if(pipe(pgidPipe) < 0) { + kwsysProcessCleanupDescriptor(&si->ErrorPipe[0]); + kwsysProcessCleanupDescriptor(&si->ErrorPipe[1]); + return 0; + } + + /* Set close-on-exec flag on the pipe's write end. */ + if(fcntl(si->ErrorPipe[1], F_SETFD, FD_CLOEXEC) < 0 || + fcntl(pgidPipe[1], F_SETFD, FD_CLOEXEC) < 0) + { + kwsysProcessCleanupDescriptor(&si->ErrorPipe[0]); + kwsysProcessCleanupDescriptor(&si->ErrorPipe[1]); + kwsysProcessCleanupDescriptor(&pgidPipe[0]); + kwsysProcessCleanupDescriptor(&pgidPipe[1]); + return 0; + } + + /* Block SIGINT / SIGTERM while we start. The purpose is so that our signal + handler doesn't get called from the child process after the fork and + before the exec, and subsequently start kill()'ing PIDs from ForkPIDs. */ + sigemptyset(&mask); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGTERM); + if(sigprocmask(SIG_BLOCK, &mask, &old_mask) < 0) + { + kwsysProcessCleanupDescriptor(&si->ErrorPipe[0]); + kwsysProcessCleanupDescriptor(&si->ErrorPipe[1]); + kwsysProcessCleanupDescriptor(&pgidPipe[0]); + kwsysProcessCleanupDescriptor(&pgidPipe[1]); return 0; } @@ -1774,13 +1881,19 @@ static int kwsysProcessCreate(kwsysProcess* cp, int prIndex, #if defined(__VMS) /* VMS needs vfork and execvp to be in the same function because they use setjmp/longjmp to run the child startup code in the - parent! TODO: OptionDetach. */ + parent! TODO: OptionDetach. Also + TODO: CreateProcessGroup. */ cp->ForkPIDs[prIndex] = vfork(); #else cp->ForkPIDs[prIndex] = kwsysProcessFork(cp, si); #endif if(cp->ForkPIDs[prIndex] < 0) { + sigprocmask(SIG_SETMASK, &old_mask, 0); + kwsysProcessCleanupDescriptor(&si->ErrorPipe[0]); + kwsysProcessCleanupDescriptor(&si->ErrorPipe[1]); + kwsysProcessCleanupDescriptor(&pgidPipe[0]); + kwsysProcessCleanupDescriptor(&pgidPipe[1]); return 0; } @@ -1790,8 +1903,10 @@ static int kwsysProcessCreate(kwsysProcess* cp, int prIndex, /* Specify standard pipes for child process. */ decc$set_child_standard_streams(si->StdIn, si->StdOut, si->StdErr); #else - /* Close the read end of the error reporting pipe. */ + /* Close the read end of the error reporting / process group + setup pipe. */ close(si->ErrorPipe[0]); + close(pgidPipe[0]); /* Setup the stdin, stdout, and stderr pipes. */ if(si->StdIn > 0) @@ -1819,11 +1934,25 @@ static int kwsysProcessCreate(kwsysProcess* cp, int prIndex, /* Restore all default signal handlers. */ kwsysProcessRestoreDefaultSignalHandlers(); + + /* Now that we have restored default signal handling and created the + process group, restore mask. */ + sigprocmask(SIG_SETMASK, &old_mask, 0); + + /* Create new process group. We use setsid instead of setpgid to avoid + the child getting hung up on signals like SIGTTOU. (In the real world, + this has been observed where "git svn" ends up calling the "resize" + program which opens /dev/tty. */ + if(cp->CreateProcessGroup && setsid() < 0) + { + kwsysProcessChildErrorExit(si->ErrorPipe[1]); + } #endif /* Execute the real process. If successful, this does not return. */ execvp(cp->Commands[prIndex][0], cp->Commands[prIndex]); /* TODO: What does VMS do if the child fails to start? */ + /* TODO: On VMS, how do we put the process in a new group? */ /* Failure. Report error to parent and terminate. */ kwsysProcessChildErrorExit(si->ErrorPipe[1]); @@ -1834,12 +1963,33 @@ static int kwsysProcessCreate(kwsysProcess* cp, int prIndex, decc$set_child_standard_streams(0, 1, 2); #endif + /* We are done with the error reporting pipe and process group setup pipe + write end. */ + kwsysProcessCleanupDescriptor(&si->ErrorPipe[1]); + kwsysProcessCleanupDescriptor(&pgidPipe[1]); + + /* Make sure the child is in the process group before we proceed. This + avoids race conditions with calls to the kill function that we make for + signalling process groups. */ + while((readRes = read(pgidPipe[0], &tmp, 1)) > 0); + if(readRes < 0) + { + sigprocmask(SIG_SETMASK, &old_mask, 0); + kwsysProcessCleanupDescriptor(&si->ErrorPipe[0]); + kwsysProcessCleanupDescriptor(&pgidPipe[0]); + return 0; + } + + /* Unmask signals. */ + if(sigprocmask(SIG_SETMASK, &old_mask, 0) < 0) + { + kwsysProcessCleanupDescriptor(&si->ErrorPipe[0]); + return 0; + } + /* A child has been created. */ ++cp->CommandsLeft; - /* We are done with the error reporting pipe write end. */ - kwsysProcessCleanupDescriptor(&si->ErrorPipe[1]); - /* Block until the child's exec call succeeds and closes the error pipe or writes data to the pipe to report an error. */ { @@ -1877,6 +2027,17 @@ static void kwsysProcessDestroy(kwsysProcess* cp) /* A child process has terminated. Reap it if it is one handled by this object. */ int i; + /* Temporarily disable signals that access ForkPIDs. We don't want them to + read a reaped PID, and writes to ForkPIDs are not atomic. */ + sigset_t mask, old_mask; + sigemptyset(&mask); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGTERM); + if(sigprocmask(SIG_BLOCK, &mask, &old_mask) < 0) + { + return; + } + for(i=0; i < cp->NumberOfCommands; ++i) { if(cp->ForkPIDs[i]) @@ -1910,6 +2071,9 @@ static void kwsysProcessDestroy(kwsysProcess* cp) } } } + + /* Re-enable signals. */ + sigprocmask(SIG_SETMASK, &old_mask, 0); } /*--------------------------------------------------------------------------*/ @@ -2590,13 +2754,15 @@ static struct sigaction kwsysProcessesOldSigTermAction; /*--------------------------------------------------------------------------*/ static void kwsysProcessesUpdate(kwsysProcessInstances* newProcesses) { - /* Block SIGCHLD while we update the set of pipes to check. + /* Block signals while we update the set of pipes to check. TODO: sigprocmask is undefined for threaded apps. See pthread_sigmask. */ sigset_t newset; sigset_t oldset; sigemptyset(&newset); sigaddset(&newset, SIGCHLD); + sigaddset(&newset, SIGINT); + sigaddset(&newset, SIGTERM); sigprocmask(SIG_BLOCK, &newset, &oldset); /* Store the new set in that seen by the signal handler. */ @@ -2784,7 +2950,7 @@ static void kwsysProcessesSignalHandler(int signum #endif ) { - int i, procStatus, old_errno = errno; + int i, j, procStatus, old_errno = errno; #if KWSYSPE_USE_SIGINFO (void)info; (void)ucontext; @@ -2808,10 +2974,34 @@ static void kwsysProcessesSignalHandler(int signum break; case SIGINT: case SIGTERM: + /* Signal child processes that are running in new process groups. */ + for(i=0; i < kwsysProcesses.Count; ++i) + { + kwsysProcess* cp = kwsysProcesses.Processes[i]; + /* Check Killed to avoid data race condition when killing. + Check State to avoid data race condition in kwsysProcessCleanup + when there is an error (it leaves a reaped PID). */ + if(cp->CreateProcessGroup && !cp->Killed && + cp->State != kwsysProcess_State_Error && cp->ForkPIDs) + { + for(j=0; j < cp->NumberOfCommands; ++j) + { + /* Make sure the PID is still valid. */ + if(cp->ForkPIDs[j]) + { + /* The user created a process group for this process. The group ID + is the process ID for the original process in the group. */ + kill(-cp->ForkPIDs[j], SIGINT); + } + } + } + } + /* Wait for all processes to terminate. */ while(wait(&procStatus) >= 0 || errno != ECHILD) { } + /* Terminate the process, which is now in an inconsistent state because we reaped all the PIDs that it may have been reaping or may have reaped in the future. Reraise the signal so that diff --git a/ProcessWin32.c b/ProcessWin32.c index 086fc39..ba3397c 100644 --- a/ProcessWin32.c +++ b/ProcessWin32.c @@ -136,7 +136,8 @@ static void kwsysProcessDisablePipeThreads(kwsysProcess* cp); static int kwsysProcessesInitialize(void); static int kwsysTryEnterCreateProcessSection(void); static void kwsysLeaveCreateProcessSection(void); -static int kwsysProcessesAdd(HANDLE hProcess); +static int kwsysProcessesAdd(HANDLE hProcess, DWORD dwProcessId, + int newProcessGroup); static void kwsysProcessesRemove(HANDLE hProcess); static BOOL WINAPI kwsysCtrlHandler(DWORD dwCtrlType); @@ -228,6 +229,9 @@ struct kwsysProcess_s /* Whether to merge stdout/stderr of the child. */ int MergeOutput; + /* Whether to create the process in a new process group. */ + int CreateProcessGroup; + /* Mutex to protect the shared index used by threads to report data. */ HANDLE SharedIndexMutex; @@ -852,6 +856,8 @@ int kwsysProcess_GetOption(kwsysProcess* cp, int optionId) case kwsysProcess_Option_HideWindow: return cp->HideWindow; case kwsysProcess_Option_MergeOutput: return cp->MergeOutput; case kwsysProcess_Option_Verbatim: return cp->Verbatim; + case kwsysProcess_Option_CreateProcessGroup: + return cp->CreateProcessGroup; default: return 0; } } @@ -870,6 +876,8 @@ void kwsysProcess_SetOption(kwsysProcess* cp, int optionId, int value) case kwsysProcess_Option_HideWindow: cp->HideWindow = value; break; case kwsysProcess_Option_MergeOutput: cp->MergeOutput = value; break; case kwsysProcess_Option_Verbatim: cp->Verbatim = value; break; + case kwsysProcess_Option_CreateProcessGroup: + cp->CreateProcessGroup = value; break; default: break; } } @@ -1475,6 +1483,52 @@ int kwsysProcess_WaitForExit(kwsysProcess* cp, double* userTimeout) return 1; } +/*--------------------------------------------------------------------------*/ +void kwsysProcess_Interrupt(kwsysProcess* cp) +{ + int i; + /* Make sure we are executing a process. */ + if(!cp || cp->State != kwsysProcess_State_Executing || cp->TimeoutExpired || + cp->Killed) + { + KWSYSPE_DEBUG((stderr, "interrupt: child not executing\n")); + return; + } + + /* Skip actually interrupting the child if it has already terminated. */ + if(cp->Terminated) + { + KWSYSPE_DEBUG((stderr, "interrupt: child already terminated\n")); + return; + } + + /* Interrupt the children. */ + if (cp->CreateProcessGroup) + { + if(cp->ProcessInformation) + { + for(i=0; i < cp->NumberOfCommands; ++i) + { + /* Make sure the process handle isn't closed (e.g. from disowning). */ + if(cp->ProcessInformation[i].hProcess) + { + /* The user created a process group for this process. The group ID + is the process ID for the original process in the group. Note + that we have to use Ctrl+Break: Ctrl+C is not allowed for process + groups. */ + GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, + cp->ProcessInformation[i].dwProcessId); + } + } + } + } + else + { + /* No process group was created. Kill our own process group... */ + GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, 0); + } +} + /*--------------------------------------------------------------------------*/ void kwsysProcess_Kill(kwsysProcess* cp) { @@ -1745,6 +1799,7 @@ static int kwsysProcessCreateChildHandle(PHANDLE out, HANDLE in, int isStdIn) int kwsysProcessCreate(kwsysProcess* cp, int index, kwsysProcessCreateInformation* si) { + DWORD creationFlags; int res; /* Check if we are currently exiting. */ @@ -1757,6 +1812,14 @@ int kwsysProcessCreate(kwsysProcess* cp, int index, Sleep(INFINITE); } + /* Create the child in a suspended state so we can wait until all + children have been created before running any one. */ + creationFlags = CREATE_SUSPENDED; + if (cp->CreateProcessGroup) + { + creationFlags |= CREATE_NEW_PROCESS_GROUP; + } + res = /* Create inherited copies the handles. */ @@ -1767,9 +1830,7 @@ int kwsysProcessCreate(kwsysProcess* cp, int index, kwsysProcessCreateChildHandle(&si->StartupInfo.hStdError, si->hStdError, 0) && - /* Create the child in a suspended state so we can wait until all - children have been created before running any one. */ - CreateProcessW(0, cp->Commands[index], 0, 0, TRUE, CREATE_SUSPENDED, 0, + CreateProcessW(0, cp->Commands[index], 0, 0, TRUE, creationFlags, 0, 0, &si->StartupInfo, &cp->ProcessInformation[index]); /* Close the inherited copies of the handles. */ @@ -1787,7 +1848,8 @@ int kwsysProcessCreate(kwsysProcess* cp, int index, } /* Add the process to the global list of processes. */ - if (!kwsysProcessesAdd(cp->ProcessInformation[index].hProcess)) + if (!kwsysProcessesAdd(cp->ProcessInformation[index].hProcess, + cp->ProcessInformation[index].dwProcessId, cp->CreateProcessGroup)) { /* This failed for some reason. Kill the suspended process. */ TerminateProcess(cp->ProcessInformation[index].hProcess, 1); @@ -2715,6 +2777,13 @@ static void kwsysProcessDisablePipeThreads(kwsysProcess* cp) everything it does must be thread safe. Here, we track the hProcess HANDLEs directly instead of kwsysProcess instances, so that we don't have to make kwsysProcess thread safe. */ +typedef struct kwsysProcessInstance_s +{ + HANDLE hProcess; + DWORD dwProcessId; + int NewProcessGroup; /* Whether the process was created in a new group. */ +} kwsysProcessInstance; + typedef struct kwsysProcessInstances_s { /* Whether we have initialized key fields below, like critical sections. */ @@ -2726,7 +2795,7 @@ typedef struct kwsysProcessInstances_s int Exiting; size_t Count; size_t Size; - HANDLE* Processes; + kwsysProcessInstance* Processes; } kwsysProcessInstances; static kwsysProcessInstances kwsysProcesses; @@ -2784,8 +2853,10 @@ static void kwsysLeaveCreateProcessSection(void) /*--------------------------------------------------------------------------*/ /* Add new process to global process list. The Ctrl handler will wait for the process to exit before it returns. Do not close the process handle - until after calling kwsysProcessesRemove. */ -static int kwsysProcessesAdd(HANDLE hProcess) + until after calling kwsysProcessesRemove. The newProcessGroup parameter + must be set if the process was created with CREATE_NEW_PROCESS_GROUP. */ +static int kwsysProcessesAdd(HANDLE hProcess, DWORD dwProcessid, + int newProcessGroup) { if(!kwsysProcessesInitialize() || !hProcess || hProcess == INVALID_HANDLE_VALUE) @@ -2800,19 +2871,20 @@ static int kwsysProcessesAdd(HANDLE hProcess) if(kwsysProcesses.Count == kwsysProcesses.Size) { size_t newSize; - HANDLE *newArray; + kwsysProcessInstance *newArray; /* Start with enough space for a small number of process handles and double the size each time more is needed. */ newSize = kwsysProcesses.Size? kwsysProcesses.Size*2 : 4; /* Try allocating the new block of memory. */ - if(newArray = (HANDLE*)malloc(newSize*sizeof(HANDLE))) + if(newArray = (kwsysProcessInstance*)malloc( + newSize*sizeof(kwsysProcessInstance))) { /* Copy the old process handles to the new memory. */ if(kwsysProcesses.Count > 0) { memcpy(newArray, kwsysProcesses.Processes, - kwsysProcesses.Count * sizeof(HANDLE)); + kwsysProcesses.Count * sizeof(kwsysProcessInstance)); } } else @@ -2830,8 +2902,11 @@ static int kwsysProcessesAdd(HANDLE hProcess) kwsysProcesses.Processes = newArray; } - /* Append the new process handle to the set. */ - kwsysProcesses.Processes[kwsysProcesses.Count++] = hProcess; + /* Append the new process information to the set. */ + kwsysProcesses.Processes[kwsysProcesses.Count].hProcess = hProcess; + kwsysProcesses.Processes[kwsysProcesses.Count].dwProcessId = dwProcessid; + kwsysProcesses.Processes[kwsysProcesses.Count++].NewProcessGroup = + newProcessGroup; /* Leave critical section and return success. */ LeaveCriticalSection(&kwsysProcesses.Lock); @@ -2855,7 +2930,7 @@ static void kwsysProcessesRemove(HANDLE hProcess) /* Find the given process in the set. */ for(i=0; i < kwsysProcesses.Count; ++i) { - if(kwsysProcesses.Processes[i] == hProcess) + if(kwsysProcesses.Processes[i].hProcess == hProcess) { break; } @@ -2884,6 +2959,7 @@ static void kwsysProcessesRemove(HANDLE hProcess) /*--------------------------------------------------------------------------*/ static BOOL WINAPI kwsysCtrlHandler(DWORD dwCtrlType) { + size_t i; (void)dwCtrlType; /* Enter critical section. */ EnterCriticalSection(&kwsysProcesses.Lock); @@ -2891,13 +2967,26 @@ static BOOL WINAPI kwsysCtrlHandler(DWORD dwCtrlType) /* Set flag indicating that we are exiting. */ kwsysProcesses.Exiting = 1; + /* If some of our processes were created in a new process group, we must + manually interrupt them. They won't otherwise receive a Ctrl+C/Break. */ + for(i=0; i < kwsysProcesses.Count; ++i) + { + if(kwsysProcesses.Processes[i].NewProcessGroup) + { + DWORD groupId = kwsysProcesses.Processes[i].dwProcessId; + if(groupId) + { + GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, groupId); + } + } + } + /* Wait for each child process to exit. This is the key step that prevents us from leaving several orphaned children processes running in the background when the user presses Ctrl+C. */ - if(kwsysProcesses.Count > 0) /* WaitForMultipleObjects forbids Count==0. */ + for(i=0; i < kwsysProcesses.Count; ++i) { - WaitForMultipleObjects((DWORD)kwsysProcesses.Count, - kwsysProcesses.Processes, TRUE, INFINITE); + WaitForSingleObject(kwsysProcesses.Processes[i].hProcess, INFINITE); } /* Leave critical section. */ -- GitLab