You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
notcurses/src/lib/fd.c

486 lines
13 KiB
C

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include "internal.h"
#ifdef USING_PIDFD
#error "USING_PIDFD was already defined; it should not be."
#endif
#ifdef __MINGW32__
#include <winsock2.h>
#else
#include <spawn.h>
#endif
#if (defined(__linux__))
#include <linux/wait.h>
#include <asm/unistd.h>
#include <linux/sched.h>
#define NCPOLLEVENTS (POLLIN | POLLRDHUP)
#if (defined(__NR_clone3) && defined(P_PIDFD) && defined(CLONE_CLEAR_SIGHAND))
#define USING_PIDFD
#endif
#else
#define NCPOLLEVENTS (POLLIN)
#endif
// release the memory and fd, but don't join the thread (since we might be
// getting called within the thread's context, on a callback).
static int
ncfdplane_destroy_inner(ncfdplane* n){
int ret = close(n->fd);
free(n);
return ret;
}
// if pidfd is < 0, it won't be used in the poll()
static void
fdthread(ncfdplane* ncfp, int pidfd){
struct pollfd pfds[2];
memset(pfds, 0, sizeof(pfds));
char* buf = malloc(BUFSIZ + 1);
pfds[0].fd = ncfp->fd;
pfds[0].events = NCPOLLEVENTS;
const int fdcount = pidfd < 0 ? 1 : 2;
if(fdcount > 1){
pfds[1].fd = pidfd;
pfds[1].events = NCPOLLEVENTS;
}
ssize_t r = 0;
#ifndef __MINGW32__
while(poll(pfds, fdcount, -1) >= 0 || errno == EINTR){
#else
while(WSAPoll(pfds, fdcount, -1) >= 0){
#endif
if(pfds[0].revents){
while((r = read(ncfp->fd, buf, BUFSIZ)) >= 0){
if(r == 0){
break;
}
buf[r] = '\0';
if( (r = ncfp->cb(ncfp, buf, r, ncfp->curry)) ){
break;
}
if(ncfp->destroyed){
break;
}
}
// if we're not doing follow, break out on a zero-byte read
if(r == 0 && !ncfp->follow){
break;
}
}
if(fdcount > 1 && pfds[1].revents){
r = 0;
break;
}
}
if(r <= 0 && !ncfp->destroyed){
ncfp->donecb(ncfp, r == 0 ? 0 : errno, ncfp->curry);
}
if(ncfp->destroyed){
ncfdplane_destroy_inner(ncfp);
}
free(buf);
}
static void *
ncfdplane_thread(void* vncfp){
fdthread(vncfp, -1);
return NULL;
}
static ncfdplane*
ncfdplane_create_internal(ncplane* n, const ncfdplane_options* opts, int fd,
ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn,
bool thread){
if(opts->flags > 0){
logwarn("Provided unsupported flags %016" PRIx64 "\n", opts->flags);
}
ncfdplane* ret = malloc(sizeof(*ret));
if(ret == NULL){
return ret;
}
ret->cb = cbfxn;
ret->donecb = donecbfxn;
ret->follow = opts->follow;
ret->ncp = n;
ret->destroyed = false;
ncplane_set_scrolling(ret->ncp, true);
ret->fd = fd;
ret->curry = opts->curry;
if(thread){
if(pthread_create(&ret->tid, NULL, ncfdplane_thread, ret)){
free(ret);
return NULL;
}
}
return ret;
}
ncfdplane* ncfdplane_create(ncplane* n, const ncfdplane_options* opts, int fd,
ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn){
ncfdplane_options zeroed = {0};
if(!opts){
opts = &zeroed;
}
if(fd < 0 || !cbfxn || !donecbfxn){
return NULL;
}
return ncfdplane_create_internal(n, opts, fd, cbfxn, donecbfxn, true);
}
ncplane* ncfdplane_plane(ncfdplane* n){
return n->ncp;
}
int ncfdplane_destroy(ncfdplane* n){
int ret = 0;
if(n){
if(pthread_equal(pthread_self(), n->tid)){
n->destroyed = true; // ncfdplane_destroy_inner() is called on thread exit
}else{
void* vret = NULL;
ret |= cancel_and_join("fdplane", n->tid, &vret);
ret |= ncfdplane_destroy_inner(n);
}
}
return ret;
}
#ifndef __MINGW32__
// get 2 pipes, and ensure they're both set to close-on-exec
static int
lay_pipes(int pipes[static 2]){
#ifdef __linux__
if(pipe2(pipes, O_CLOEXEC)){ // can't use O_NBLOCK here (affects client)
#else
if(pipe(pipes)){
#endif
return -1;
}
#ifndef __linux__
if(set_fd_cloexec(pipes[0], 1, NULL) || set_fd_cloexec(pipes[1], 1, NULL)){
close(pipes[0]);
close(pipes[1]);
return -1;
}
#endif
return 0;
}
// ncsubproc creates a pipe, retaining the read end. it clone()s a subprocess,
// getting a pidfd. the subprocess dup2()s the write end of the pipe onto file
// 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
// parent then returns.
static pid_t
launch_pipe_process(int* pipefd, int* pidfd, unsigned usepath,
const char* bin, char* const arg[], char* const env[]){
*pidfd = -1;
int pipes[2];
if(lay_pipes(pipes)){
return -1;
}
pid_t p = -1;
#ifdef USING_PIDFD
// on linux, we try to use the brand-new pidfd capability via clone3(). if
// that fails, fall through to posix_spawn(), our only option on freebsd.
// FIXME clone3 is not yet supported on debian sparc64/alpha as of 2020-07
struct clone_args clargs;
memset(&clargs, 0, sizeof(clargs));
clargs.pidfd = (uintptr_t)pidfd;
clargs.flags = CLONE_CLEAR_SIGHAND | CLONE_FS | CLONE_PIDFD;
clargs.exit_signal = SIGCHLD;
p = syscall(__NR_clone3, &clargs, sizeof(clargs));
if(p == 0){ // child
if(dup2(pipes[1], STDOUT_FILENO) < 0 || dup2(pipes[1], STDERR_FILENO) < 0){
logerror("Couldn't dup() %d (%s)\n", pipes[1], strerror(errno));
exit(EXIT_FAILURE);
}
if(env){
execvpe(bin, arg, env);
}else if(usepath){
execvp(bin, arg);
}else{
execv(bin, arg);
}
exit(EXIT_FAILURE);
}else if(p < 0){
logwarn("clone3() failed (%s), using posix_spawn()\n", strerror(errno));
}
#endif
if(p < 0){
posix_spawn_file_actions_t factions;
if(posix_spawn_file_actions_init(&factions)){
logerror("couldn't initialize spawn file actions\n");
return -1;
}
posix_spawn_file_actions_adddup2(&factions, pipes[1], STDOUT_FILENO);
posix_spawn_file_actions_adddup2(&factions, pipes[1], STDERR_FILENO);
int r;
if(usepath){
r = posix_spawnp(&p, bin, &factions, NULL, arg, env);
}else{
r = posix_spawn(&p, bin, &factions, NULL, arg, env);
}
if(r){
logerror("posix_spawn %s failed (%s)\n", bin, strerror(errno));
}
posix_spawn_file_actions_destroy(&factions);
}
if(p > 0){ // parent
*pipefd = pipes[0];
set_fd_nonblocking(*pipefd, 1, NULL);
}
return p;
}
#endif
#ifndef __MINGW32__
// nuke the just-spawned process, and reap it. called before the subprocess
// reader thread is launched (which otherwise reaps the subprocess).
static int
kill_and_wait_subproc(pid_t pid, int pidfd, int* status){
int ret = -1;
// on linux, we try pidfd_send_signal, if the pidfd has been defined.
// otherwise, we fall back to regular old kill();
if(pidfd >= 0){
#ifdef USING_PIDFD
ret = syscall(__NR_pidfd_send_signal, pidfd, SIGKILL, NULL, 0);
siginfo_t info;
memset(&info, 0, sizeof(info));
waitid(P_PIDFD, pidfd, &info, 0);
#endif
}
if(ret < 0){
kill(pid, SIGKILL);
}
// process ought be available immediately following waitid(), so supply
// WNOHANG to avoid possible lockups due to weirdness
if(pid != waitpid(pid, status, WNOHANG)){
return -1;
}
return 0;
}
// need a poll on both main fd and pidfd
static void *
ncsubproc_thread(void* vncsp){
int* status = malloc(sizeof(*status));
if(status){
ncsubproc* ncsp = vncsp;
fdthread(ncsp->nfp, ncsp->pidfd);
if(kill_and_wait_subproc(ncsp->pid, ncsp->pidfd, status)){
*status = -1;
}
if(ncsp->nfp->destroyed){
ncfdplane_destroy_inner(ncsp->nfp);
free(ncsp);
}
}
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){
free(status);
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*
ncsubproc_launch(ncplane* n, ncsubproc* ret, const ncsubproc_options* opts, int fd,
ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn){
ncfdplane_options popts = {
.curry = opts->curry,
.follow = true,
};
ret->nfp = ncfdplane_create_internal(n, &popts, fd, cbfxn, donecbfxn, false);
if(ret->nfp == NULL){
return NULL;
}
if(pthread_create(&ret->nfp->tid, NULL, ncsubproc_thread, ret)){
ncfdplane_destroy_inner(ret->nfp);
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;
}
#endif
// use of env implies usepath
static ncsubproc*
ncexecvpe(ncplane* n, const ncsubproc_options* opts, unsigned usepath,
const char* bin, char* const arg[], char* const env[],
ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn){
ncsubproc_options zeroed = {0};
if(!opts){
opts = &zeroed;
}
if(!cbfxn || !donecbfxn){
return NULL;
}
if(opts->flags > 0){
logwarn("Provided unsupported flags %016" PRIx64 "\n", opts->flags);
}
#ifndef __MINGW32__
int fd = -1;
ncsubproc* ret = malloc(sizeof(*ret));
if(ret == NULL){
return NULL;
}
memset(ret, 0, sizeof(*ret));
ret->pid = launch_pipe_process(&fd, &ret->pidfd, usepath, bin, arg, env);
if(ret->pid < 0){
free(ret);
return NULL;
}
if((ret->nfp = ncsubproc_launch(n, ret, opts, fd, cbfxn, donecbfxn)) == NULL){
kill_and_wait_subproc(ret->pid, ret->pidfd, NULL);
free(ret);
return NULL;
}
return ret;
#else
// FIXME use CreateProcess()
(void)n;
(void)usepath;
(void)bin;
(void)arg;
(void)env;
(void)cbfxn;
(void)donecbfxn;
return NULL;
#endif
}
ncsubproc* ncsubproc_createv(ncplane* n, const ncsubproc_options* opts,
const char* bin, const char* const arg[],
ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn){
return ncexecvpe(n, opts, 0, bin, (char* const *)arg, NULL, cbfxn, donecbfxn);
}
ncsubproc* ncsubproc_createvp(ncplane* n, const ncsubproc_options* opts,
const char* bin, const char* const arg[],
ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn){
return ncexecvpe(n, opts, 1, bin, (char* const *)arg, NULL, cbfxn, donecbfxn);
}
ncsubproc* ncsubproc_createvpe(ncplane* n, const ncsubproc_options* opts,
const char* bin, const char* const arg[],
const char* const env[],
ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn){
return ncexecvpe(n, opts, 1, bin, (char* const *)arg, (char* const*)env, cbfxn, donecbfxn);
}
int ncsubproc_destroy(ncsubproc* n){
int ret = 0;
if(n){
void* vret = NULL;
//fprintf(stderr, "pid: %u pidfd: %d waittid: %u\n", n->pid, n->pidfd, n->waittid);
#ifndef __MINGW32__
#ifdef USING_PIDFD
if(n->pidfd >= 0){
loginfo("Sending SIGKILL to pidfd %d\n", n->pidfd);
if(syscall(__NR_pidfd_send_signal, n->pidfd, SIGKILL, NULL, 0)){
kill(n->pid, SIGKILL);
}
}
#else
pthread_mutex_lock(&n->lock);
if(!n->waited){
loginfo("Sending SIGKILL to PID %d\n", n->pid);
kill(n->pid, SIGKILL);
}
pthread_mutex_unlock(&n->lock);
#endif
#endif
// the thread waits on the subprocess via pidfd (iff pidfd >= 0), and
// 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);
}else{
pthread_join(n->nfp->tid, NULL);
}
pthread_mutex_destroy(&n->lock);
free(n);
if(vret == NULL){
ret = -1;
}else if(vret != PTHREAD_CANCELED){
ret = *(int*)vret;
free(vret);
}
}
return ret;
}
ncplane* ncsubproc_plane(ncsubproc* n){
return n->nfp->ncp;
}
// if ttyfp is a tty, return a file descriptor extracted from it. otherwise,
// try to get the controlling terminal. otherwise, return -1.
int get_tty_fd(FILE* ttyfp){
int fd = -1;
if(ttyfp){
if((fd = fileno(ttyfp)) < 0){
logwarn("no file descriptor was available in outfp %p\n", ttyfp);
}else{
if(tty_check(fd)){
fd = dup(fd);
}else{
loginfo("fd %d is not a TTY\n", fd);
fd = -1;
}
}
}
if(fd < 0){
fd = open("/dev/tty", O_RDWR | O_CLOEXEC | O_NOCTTY);
if(fd < 0){
loginfo("couldn't open /dev/tty (%s)\n", strerror(errno));
}else{
if(!tty_check(fd)){
loginfo("file descriptor for /dev/tty (%d) is not actually a TTY\n", fd);
close(fd);
fd = -1;
}
}
}
if(fd >= 0){
loginfo("returning TTY fd %d\n", fd);
}
return fd;
}