4

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)

Hans Lub
  • 5,513
  • 1
  • 23
  • 43
  • [Per POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/functions/read.html): "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. – Andrew Henle Feb 09 '21 at 11:22
  • Yes, I saw that too. My problem is: it states _if at EOF, then return 0_. But it doesn't say _if 0 is returned, you may assume EOF_. However, the prhrase _to indicate end-of-file_ seems to imply that de implication indeed can be read both ways. – Hans Lub Feb 09 '21 at 11:25
  • 2
    Out of curiosity, could you try `ioctl(STDOUT_FILENO, I_SWROPT, 0)` at the beginning of `pointless.c` on AIX and see what happens? ([why I think this may change the behavior](https://www.ibm.com/support/knowledgecenter/SSLTBW_2.3.0/com.ibm.zos.v2r3.bpxbd00/ioctl.htm)) – Sergey Kalinichenko Feb 09 '21 at 11:42
  • @Sergey_Kalinichenko: good one. I'l try it when I have time again... – Hans Lub Feb 09 '21 at 11:45
  • @Sergey_Kalinichenko: `ioctl(STDOUT_FILENO, I_SWROPT, 0)` does the trick! When I insert it at the beginning of `pointless.c` `rlwrap` doesn't return from the `pselect()` when `pointless` does the 0-bit `write`. I think I will use this between `fork()` and `execute()` on the slave side of the pty. Thanks a lot for the tip! In theory, the client program could undo the `ioctl()`, but this bug has gone unnoticed for over 20 years, so the sollution doesn't have to be perfect..... – Hans Lub Feb 09 '21 at 19:43

2 Answers2

2

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.

Devolus
  • 21,661
  • 13
  • 66
  • 113
2

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.

Andrew Henle
  • 32,625
  • 3
  • 24
  • 56
  • This is the key point. Don't call `write` with `nbytes==0` unless you want to deal with the unspecified, possibly implementation-specific consequences of doing so. – R.. GitHub STOP HELPING ICE Feb 09 '21 at 15:27
  • 1
    The point is that I, as `rlwrap` maintainer, encounter programs that _do_ stuff like calling `write` with `nbytes = 0`. Of course I could just say that those programs are doing it wrong, but they _exist_ so I'll have to deal with them.... – Hans Lub Feb 09 '21 at 15:40
  • @HansLub [`isastream()`](https://pubs.opengroup.org/onlinepubs/9699919799/functions/isastream.html) might prove useful. – Andrew Henle Feb 09 '21 at 16:10