ncsubproc: properly catch exit on non-pidfd path

When we don't have a pidfd available on which to poll(2) (this
is true of Linux pre-5.3, and FreeBSD), we can't rely on a
child death breaking our poll loop. Instead, in this case launch
a second thread, which just sits on a blocking waitpid(2). If
it gets an exit, it calls the completion callback, triggering
the teardown. Closes #728, and ought lets us run the test suite
on FreeBSD.
This commit is contained in:
nick black 2020-06-28 05:56:38 -04:00
parent f927af00f2
commit 5815a65fb5
No known key found for this signature in database
GPG Key ID: 5F43400C21CBFACC
3 changed files with 66 additions and 19 deletions

View File

@ -28,7 +28,7 @@ fdthread(ncfdplane* ncfp, int pidfd){
char* buf = malloc(BUFSIZ); char* buf = malloc(BUFSIZ);
int pevents; int pevents;
pfds[0].fd = ncfp->fd; pfds[0].fd = ncfp->fd;
pfds[0].events = POLLIN; pfds[0].events = POLLIN | POLLRDHUP;
const int fdcount = pidfd < 0 ? 1 : 2; const int fdcount = pidfd < 0 ? 1 : 2;
if(fdcount > 1){ if(fdcount > 1){
pfds[1].fd = pidfd; pfds[1].fd = pidfd;
@ -143,7 +143,6 @@ int ncfdplane_destroy(ncfdplane* n){
// descriptors 1 and 2, exec()s, and begins running. the parent creates an // descriptors 1 and 2, exec()s, and begins running. the parent creates an
// ncfdplane around the read end, involving creation of a new thread. the // ncfdplane around the read end, involving creation of a new thread. the
// parent then returns. // parent then returns.
static pid_t static pid_t
launch_pipe_process(int* pipe, int* pidfd){ launch_pipe_process(int* pipe, int* pidfd){
*pidfd = -1; *pidfd = -1;
@ -154,8 +153,8 @@ launch_pipe_process(int* pipe, int* pidfd){
pid_t p = -1; pid_t p = -1;
// on linux, we try to use the brand-new pidfd capability via clone3(). if // on linux, we try to use the brand-new pidfd capability via clone3(). if
// that fails, fall through to fork(), which is all we try to use on freebsd. // that fails, fall through to fork(), which is all we try to use on freebsd.
// FIXME clone3 is not yet supported on debian sparc64, as of 2020-06 anyway // FIXME clone3 is not yet supported on debian sparc64/alpha as of 2020-07
#if (defined(__linux__) && !defined(__sparc__)) #if (defined(__linux__) && !defined(__sparc__) && !defined(__alpha__))
// FIXME introduced in linux 5.5 // FIXME introduced in linux 5.5
#ifndef CLONE_CLEAR_SIGHAND #ifndef CLONE_CLEAR_SIGHAND
#define CLONE_CLEAR_SIGHAND 0x100000000ULL #define CLONE_CLEAR_SIGHAND 0x100000000ULL
@ -184,7 +183,6 @@ launch_pipe_process(int* pipe, int* pidfd){
// nuke the just-spawned process, and reap it. called before the subprocess // nuke the just-spawned process, and reap it. called before the subprocess
// reader thread is launched (which otherwise reaps the subprocess). // reader thread is launched (which otherwise reaps the subprocess).
// FIXME rigourize and port this
static int static int
kill_and_wait_subproc(pid_t pid, int pidfd, int* status){ kill_and_wait_subproc(pid_t pid, int pidfd, int* status){
int ret = -1; int ret = -1;
@ -227,6 +225,30 @@ ncsubproc_thread(void* vncsp){
return status; return status;
} }
// this is only used if we don't have a pidfd available for poll()ing. in that
// case, we want to perform a blocking waitpid() on the pid, calling the
// completion callback when it exits (since the process exit won't necessarily
// wake up our poll()ing thread).
static void *
ncsubproc_waiter(void* vncsp){
ncsubproc* ncsp = vncsp;
int* status = malloc(sizeof(*status));
pid_t pid;
while((pid = waitpid(ncsp->pid, status, 0)) < 0 && errno == EINTR){
;
}
if(pid != ncsp->pid){
return NULL;
}
pthread_mutex_lock(&ncsp->lock);
ncsp->waited = true;
pthread_mutex_unlock(&ncsp->lock);
if(!ncsp->nfp->destroyed){
ncsp->nfp->donecb(ncsp->nfp, *status, ncsp->nfp->curry);
}
return status;
}
static ncfdplane* static ncfdplane*
ncsubproc_launch(ncplane* n, ncsubproc* ret, const ncsubproc_options* opts, int fd, ncsubproc_launch(ncplane* n, ncsubproc* ret, const ncsubproc_options* opts, int fd,
ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn){ ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn){
@ -242,6 +264,15 @@ ncsubproc_launch(ncplane* n, ncsubproc* ret, const ncsubproc_options* opts, int
ncfdplane_destroy_inner(ret->nfp); ncfdplane_destroy_inner(ret->nfp);
ret->nfp = NULL; ret->nfp = NULL;
} }
if(ret->pidfd < 0){
// if we don't have a pidfd to throw into our poll(), we need spin up a
// thread to call waitpid() on our pid
if(pthread_create(&ret->waittid, NULL, ncsubproc_waiter, ret)){
// FIXME cancel and join thread
ncfdplane_destroy_inner(ret->nfp);
ret->nfp = NULL;
}
}
return ret->nfp; return ret->nfp;
} }
@ -260,8 +291,6 @@ ncsubproc* ncsubproc_createv(ncplane* n, const ncsubproc_options* opts,
ret->pid = launch_pipe_process(&fd, &ret->pidfd); ret->pid = launch_pipe_process(&fd, &ret->pidfd);
if(ret->pid == 0){ if(ret->pid == 0){
execv(bin, arg); execv(bin, arg);
//fprintf(stderr, "Error execv()ing %s\n", bin);
raise(SIGKILL);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
}else if(ret->pid < 0){ }else if(ret->pid < 0){
free(ret); free(ret);
@ -291,7 +320,6 @@ ncsubproc* ncsubproc_createvp(ncplane* n, const ncsubproc_options* opts,
if(ret->pid == 0){ if(ret->pid == 0){
execvp(bin, arg); execvp(bin, arg);
//fprintf(stderr, "Error execv()ing %s\n", bin); //fprintf(stderr, "Error execv()ing %s\n", bin);
raise(SIGKILL);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
}else if(ret->pid < 0){ }else if(ret->pid < 0){
free(ret); free(ret);
@ -325,12 +353,12 @@ ncsubproc* ncsubproc_createvpe(ncplane* n, const ncsubproc_options* opts,
execvpe(bin, arg, env); execvpe(bin, arg, env);
#endif #endif
//fprintf(stderr, "Error execv()ing %s\n", bin); //fprintf(stderr, "Error execv()ing %s\n", bin);
raise(SIGKILL);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
}else if(ret->pid < 0){ }else if(ret->pid < 0){
free(ret); free(ret);
return NULL; return NULL;
} }
pthread_mutex_init(&ret->lock, NULL);
if((ret->nfp = ncsubproc_launch(n, ret, opts, fd, cbfxn, donecbfxn)) == NULL){ if((ret->nfp = ncsubproc_launch(n, ret, opts, fd, cbfxn, donecbfxn)) == NULL){
kill_and_wait_subproc(ret->pid, ret->pidfd, NULL); kill_and_wait_subproc(ret->pid, ret->pidfd, NULL);
free(ret); free(ret);
@ -339,23 +367,38 @@ ncsubproc* ncsubproc_createvpe(ncplane* n, const ncsubproc_options* opts,
return ret; return ret;
} }
// FIXME rigourize and port
int ncsubproc_destroy(ncsubproc* n){ int ncsubproc_destroy(ncsubproc* n){
int ret = 0; int ret = 0;
if(n){ if(n){
void* vret = NULL; void* vret = NULL;
#ifdef __linux__ #ifdef __linux__
if(n->pidfd >= 0){
syscall(__NR_pidfd_send_signal, n->pidfd, SIGKILL, NULL, 0); syscall(__NR_pidfd_send_signal, n->pidfd, SIGKILL, NULL, 0);
}
#else #else
pthread_mutex_lock(&n->lock);
if(!n->waited){
kill(n->pid, SIGKILL); kill(n->pid, SIGKILL);
}
pthread_mutex_unlock(&n->lock);
#endif #endif
// the thread waits on the subprocess via pidfd, and then exits. don't try // the thread waits on the subprocess via pidfd (iff pidfd >= 0), and
// to cancel the thread; rely on killing the subprocess. // then exits. don't try to cancel the thread in that case; rely instead on
// killing the subprocess.
if(n->pidfd < 0){
pthread_cancel(n->nfp->tid);
// shouldn't need a cancellation of waittid thanks to SIGKILL
pthread_join(n->waittid, &vret);
}
if(vret == NULL){
pthread_join(n->nfp->tid, &vret); pthread_join(n->nfp->tid, &vret);
}else{
pthread_join(n->nfp->tid, NULL);
}
free(n); free(n);
if(vret == NULL){ if(vret == NULL){
ret = -1; ret = -1;
}else{ }else if(vret != PTHREAD_CANCELED){
ret = *(int*)vret; ret = *(int*)vret;
free(vret); free(vret);
} }

View File

@ -150,6 +150,9 @@ typedef struct ncsubproc {
ncfdplane* nfp; ncfdplane* nfp;
pid_t pid; // subprocess pid_t pid; // subprocess
int pidfd; // for signalling/watching the subprocess int pidfd; // for signalling/watching the subprocess
pthread_t waittid; // wait()ing thread if pidfd is not available
pthread_mutex_t lock; // guards waited
bool waited; // we've wait()ed on it, don't kill/wait further
} ncsubproc; } ncsubproc;
typedef struct ncreader { typedef struct ncreader {

View File

@ -26,6 +26,7 @@ build() {
-DBUILD_TESTING=off \ -DBUILD_TESTING=off \
-DUSE_PANDOC=off \ -DUSE_PANDOC=off \
-DUSE_QRCODEGEN=off \ -DUSE_QRCODEGEN=off \
-DUSE_PYTHON=off \
${CMAKE_CROSSOPTS} ${CMAKE_CROSSOPTS}
make -C "build" make -C "build"
} }