Samuel Williams Thursday, 02 April 2009

I've had quite a few issues with poll and kqueue on Mac OS X/Darwin. When trying to poll for events using fileno(stdin) or STDIN_FILENO, it gives errors such as Operation not supported (kqueue) or POLLNVAL (poll).

On Darwin (at least up until Mac OS X 10.5.5), stdin, stdout and stderr are actually devices! Not pipes like one might expect. Also, poll is implemented on top of kqueue, therefore problems with kqueue also affect poll.

Because of this, polling stdin won't work. Here is an example which fails on Mac OS X 10.5.5:

#include <stdio.h>
#include <poll.h>
 
int main (int argc, char ** argv)
{
	struct pollfd pfd;
	pfd.fd = fileno(stdin);
	pfd.events = POLLIN|POLLOUT;
	pfd.revents = 0;
 
	int result = poll(&pfd, 1, 0);
 
	if (result < 0) {
		printf("poll = %d\n", result);
		perror("poll failed");
	}
 
	printf("pfd.revents = %d\n", pfd.revents);
 
	if (pfd.revents & POLLNVAL) {
		printf("invalid file descriptor\n");
	}
 
	return 0;
}

A hackity solution

This code uses boost threads. What it does is create three pipes, one for stdin, one for stdout and one for stderr. The originals are moved to different fds and then we spawn three threads to process data between the original devices and the newly created pipes. The result of this is that STDIN_FILENO, as an example, now refers to the write end of a pipe. The other end of this pipe is being read by the thread, and any data is written out to the original stdin device.

class ConsolePipeRedirector {
	boost::thread_group m_threads;
	bool m_done;
 
public:
	ConsolePipeRedirector () : m_done(false)
	{
 
	}
 
	static void copyDataBetweenFileDescriptors (int input, int output)
	{
		char buf[512];
 
		while(1) {
			ssize_t count = read(input, buf, 512);
 
			if (count < 0) {
				perror("copyDataBetweenFileDescriptors");
			} else if (count == 0) {
				break;
			}
 
			write(output, buf, count);
		}				
	}
 
	void reopenStandardFileDescriptorsAsPipes ()
	{
		if (m_done) return;
		m_done = true;
 
		int result;
		int stdinPipe[2], stdoutPipe[2], stderrPipe[2];
 
		// Create a new pipe for stdin data
		result = pipe(stdinPipe);
		if (result) perror("pipe(stdinPipe)");
		// Copy the stdin device to a new fd
		int stdinDevice = dup(STDIN_FILENO);
 
		// Same again for stdout
		result = pipe(stdoutPipe);
		if (result) perror("pipe(stdoutPipe)");
		int stdoutDevice = dup(STDOUT_FILENO);
 
		// Same again for stderr
		result = pipe(stderrPipe);
		if (result) perror("pipe(stderrPipe)");
		int stderrDevice = dup(STDERR_FILENO);
 
		// Copy the pipe endpoint into the standard place for in, out and err.
		dup2(stdinPipe[0], STDIN_FILENO);
		dup2(stdoutPipe[1], STDOUT_FILENO);
		dup2(stderrPipe[1], STDERR_FILENO);
 
		// Spawn threads to handle reading and writing in a blocking fashion.
		m_threads.create_thread(bind(copyDataBetweenFileDescriptors, stdinDevice, stdinPipe[1]));
		m_threads.create_thread(bind(copyDataBetweenFileDescriptors, stdoutPipe[0], stdoutDevice));
		m_threads.create_thread(bind(copyDataBetweenFileDescriptors, stderrPipe[0], stderrDevice));
	}
};

In this case, I've fixed all three standard fds. However, it is probably only useful in most cases to fix stdin - this is likely to be the only device you would want to poll. I've included support for all three as an example of how it would be done.

Poll specifically is a bit different across different platforms. Therefore, you need to be aware of the differences.

Comments

Leave a comment

Please note, comments must be formatted using Markdown. Links can be enclosed in angle brackets, e.g. <www.codeotaku.com>.