Non-Master PTY output discarded on child process exit

Hi! I use "non-master" term as a replacement for *****. I cannot find a documented safe way to read an output from non-master PTY because unlike Linux, on macOS it is discarded immediately after the child process exits. The situation is similar, regardless of whether I use forkpty(), or open /dev/ptmx directly. Here is my example:

Code Block C
#undef NDEBUG
#define CHECK assert
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#if defined(__APPLE__)
#include <util.h>
#else
#include <pty.h>
#endif
int main(void)
{
int master;
char name[PATH_MAX];
pid_t pid = forkpty(&master, name, NULL, NULL);
CHECK(pid != -1);
if (pid == 0) { // child
//char *args[] = {"/bin/echo", "01234567890123456789012345678901234567890123456789", NULL};
//execv(args[0], args);
char buf[] =
"01234567890123456789012345678901234567890123456789" // 50
"01234567890123456789012345678901234567890123456789"; // 100
CHECK(printf("%s", buf) == sizeof buf - 1);
CHECK(fflush(stdout) == 0);
exit(EXIT_SUCCESS);
}
// parent
#define BUFSIZE 64
char buf[BUFSIZE];
//int non-master = open(name, O_WRONLY); // here!
//CHECK(non-master != -1);
sleep(2); // simulate process scheduling
int len;
do {
len = read(master, buf, BUFSIZE);
if (len == -1) {
printf("read = %d, err = %d\n", len, errno); // EIO on Linux
break;
}
printf("read %d bytes: %.*s\n", len, len, buf);
} while (len != 0);
int status;
CHECK(waitpid(pid, &status, 0) == pid);
CHECK(WIFEXITED(status));
CHECK(WEXITSTATUS(status) == EXIT_SUCCESS);
CHECK(close(master) != -1);
return 0;
}


On Linux, the output is:
Code Block sh
$ ./a.out
read 64 bytes: 0123456789012345678901234567890123456789012345678901234567890123
read 36 bytes: 456789012345678901234567890123456789
read = -1, err = 5


but on macOS:
Code Block sh
$ ./a.out
read 0 bytes:


There is a known workaround -- keep an open non-master fd in the parent process (see unix.stackexchange.com/a/478969), but it really looks like a "hacky" way, which btw causes a bug on Linux (read() never returns), and which isn't documented anywhere, right? So from this point of view, the code above should work as good as on Linux, and many projects use PTY this way (w/o non-master fd), which turns out to not work.

For example, Python's Pexpect will fail on macOS if there is a delay between spawn() and the first .expect():
Code Block python
import pexpect, time
child = pexpect.spawn('python3 -c \'print("%s", flush=True)\'' % ("0123456789" * 10))
time.sleep(2)
child.expect("0123456789" * 10)


results to:
Code Block sh
$ python3 pexpect_test.py
Traceback (most recent call last):
File "pexpect_test.py", line 5, in <module>
child.expect("0123456789" * 10)
File "/usr/local/lib/python3.8/site-packages/pexpect/spawnbase.py", line 343, in expect
return self.expect_list(compiled_pattern_list,
File "/usr/local/lib/python3.8/site-packages/pexpect/spawnbase.py", line 372, in expect_list
return exp.expect_loop(timeout)
File "/usr/local/lib/python3.8/site-packages/pexpect/expect.py", line 179, in expect_loop
return self.eof(e)
File "/usr/local/lib/python3.8/site-packages/pexpect/expect.py", line 122, in eof
raise exc
pexpect.exceptions.EOF: End Of File (EOF). Empty string style platform.


Is this the expected behavior that should be specified in openpty(3) manual page, or is it a bug in macOS Catalina 10.15.7?
Update: While reporting this issue to Pexpect: https://github.com/pexpect/pexpect/issues/662 , I found that their own implementation _fork_pty.fork_pty() for Solaris also works on macOS, due to explicit opening /dev/tty in the child process:
Code Block C++
if (pid == 0) { // child
int ttyfd = open("/dev/tty", O_WRONLY);
CHECK(ttyfd != -1);
CHECK(close(ttyfd) != -1);
[...]


after that (with these 3 lines added after line #26), the output on macOS is:
Code Block sh
$ ./a.out
read 64 bytes: 0123456789012345678901234567890123456789012345678901234567890123
read 36 bytes: 456789012345678901234567890123456789
read 0 bytes:

Non-Master PTY output discarded on child process exit
 
 
Q