Skip to content

Commit

Permalink
Fix bugs in poll(), select(), ppoll(), and pselect()
Browse files Browse the repository at this point in the history
poll() and select() now delegate to ppoll() and pselect() for assurances
that both polyfill implementations are correct and well-tested. Poll now
polyfills XNU and BSD quirks re: the hanndling of POLLNVAL and the other
similar status flags. This change resolves a misunderstanding concerning
how select(exceptfds) is intended to map to POLPRI. We now use E2BIG for
bouncing requests that exceed the 64 handle limit on Windows. With pipes
and consoles on Windows our poll impl will now report POLLHUP correctly.

Issues with Windows path generation have been fixed. For example, it was
problematic on Windows to say: posix_spawn_file_actions_addchdir_np("/")
due to the need to un-UNC paths in some additional places. Calling fstat
on UNC style volume path handles will now work. posix_spawn now supports
simulating the opening of /dev/null and other special paths on Windows.

Cosmopolitan no longer defines epoll(). I think wepoll is a nice project
for using epoll() on Windows socket handles. However we need generalized
file descriptor support to make epoll() for Windows work well enough for
inclusion in a C library. It's also not worth having epoll() if we can't
get it to work on XNU and BSD OSes which provide different abstractions.
Even epoll() on Linux isn't that great of an abstraction since it's full
of footguns. Last time I tried to get it to be useful I had little luck.
Considering how long it took to get poll() and select() to be consistent
across platforms, we really have no business claiming to have epoll too.
While it'd be nice to have fully implemented, the only software that use
epoll() are event i/o libraries used by things like nodejs. Event i/o is
not the best paradigm for handling i/o; threads make so much more sense.
  • Loading branch information
jart committed Sep 2, 2024
1 parent 39e7f24 commit 2ec413b
Show file tree
Hide file tree
Showing 27 changed files with 663 additions and 2,131 deletions.
314 changes: 224 additions & 90 deletions examples/spawn.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,147 +86,281 @@
// performance is critical.

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/wait.h>
#include <unistd.h>

#define max(X, Y) ((Y) < (X) ? (X) : (Y))

#define USE_SELECT 0 // want poll() or select()? they both work great

#define PIPE_READ 0
#define PIPE_WRITE 1

