Skip to content

Commit 2ec413b

Browse files
committed
Fix bugs in poll(), select(), ppoll(), and pselect()
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.
1 parent 39e7f24 commit 2ec413b

File tree

27 files changed

+663
-2131
lines changed

27 files changed

+663
-2131
lines changed

examples/spawn.c

Lines changed: 224 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -86,147 +86,281 @@
8686
// performance is critical.
8787

8888
#define _GNU_SOURCE
89+
#include <errno.h>
8990
#include <fcntl.h>
91+
#include <poll.h>
92+
#include <signal.h>
9093
#include <spawn.h>
9194
#include <stdio.h>
9295
#include <stdlib.h>
9396
#include <string.h>
97+
#include <sys/select.h>
9498
#include <sys/wait.h>
9599
#include <unistd.h>
96100

101+
#define max(X, Y) ((Y) < (X) ? (X) : (Y))
102+
103+
#define USE_SELECT 0 // want poll() or select()? they both work great
104+
97105
#define PIPE_READ 0
98106
#define PIPE_WRITE 1
99107

100108
int main() {
101-
pid_t pid;
102-
int status, ret;
103-
posix_spawnattr_t attr;
104-
posix_spawn_file_actions_t actions;
105-
char *const argv[] = {"ls", "--dired", NULL};
106-
int pipe_stdout[2], pipe_stderr[2];
109+
errno_t err;
107110

108-
// Initialize file actions
109-
ret = posix_spawnattr_init(&attr);
110-
if (ret != 0) {
111-
fprintf(stderr, "posix_spawnattr_init failed: %s\n", strerror(ret));
112-
return 1;
111+
// Create spawn attributes object.
112+
posix_spawnattr_t attr;
113+
err = posix_spawnattr_init(&attr);
114+
if (err != 0) {
115+
fprintf(stderr, "posix_spawnattr_init failed: %s\n", strerror(err));
116+
exit(1);
113117
}
114118

115-
// Explicitly request vfork() from posix_spawn() implementation
119+
// Explicitly request vfork() from posix_spawn() implementation.
116120
//
117121
// This is currently the default for Cosmopolitan Libc, however you
118122
// may want to set this anyway, for portability with other platforms.
119123
// Please note that vfork() isn't officially specified by POSIX, so
120124
// portable code may want to omit this and just use the default.
121-
ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK);
122-
if (ret != 0) {
123-
fprintf(stderr, "posix_spawnattr_setflags failed: %s\n", strerror(ret));
124-
return 1;
125+
err = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK);
126+
if (err != 0) {
127+
fprintf(stderr, "posix_spawnattr_setflags: %s\n", strerror(err));
128+
exit(2);
125129
}
126130

127-
// Initialize file actions
128-
ret = posix_spawn_file_actions_init(&actions);
129-
if (ret != 0) {
130-
fprintf(stderr, "posix_spawn_file_actions_init failed: %s\n",
131-
strerror(ret));
132-
return 1;
131+
// Create file actions object.
132+
posix_spawn_file_actions_t actions;
133+
err = posix_spawn_file_actions_init(&actions);
134+
if (err != 0) {
135+
fprintf(stderr, "posix_spawn_file_actions_init: %s\n", strerror(err));
136+
exit(3);
133137
}
134138

135-
// Change directory to $HOME
136-
ret = posix_spawn_file_actions_addchdir_np(&actions, getenv("HOME"));
137-
if (ret != 0) {
138-
fprintf(stderr, "posix_spawn_file_actions_addchdir_np failed: %s\n",
139-
strerror(ret));
140-
return 1;
139+
// Change directory to root directory in child process.
140+
err = posix_spawn_file_actions_addchdir_np(&actions, "/");
141+
if (err != 0) {
142+
fprintf(stderr, "posix_spawn_file_actions_addchdir_np: %s\n",
143+
strerror(err));
144+
exit(4);
141145
}
142146

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

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

158-
// Redirect child's stderr to pipe
159-
ret = posix_spawn_file_actions_adddup2(&actions, pipe_stderr[PIPE_WRITE],
160-
STDERR_FILENO);
161-
if (ret != 0) {
162-
fprintf(stderr, "posix_spawn_file_actions_adddup2 (stderr) failed: %s\n",
163-
strerror(ret));
164-
return 1;
183+
// Redirect child's stdout/stderr to pipes
184+
if ((err = posix_spawn_file_actions_adddup2(&actions, pipe_stdout[PIPE_WRITE],
185+
STDOUT_FILENO)) ||
186+
(err = posix_spawn_file_actions_adddup2(&actions, pipe_stderr[PIPE_WRITE],
187+
STDERR_FILENO))) {
188+
fprintf(stderr, "posix_spawn_file_actions_adddup2: %s\n", strerror(err));
189+
exit(7);
165190
}
166191

167-
// Close unused write ends of pipes in the child process
168-
ret = posix_spawn_file_actions_addclose(&actions, pipe_stdout[PIPE_READ]);
169-
if (ret != 0) {
170-
fprintf(stderr,
171-
"posix_spawn_file_actions_addclose (stdout read) failed: %s\n",
172-
strerror(ret));
173-
return 1;
174-
}
175-
ret = posix_spawn_file_actions_addclose(&actions, pipe_stderr[PIPE_READ]);
176-
if (ret != 0) {
177-
fprintf(stderr,
178-
"posix_spawn_file_actions_addclose (stderr read) failed: %s\n",
179-
strerror(ret));
180-
return 1;
181-
}
192+
// Close unwanted write ends of pipes in the child process
193+
if ((err = posix_spawn_file_actions_addclose(&actions,
194+
pipe_stdout[PIPE_READ])) ||
195+
(err = posix_spawn_file_actions_addclose(&actions,
196+
pipe_stderr[PIPE_READ]))) {
197+
fprintf(stderr, "posix_spawn_file_actions_addclose: %s\n", strerror(err));
198+
exit(8);
199+
};
182200

183-
// Spawn the child process
184-
ret = posix_spawnp(&pid, "ls", &actions, NULL, argv, NULL);
185-
if (ret != 0) {
186-
fprintf(stderr, "posix_spawn failed: %s\n", strerror(ret));
187-
return 1;
201+
// Asynchronously launch the child process.
202+
pid_t pid;
203+
char *const argv[] = {"ls", "--dired", NULL};
204+
printf("** Launching `ls --dired` in root directory\n");
205+
err = posix_spawnp(&pid, argv[0], &actions, NULL, argv, NULL);
206+
if (err) {
207+
fprintf(stderr, "posix_spawn: %s\n", strerror(err));
208+
exit(9);
188209
}
189210

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

194-
// Read and print output from child process
195-
char buffer[4096];
196-
ssize_t bytes_read;
197-
198-
printf("Stdout from child process:\n");
199-
while ((bytes_read = read(pipe_stdout[PIPE_READ], buffer, sizeof(buffer))) >
200-
0) {
201-
write(STDOUT_FILENO, buffer, bytes_read);
215+
// we need poll() or select() because we're multiplexing output
216+
// both poll() and select() work across all supported platforms
217+
#if USE_SELECT
218+
// Relay output from child process using select()
219+
char buffer[512];
220+
ssize_t got_stdout = 1;
221+
ssize_t got_stderr = 1;
222+
while (got_stdout > 0 || got_stderr > 0) {
223+
fd_set rfds;
224+
FD_ZERO(&rfds);
225+
if (got_stdout > 0)
226+
FD_SET(pipe_stdout[PIPE_READ], &rfds);
227+
if (got_stderr > 0)
228+
FD_SET(pipe_stderr[PIPE_READ], &rfds);
229+
int nfds = max(pipe_stdout[PIPE_READ], pipe_stderr[PIPE_READ]) + 1;
230+
if (select(nfds, &rfds, 0, 0, 0) == -1) {
231+
perror("select");
232+
exit(10);
233+
}
234+
if (FD_ISSET(pipe_stdout[PIPE_READ], &rfds)) {
235+
got_stdout = read(pipe_stdout[PIPE_READ], buffer, sizeof(buffer));
236+
printf("\n");
237+
if (got_stdout > 0) {
238+
printf("** Got stdout from child process:\n");
239+
fflush(stdout);
240+
write(STDOUT_FILENO, buffer, got_stdout);
241+
} else if (!got_stdout) {
242+
printf("** Got stdout EOF from child process\n");
243+
} else {
244+
printf("** Got stdout read() error from child process: %s\n",
245+
strerror(errno));
246+
}
247+
}
248+
if (FD_ISSET(pipe_stderr[PIPE_READ], &rfds)) {
249+
got_stderr = read(pipe_stderr[PIPE_READ], buffer, sizeof(buffer));
250+
printf("\n");
251+
if (got_stderr > 0) {
252+
printf("** Got stderr from child process:\n");
253+
fflush(stdout);
254+
write(STDOUT_FILENO, buffer, got_stderr);
255+
} else if (!got_stderr) {
256+
printf("** Got stderr EOF from child process\n");
257+
} else {
258+
printf("** Got stderr read() error from child process: %s\n",
259+
strerror(errno));
260+
}
261+
}
202262
}
203263

204-
printf("\nStderr from child process:\n");
205-
while ((bytes_read = read(pipe_stderr[PIPE_READ], buffer, sizeof(buffer))) >
206-
0) {
207-
write(STDERR_FILENO, buffer, bytes_read);
264+
#else
265+
// Relay output from child process using poll()
266+
char buffer[512];
267+
ssize_t got_stdout = 1;
268+
ssize_t got_stderr = 1;
269+
while (got_stdout > 0 || got_stderr > 0) {
270+
struct pollfd fds[2];
271+
fds[0].fd = got_stdout > 0 ? pipe_stdout[PIPE_READ] : -1;
272+
fds[0].events = POLLIN; // POLLHUP, POLLNVAL, and POLLERR are implied
273+
fds[1].fd = got_stderr > 0 ? pipe_stderr[PIPE_READ] : -1;
274+
fds[1].events = POLLIN; // POLLHUP, POLLNVAL, and POLLERR are implied
275+
if (poll(fds, 2, -1) == -1) {
276+
perror("select");
277+
exit(10);
278+
}
279+
if (fds[0].revents) {
280+
printf("\n");
281+
if (fds[0].revents & POLLIN)
282+
printf("** Got POLLIN on stdout from child process\n");
283+
if (fds[0].revents & POLLHUP)
284+
printf("** Got POLLHUP on stdout from child process\n");
285+
if (fds[0].revents & POLLERR)
286+
printf("** Got POLLERR on stdout from child process\n");
287+
if (fds[0].revents & POLLNVAL)
288+
printf("** Got POLLNVAL on stdout from child process\n");
289+
got_stdout = read(pipe_stdout[PIPE_READ], buffer, sizeof(buffer));
290+
if (got_stdout > 0) {
291+
printf("** Got stdout from child process:\n");
292+
fflush(stdout);
293+
write(STDOUT_FILENO, buffer, got_stdout);
294+
} else if (!got_stdout) {
295+
printf("** Got stdout EOF from child process\n");
296+
} else {
297+
printf("** Got stdout read() error from child process: %s\n",
298+
strerror(errno));
299+
}
300+
}
301+
if (fds[1].revents) {
302+
printf("\n");
303+
if (fds[1].revents & POLLIN)
304+
printf("** Got POLLIN on stderr from child process\n");
305+
if (fds[1].revents & POLLHUP)
306+
printf("** Got POLLHUP on stderr from child process\n");
307+
if (fds[1].revents & POLLERR)
308+
printf("** Got POLLERR on stderr from child process\n");
309+
if (fds[1].revents & POLLNVAL)
310+
printf("** Got POLLNVAL on stderr from child process\n");
311+
got_stderr = read(pipe_stderr[PIPE_READ], buffer, sizeof(buffer));
312+
if (got_stderr > 0) {
313+
printf("** Got stderr from child process:\n");
314+
fflush(stdout);
315+
write(STDOUT_FILENO, buffer, got_stderr);
316+
} else if (!got_stderr) {
317+
printf("** Got stderr EOF from child process\n");
318+
} else {
319+
printf("** Got stderr read() error from child process: %s\n",
320+
strerror(errno));
321+
}
322+
}
208323
}
324+
#endif
209325

210-
// Wait for the child process to complete
211-
if (waitpid(pid, &status, 0) == -1) {
326+
// Wait for child process to die.
327+
int wait_status;
328+
if (waitpid(pid, &wait_status, 0) == -1) {
212329
perror("waitpid");
213-
return 1;
330+
exit(11);
214331
}
215332

216-
// Clean up
333+
// Clean up resources.
217334
posix_spawn_file_actions_destroy(&actions);
218335
posix_spawnattr_destroy(&attr);
219336
close(pipe_stdout[PIPE_READ]);
220337
close(pipe_stderr[PIPE_READ]);
221338

222-
if (WIFEXITED(status)) {
223-
printf("Child process exited with status %d\n", WEXITSTATUS(status));
224-
} else if (WIFSIGNALED(status)) {
225-
printf("Child process terminated with signal %s\n",
226-
strsignal(WTERMSIG(status)));
339+
// Report wait status.
340+
//
341+
// When a process dies, it's almost always due to calling _Exit() or
342+
// being killed due to an unhandled signal. On both UNIX and Windows
343+
// this information will be propagated to the parent. That status is
344+
// able to be propagated to the parent of this process too.
345+
printf("\n");
346+
if (WIFEXITED(wait_status)) {
347+
printf("** Child process exited with exit code %d\n",
348+
WEXITSTATUS(wait_status));
349+
exit(WEXITSTATUS(wait_status));
350+
} else if (WIFSIGNALED(wait_status)) {
351+
printf("** Child process terminated with signal %s\n",
352+
strsignal(WTERMSIG(wait_status)));
353+
fflush(stdout);
354+
sigset_t sm;
355+
sigemptyset(&sm);
356+
sigaddset(&sm, WTERMSIG(wait_status));
357+
sigprocmask(SIG_UNBLOCK, &sm, 0);
358+
signal(SIGABRT, SIG_DFL);
359+
raise(WTERMSIG(wait_status));
360+
exit(128 + WTERMSIG(wait_status));
227361
} else {
228-
printf("Child process did not exit normally\n");
362+
printf("** Child process exited weirdly with wait status 0x%08x\n",
363+
wait_status);
364+
exit(12);
229365
}
230-
231-
return 0;
232366
}

libc/calls/close-nt.c

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,6 @@ textwindows int sys_close_nt(int fd, int fildes) {
5252
FlushFileBuffers(f->handle);
5353
}
5454
break;
55-
case kFdEpoll:
56-
if (_weaken(sys_close_epoll_nt)) {
57-
return _weaken(sys_close_epoll_nt)(fd);
58-
}
59-
break;
6055
case kFdSocket:
6156
if (_weaken(sys_closesocket_nt)) {
6257
return _weaken(sys_closesocket_nt)(g_fds.p + fd);

0 commit comments

Comments
 (0)