mirror of
https://github.com/jart/cosmopolitan.git
synced 2025-01-31 11:37:35 +00:00
2ec413b5a9
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.
366 lines
14 KiB
C
366 lines
14 KiB
C
#if 0
|
|
/*─────────────────────────────────────────────────────────────────╗
|
|
│ To the extent possible under law, Justine Tunney has waived │
|
|
│ all copyright and related or neighboring rights to this file, │
|
|
│ as it is written in the following disclaimers: │
|
|
│ • http://unlicense.org/ │
|
|
│ • http://creativecommons.org/publicdomain/zero/1.0/ │
|
|
╚─────────────────────────────────────────────────────────────────*/
|
|
#endif
|
|
|
|
// posix_spawn() example
|
|
//
|
|
// This program demonstrates the use of posix_spawn() to run the command
|
|
// `ls --dired` and capture its output. It teaches several key features:
|
|
//
|
|
// - Changing the working directory for the child process
|
|
// - Redirecting stdout and stderr to pipes
|
|
// - Handling the output from the child process
|
|
//
|
|
// The primary advantage of using posix_spawn() instead of the
|
|
// traditional fork()/execve() combination for launching processes is
|
|
// safety, efficiency, and cross-platform compatibility.
|
|
//
|
|
// 1. On Linux, FreeBSD, and NetBSD:
|
|
//
|
|
// Cosmopolitan Libc's posix_spawn() uses vfork() under the hood on
|
|
// these platforms automatically, since it's faster than fork(). It's
|
|
// because vfork() creates a child process without needing to copy
|
|
// the parent's page tables, making it more efficient, especially for
|
|
// large processes. Furthermore, vfork() avoids the need to acquire
|
|
// every single mutex (see pthread_atfork() for more details) which
|
|
// makes it scalable in multi-threaded apps, since the other threads
|
|
// in your app can keep going while the spawning thread waits for the
|
|
// subprocess to call execve(). Normally vfork() is error-prone since
|
|
// there exists few functions that are @vforksafe. the posix_spawn()
|
|
// API is designed to offer maximum assurance that you can't shoot
|
|
// yourself in the foot. If you do, then file a bug with Cosmo.
|
|
//
|
|
// 2. On Windows:
|
|
//
|
|
// posix_spawn() avoids fork() entirely. Windows doesn't natively
|
|
// support fork(), and emulating it can be slow and memory-intensive.
|
|
// By using posix_spawn(), we get a much faster process creation on
|
|
// Windows systems, because it only needs to call CreateProcess().
|
|
// Your file actions are replayed beforehand in a simulated way. Only
|
|
// Cosmopolitan Libc offers this level of quality. With Cygwin you'd
|
|
// have to use its proprietary APIs to achieve the same performance.
|
|
//
|
|
// 3. Simplified error handling:
|
|
//
|
|
// posix_spawn() combines process creation and program execution in a
|
|
// single call, reducing the points of failure and simplifying error
|
|
// handling. One important thing that happens with Cosmopolitan's
|
|
// posix_spawn() implementation is that the error code of execve()
|
|
// inside your subprocess, should it fail, will be propagated to your
|
|
// parent process. This will happen efficiently via vfork() shared
|
|
// memory in the event your Linux environment supports this. If it
|
|
// doesn't, then Cosmopolitan will fall back to a throwaway pipe().
|
|
// The pipe is needed on platforms like XNU and OpenBSD which do not
|
|
// support vfork(). It's also needed under QEMU User.
|
|
//
|
|
// 4. Signal safety:
|
|
//
|
|
// posix_spawn() guarantees your signal handler callback functions
|
|
// won't be executed in the child process. By default, it'll remove
|
|
// sigaction() callbacks atomically. This ensures that if something
|
|
// like a SIGTERM or SIGHUP is sent to the child process before it's
|
|
// had a chance to call execve(), then the child process will simply
|
|
// be terminated (like the spawned process would) instead of running
|
|
// whatever signal handlers the spawning process has installed. If
|
|
// you've set some signals to SIG_IGN, then that'll be preserved for
|
|
// the child process by posix_spawn(), unless you explicitly call
|
|
// posix_spawnattr_setsigdefault() to reset them.
|
|
//
|
|
// 5. Portability:
|
|
//
|
|
// posix_spawn() is part of the POSIX standard, making it more
|
|
// portable across different UNIX-like systems and Windows (with
|
|
// appropriate libraries). Even the non-POSIX APIs we use here are
|
|
// portable; e.g. posix_spawn_file_actions_addchdir_np() is supported
|
|
// by glibc, musl, freebsd, and apple too.
|
|
//
|
|
// These benefits make posix_spawn() a preferred choice for efficient
|
|
// and portable process creation in many scenarios, especially when
|
|
// launching many processes or on systems where process creation
|
|
// 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() {
|
|
errno_t err;
|
|
|
|
// 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.
|
|
//
|
|
// 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.
|
|
err = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK);
|
|
if (err != 0) {
|
|
fprintf(stderr, "posix_spawnattr_setflags: %s\n", strerror(err));
|
|
exit(2);
|
|
}
|
|
|
|
// 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 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);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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 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 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);
|
|
};
|
|
|
|
// 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]);
|
|
|
|
// 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));
|
|
}
|
|
}
|
|
}
|
|
|
|
#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 child process to die.
|
|
int wait_status;
|
|
if (waitpid(pid, &wait_status, 0) == -1) {
|
|
perror("waitpid");
|
|
exit(11);
|
|
}
|
|
|
|
// Clean up resources.
|
|
posix_spawn_file_actions_destroy(&actions);
|
|
posix_spawnattr_destroy(&attr);
|
|
close(pipe_stdout[PIPE_READ]);
|
|
close(pipe_stderr[PIPE_READ]);
|
|
|
|
// 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 exited weirdly with wait status 0x%08x\n",
|
|
wait_status);
|
|
exit(12);
|
|
}
|
|
}
|