int main() {
pid_t pid;
int status, ret;
posix_spawnattr_t attr;
posix_spawn_file_actions_t actions;
char *const argv[] = {"ls", "--dired", NULL};
int pipe_stdout[2], pipe_stderr[2];
errno_t err;

// Initialize file actions
ret = posix_spawnattr_init(&attr);
if (ret != 0) {
fprintf(stderr, "posix_spawnattr_init failed: %s\n", strerror(ret));
return 1;
// Create spawn attributes object.
posix_spawnattr_t attr;
err = posix_spawnattr_init(&attr);
if (err != 0) {
fprintf(stderr, "posix_spawnattr_init failed: %s\n", strerror(err));
exit(1);
}

// Explicitly request vfork() from posix_spawn() implementation
// Explicitly request vfork() from posix_spawn() implementation.
//
// This is currently the default for Cosmopolitan Libc, however you
// may want to set this anyway, for portability with other platforms.
// Please note that vfork() isn't officially specified by POSIX, so
// portable code may want to omit this and just use the default.
ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK);
if (ret != 0) {
fprintf(stderr, "posix_spawnattr_setflags failed: %s\n", strerror(ret));
return 1;
err = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK);
if (err != 0) {
fprintf(stderr, "posix_spawnattr_setflags: %s\n", strerror(err));
exit(2);
}

// Initialize file actions
ret = posix_spawn_file_actions_init(&actions);
if (ret != 0) {
fprintf(stderr, "posix_spawn_file_actions_init failed: %s\n",
strerror(ret));
return 1;
// Create file actions object.
posix_spawn_file_actions_t actions;
err = posix_spawn_file_actions_init(&actions);
if (err != 0) {
fprintf(stderr, "posix_spawn_file_actions_init: %s\n", strerror(err));
exit(3);
}

// Change directory to $HOME
ret = posix_spawn_file_actions_addchdir_np(&actions, getenv("HOME"));
if (ret != 0) {
fprintf(stderr, "posix_spawn_file_actions_addchdir_np failed: %s\n",
strerror(ret));
return 1;
// Change directory to root directory in child process.
err = posix_spawn_file_actions_addchdir_np(&actions, "/");
if (err != 0) {
fprintf(stderr, "posix_spawn_file_actions_addchdir_np: %s\n",
strerror(err));
exit(4);
}

// Create pipes for stdout and stderr
if (pipe(pipe_stdout) == -1 || pipe(pipe_stderr) == -1) {
perror("pipe");
return 1;
// Disable stdin in child process.
//
// By default, if you launch this example in your terminal, then child
// processes can read from your teletypewriter's keyboard too. You can
// avoid this by assigning /dev/null to standard input so if the child
// tries to read input, read() will return zero, indicating eof.
if ((err = posix_spawn_file_actions_addopen(&actions, STDIN_FILENO,
"/dev/null", O_RDONLY, 0644))) {
fprintf(stderr, "posix_spawn_file_actions_addopen: %s\n", strerror(err));
exit(5);
}

// Redirect child's stdout to pipe
ret = posix_spawn_file_actions_adddup2(&actions, pipe_stdout[PIPE_WRITE],
STDOUT_FILENO);
if (ret != 0) {
fprintf(stderr, "posix_spawn_file_actions_adddup2 (stdout) failed: %s\n",
strerror(ret));
return 1;
// Create pipes for stdout and stderr.
//
// Using O_DIRECT puts the pipe in message mode. This way we have some
// visibility into how the child process is using write(). It can also
// help ensure that logged lines won't be chopped up here, which could
// happen more frequently on platforms like Windows, which is somewhat
// less sophisticated than Linux with how it performs buffering.
//
// You can also specify O_CLOEXEC, which is a nice touch that lets you
// avoid needing to call posix_spawn_file_actions_addclose() later on.
// That's because all file descriptors are inherited by child programs
// by default. This is even the case with Cosmopolitan Libc on Windows
//
// XXX: We assume that stdin/stdout/stderr exist in this process. It's
// possible for a rogue parent process to launch this example, in
// a way where the following spawn logic will break.
int pipe_stdout[2];
int pipe_stderr[2];
if (pipe2(pipe_stdout, O_DIRECT) == -1 ||
pipe2(pipe_stderr, O_DIRECT) == -1) {
perror("pipe");
exit(6);
}

// Redirect child's stderr to pipe
ret = posix_spawn_file_actions_adddup2(&actions, pipe_stderr[PIPE_WRITE],
STDERR_FILENO);
if (ret != 0) {
fprintf(stderr, "posix_spawn_file_actions_adddup2 (stderr) failed: %s\n",
strerror(ret));
return 1;
// Redirect child's stdout/stderr to pipes
if ((err = posix_spawn_file_actions_adddup2(&actions, pipe_stdout[PIPE_WRITE],
STDOUT_FILENO)) ||
(err = posix_spawn_file_actions_adddup2(&actions, pipe_stderr[PIPE_WRITE],
STDERR_FILENO))) {
fprintf(stderr, "posix_spawn_file_actions_adddup2: %s\n", strerror(err));
exit(7);
}

// Close unused write ends of pipes in the child process
ret = posix_spawn_file_actions_addclose(&actions, pipe_stdout[PIPE_READ]);
if (ret != 0) {
fprintf(stderr,
"posix_spawn_file_actions_addclose (stdout read) failed: %s\n",
strerror(ret));
return 1;
}
ret = posix_spawn_file_actions_addclose(&actions, pipe_stderr[PIPE_READ]);
if (ret != 0) {
fprintf(stderr,
"posix_spawn_file_actions_addclose (stderr read) failed: %s\n",
strerror(ret));
return 1;
}
// Close unwanted write ends of pipes in the child process
if ((err = posix_spawn_file_actions_addclose(&actions,
pipe_stdout[PIPE_READ])) ||
(err = posix_spawn_file_actions_addclose(&actions,
pipe_stderr[PIPE_READ]))) {
fprintf(stderr, "posix_spawn_file_actions_addclose: %s\n", strerror(err));
exit(8);
};

// Spawn the child process
ret = posix_spawnp(&pid, "ls", &actions, NULL, argv, NULL);
if (ret != 0) {
fprintf(stderr, "posix_spawn failed: %s\n", strerror(ret));
return 1;
// Asynchronously launch the child process.
pid_t pid;
char *const argv[] = {"ls", "--dired", NULL};
printf("** Launching `ls --dired` in root directory\n");
err = posix_spawnp(&pid, argv[0], &actions, NULL, argv, NULL);
if (err) {
fprintf(stderr, "posix_spawn: %s\n", strerror(err));
exit(9);
}

// Close unused write ends of pipes in the parent process
close(pipe_stdout[PIPE_WRITE]);
close(pipe_stderr[PIPE_WRITE]);

// Read and print output from child process
char buffer[4096];
ssize_t bytes_read;

printf("Stdout from child process:\n");
while ((bytes_read = read(pipe_stdout[PIPE_READ], buffer, sizeof(buffer))) >
0) {
write(STDOUT_FILENO, buffer, bytes_read);
// we need poll() or select() because we're multiplexing output
// both poll() and select() work across all supported platforms
#if USE_SELECT
// Relay output from child process using select()
char buffer[512];
ssize_t got_stdout = 1;
ssize_t got_stderr = 1;
while (got_stdout > 0 || got_stderr > 0) {
fd_set rfds;
FD_ZERO(&rfds);
if (got_stdout > 0)
FD_SET(pipe_stdout[PIPE_READ], &rfds);
if (got_stderr > 0)
FD_SET(pipe_stderr[PIPE_READ], &rfds);
int nfds = max(pipe_stdout[PIPE_READ], pipe_stderr[PIPE_READ]) + 1;
if (select(nfds, &rfds, 0, 0, 0) == -1) {
perror("select");
exit(10);
}
if (FD_ISSET(pipe_stdout[PIPE_READ], &rfds)) {
got_stdout = read(pipe_stdout[PIPE_READ], buffer, sizeof(buffer));
printf("\n");
if (got_stdout > 0) {
printf("** Got stdout from child process:\n");
fflush(stdout);
write(STDOUT_FILENO, buffer, got_stdout);
} else if (!got_stdout) {
printf("** Got stdout EOF from child process\n");
} else {
printf("** Got stdout read() error from child process: %s\n",
strerror(errno));
}
}
if (FD_ISSET(pipe_stderr[PIPE_READ], &rfds)) {
got_stderr = read(pipe_stderr[PIPE_READ], buffer, sizeof(buffer));
printf("\n");
if (got_stderr > 0) {
printf("** Got stderr from child process:\n");
fflush(stdout);
write(STDOUT_FILENO, buffer, got_stderr);
} else if (!got_stderr) {
printf("** Got stderr EOF from child process\n");
} else {
printf("** Got stderr read() error from child process: %s\n",
strerror(errno));
}
}
}

printf("\nStderr from child process:\n");
while ((bytes_read = read(pipe_stderr[PIPE_READ], buffer, sizeof(buffer))) >
0) {
write(STDERR_FILENO, buffer, bytes_read);
#else
// Relay output from child process using poll()
char buffer[512];
ssize_t got_stdout = 1;
ssize_t got_stderr = 1;
while (got_stdout > 0 || got_stderr > 0) {
struct pollfd fds[2];
fds[0].fd = got_stdout > 0 ? pipe_stdout[PIPE_READ] : -1;
fds[0].events = POLLIN; // POLLHUP, POLLNVAL, and POLLERR are implied
fds[1].fd = got_stderr > 0 ? pipe_stderr[PIPE_READ] : -1;
fds[1].events = POLLIN; // POLLHUP, POLLNVAL, and POLLERR are implied
if (poll(fds, 2, -1) == -1) {
perror("select");
exit(10);
}
if (fds[0].revents) {
printf("\n");
if (fds[0].revents & POLLIN)
printf("** Got POLLIN on stdout from child process\n");
if (fds[0].revents & POLLHUP)
printf("** Got POLLHUP on stdout from child process\n");
if (fds[0].revents & POLLERR)
printf("** Got POLLERR on stdout from child process\n");
if (fds[0].revents & POLLNVAL)
printf("** Got POLLNVAL on stdout from child process\n");
got_stdout = read(pipe_stdout[PIPE_READ], buffer, sizeof(buffer));
if (got_stdout > 0) {
printf("** Got stdout from child process:\n");
fflush(stdout);
write(STDOUT_FILENO, buffer, got_stdout);
} else if (!got_stdout) {
printf("** Got stdout EOF from child process\n");
} else {
printf("** Got stdout read() error from child process: %s\n",
strerror(errno));
}
}
if (fds[1].revents) {
printf("\n");
if (fds[1].revents & POLLIN)
printf("** Got POLLIN on stderr from child process\n");
if (fds[1].revents & POLLHUP)
printf("** Got POLLHUP on stderr from child process\n");
if (fds[1].revents & POLLERR)
printf("** Got POLLERR on stderr from child process\n");
if (fds[1].revents & POLLNVAL)
printf("** Got POLLNVAL on stderr from child process\n");
got_stderr = read(pipe_stderr[PIPE_READ], buffer, sizeof(buffer));
if (got_stderr > 0) {
printf("** Got stderr from child process:\n");
fflush(stdout);
write(STDOUT_FILENO, buffer, got_stderr);
} else if (!got_stderr) {
printf("** Got stderr EOF from child process\n");
} else {
printf("** Got stderr read() error from child process: %s\n",
strerror(errno));
}
}
}
#endif

// Wait for the child process to complete
if (waitpid(pid, &status, 0) == -1) {
// Wait for child process to die.
int wait_status;
if (waitpid(pid, &wait_status, 0) == -1) {
perror("waitpid");
return 1;
exit(11);
}

// Clean up
// Clean up resources.
posix_spawn_file_actions_destroy(&actions);
posix_spawnattr_destroy(&attr);
close(pipe_stdout[PIPE_READ]);
close(pipe_stderr[PIPE_READ]);

if (WIFEXITED(status)) {
printf("Child process exited with status %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child process terminated with signal %s\n",
strsignal(WTERMSIG(status)));
// Report wait status.
//
// When a process dies, it's almost always due to calling _Exit() or
// being killed due to an unhandled signal. On both UNIX and Windows
// this information will be propagated to the parent. That status is
// able to be propagated to the parent of this process too.
printf("\n");
if (WIFEXITED(wait_status)) {
printf("** Child process exited with exit code %d\n",
WEXITSTATUS(wait_status));
exit(WEXITSTATUS(wait_status));
} else if (WIFSIGNALED(wait_status)) {
printf("** Child process terminated with signal %s\n",
strsignal(WTERMSIG(wait_status)));
fflush(stdout);
sigset_t sm;
sigemptyset(&sm);
sigaddset(&sm, WTERMSIG(wait_status));
sigprocmask(SIG_UNBLOCK, &sm, 0);
signal(SIGABRT, SIG_DFL);
raise(WTERMSIG(wait_status));
exit(128 + WTERMSIG(wait_status));
} else {
printf("Child process did not exit normally\n");
printf("** Child process exited weirdly with wait status 0x%08x\n",
wait_status);
exit(12);
}

return 0;
}
5 changes: 0 additions & 5 deletions libc/calls/close-nt.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,6 @@ textwindows int sys_close_nt(int fd, int fildes) {
FlushFileBuffers(f->handle);
}
break;
case kFdEpoll:
if (_weaken(sys_close_epoll_nt)) {
return _weaken(sys_close_epoll_nt)(fd);
}
break;
case kFdSocket:
if (_weaken(sys_closesocket_nt)) {
return _weaken(sys_closesocket_nt)(g_fds.p + fd);
Expand Down
Loading

0 comments on commit 2ec413b

Please sign in to comment.