read(fd, buf, N>0) == 0, but fd not at EOF?

595 views Asked by At

The following little C program (let's call it pointless):

/* pointless.c */
#include <stdio.h>
#include <unistd.h>

void main(){
  write(STDOUT_FILENO, "", 0); /* pointless write() of 0 bytes */
  sleep(1);
  write(STDOUT_FILENO, "still there!\n", 13);
}

will print "still there!" after a small delay, as expected. However, rlwrap ./pointless prints nothing under AIX and exits immediatly.

Apparently, rlwrap reads 0 bytes after the first write() and (incorrectly) decides that pointless has called it quits.

When running pointless without rlwrap, and with rlwrap on all other systems I could lay my hand on (Linux, OSX, FreeBSD), the "still there!" gets printed, as expected.

The relevant rlwrap (pseudo-)code is this:

/* master is  the file descriptor of the master end of a pty, while the slave is 'pointless's stdout   */
/* master was opened with O_NDELAY                                                                     */
while(pselect(nfds, &readfds, .....)) {
   if (FD_ISSET(master, &readfds)) {           /* master is "ready" for reading       */
      nread = read(master, buf, BUFFSIZE - 1); /* so try to read a buffer's worth     */
      if (nread == 0)                          /* 0 bytes read...                     */
         cleanup_and_exit();                   /* ... usually means EOF, doens't it?  */

Apparently, on all systems, except AIX, writeing 0 bytes on the slave end of a pty is a no-op, while on AIX it wakes up the select() on the master end. Writing 0 bytes seems pointless, but one of my test programs writes random-length chunks of text, which may actually happen to have length 0.

On linux, man 2 read states "on success, the number of bytes read is returned (zero indicates end of file)" (italics are mine) This question has come up before without mention of this scenario.

This begs the question: how can I portably determine whether the slave end has been closed? (In this case I can probably just wait for a SIGCHLD and then close shop, but that might open another can of worms I'd rather avoid)


Edit: POSIX states:

Writing a zero-length buffer (nbyte is 0) to a STREAMS device sends 0 bytes with 0 returned. However, writing a zero-length buffer to a STREAMS-based pipe or FIFO sends no message and 0 is returned. The process may issue I_SWROPT ioctl() to enable zero-length messages to be sent across the pipe or FIFO.

On AIX, pty is indeed a STREAMS device, moreover, not a pipe or FIFO. ioctl(STDOUT_FILENO, I_SWROPT, 0) seems to make it possible to make the pty conform to the rest of the Unix world. The sad thing is that this has to be called from the slave side, and so is outside rlwraps sphere of infuence (even though we could call the ioctl() between fork() and exec() - that would not guarantee that the executed command won't change it back)

2

There are 2 answers

5
Devolus On

From the linux man page

If count is zero and fd refers to a regular file, then write() may return a failure status if one of the errors below is detected. If no errors are detected, or error detection is not performed, 0 will be returned without causing any other effect. If count is zero and fd refers to a file other than a regular file, the results are not specified.

So, since it is unspecified, it can do whatever it likes in your case.

3
Andrew Henle On

Per POSIX:

When attempting to read from an empty pipe or FIFO:

  • If no process has the pipe open for writing, read() shall return 0 to indicate end-of-file."

So the "read of zero bytes means EOF" is POSIX-compliant.

On the write() side (bolding mine):

Before any action described below is taken, and if nbyte is zero and the file is a regular file, the write() function may detect and return errors as described below. In the absence of errors, or if error detection is not performed, the write() function shall return zero and have no other results. If nbyte is zero and the file is not a regular file, the results are unspecified.

Unfortunately, that means you can't portably depend on a write() of zero bytes to have no effect because AIX is compliant with the POSIX standard for write() here.

You probably have to rely on SIGCHLD.