#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 #include #include #include #include #include #include #include #include #include #include #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); } }