> agreed - some improvement is possible there. The manpage doesn't mention EOF > (it could also hint that checking errno would be useful after receiving ERR, > but there's no guarantee that there aren't other calls/other errors in the > interval between calling read and checking errno).
It actually appears errno is unreliable in this context. It is usually not set when getch returns ERR. It could even have a value from some unrelated earlier operation. Instead of suggesting errno, I believe the manpage should clarify that to detect specific conditions like EOF from a pipe or a terminal disconnection, the application should monitor signals (like SIGHUP) or check the file descriptor state directly. On Sun, Mar 29, 2026 at 7:06 PM Klaas van Aarsen <[email protected]> wrote: > >> Regarding the suggestion of EBADF, it is my understanding that the > kernel > >> must keep file descriptor 0 open (valid) even after a disconnect to > prevent > >> immediate FD reuse. > > either it's closed, or it's not. > > ncurses doesn't reopen it. > > Since ncurses doesn't reopen it, I don't see any reason why the dangling > > symlink would be relevant. > > To clarify my earlier point regarding EBADF: > > If the kernel were to simply close file descriptor 0 upon a disconnect, > the very next file opened by the process (or a library) would be assigned > FD 0, as it is the lowest available slot. > This would cause catastrophic confusion, as code expecting stdin would > suddenly be reading from a random file or socket. > > Instead, the descriptor remains allocated to the process but points to a > defunct terminal (often following a SIGHUP). > This is why read() returns 0 (EOF) and errno remains 0, rather than > triggering EBADF. > > We can detect this specific "ghost" state using isatty(STDIN_FILENO). > Upon disconnect, isatty will return 0 (false) because the underlying > terminal device is no longer associated with the descriptor, even though > the descriptor itself hasn't been "closed." > This approach doesn't work with a piped file though. > > My mention of the dangling symlink was simply to illustrate this state: > the process holds a valid descriptor to a path/device that no longer exists > in the active filesystem. > > > On Sun, Mar 29, 2026 at 6:38 PM Klaas van Aarsen < > [email protected]> wrote: > >> > If you're able to reproduce the problem, and modify the program to >> report >> > the errno value, then we can discuss whether the errno value matches the >> > expected behavior. >> >> I have run the requested tests to capture errno during both a pipe EOF >> and a forced SSH disconnect. >> >> Environment >> ----------- >> Ubuntu 22.04.5 LTS in WSL2 >> ncurses 6.3.20211021 >> >> Test Program >> ------------ >> #include <ncurses.h> >> #include <errno.h> >> #include <stdio.h> >> >> int main() { >> FILE *f = fopen("trace.log", "w"); >> initscr(); >> halfdelay(5); >> for (int i = 0; i < 100; ++i) { >> errno = 0; >> int ch = getch(); >> int err = errno; >> fprintf(f, "getch: %d, errno: %d\n", ch, err); >> fflush(f); >> } >> endwin(); >> fclose(f); >> } >> >> >> Scenario 1: Pipe EOF >> -------------------- >> $ echo "hi" | ./a.out ; cat trace.log >> >> Results show getch: 104, 105, 10 followed by consistent getch: -1, errno: >> 0. >> >> >> Scenario 2: SSH Disconnect >> -------------------------- >> 1. SSH into host. >> 2. Run ./a.out. >> 3. Hard-close the terminal window (sending no logout/exit). >> 4. Inspect log after 30 seconds. >> >> Trace Log Result: >> getch: 72, errno: 0 (H) >> getch: 105, errno: 0 (i) >> getch: -1, errno: 0 >> getch: -1, errno: 0 >> ... [continues until loop end] ... >> getch: -1, errno: 0 >> >> >> Scenario 3: Manually closing stdin (FD 0) >> ----------------------------------------- >> To test if getch() would report EBADF, I added close(0) inside the loop. >> >> Result: The loop completed instantly (ignoring the halfdelay timeout). >> getch() returned -1 for every iteration, but errno remained 0. >> >> This demonstrates that even when the file descriptor is explicitly >> invalid (EBADF condition), getch() does not pass that error information >> through to the caller. >> >> >> Conclusion >> ---------- >> In all cases, ncurses reports ERR but the system does not report EBADF or >> any other error via errno. >> This suggests that the terminal disconnect is being treated as a >> persistent EOF (0) rather than a file descriptor error. >> >> On Sun, Mar 29, 2026 at 5:32 PM Thomas Dickey < >> [email protected]> wrote: >> >>> On Sun, Mar 29, 2026 at 12:15:45PM +0200, Klaas van Aarsen wrote: >>> > > man read(2) >>> > > EBADF fd is not a valid file descriptor or is not open for >>> reading. >>> > > Klaas is describing a condition which isn't in the manpage - ymmv. >>> > >>> > Regarding the suggestion of EBADF, it is my understanding that the >>> kernel >>> > must keep file descriptor 0 open (valid) even after a disconnect to >>> prevent >>> > immediate FD reuse. >>> >>> either it's closed, or it's not. >>> >>> ncurses doesn't reopen it. >>> >>> Since ncurses doesn't reopen it, I don't see any reason why the dangling >>> symlink would be relevant. >>> >>> Checking errno after the failed call would be more useful than making >>> a signal handler :-) >>> >>> > If read() were to return EBADF while the descriptor is still allocated >>> to >>> > the process, it would technically contradict the POSIX definition of >>> that >>> > error. >>> >>> If you're able to reproduce the problem, and modify the program to report >>> the errno value, then we can discuss whether the errno value matches the >>> expected behavior. >>> >>> > While the kernel could conceivably return an error like EIO or ENXIO to >>> > signal a "dead" device, treating a terminal disconnect as an "end of >>> > stream" (returning 0) appears to be the standard behavior for >>> > pseudo-terminals across most modern Unix-like systems. >>> > This ensures the stream is closed gracefully without releasing the >>> > descriptor slot prematurely. >>> >>> ncurses is using the read directly (stream usually refers to things >>> opened via fopen). >>> >>> initscr (used in this program) calls newterm >>> >>> if (newterm(name, stdout, stdin) == NULL) { >>> >>> newterm copies the file descriptor: >>> >>> SP_PARM->_ifd = fileno(_ifp); >>> >>> and after that, it ignores any buffering that the caller may have on that >>> file descriptor. ncurses never closes that file descriptor. If you got >>> EBADF, then that would indicate that the connection was closed (either >>> end of a connection can close it). >>> >>> > I appreciate the technical dialogue on this. >>> > >>> > >>> > On Sun, Mar 29, 2026 at 11:59 AM Thomas Dickey < >>> [email protected]> >>> > wrote: >>> > >>> > > On Sun, Mar 29, 2026 at 11:40:56AM +0200, Stian Skjelstad wrote: >>> > > > > Actually what you're telling us is that there's a defect in >>> (presumably >>> > > > Linux) >>> > > > > which should be documented in read(2). Once you've gotten it >>> > > documented >>> > > > in >>> > > > >the proper place (Linux manpages), then others can use the >>> > > documentation. >>> > > > >>> > > > It is normal for read() to return 0, without an error when EOF is >>> > > reached. >>> > > > This is how you detect EOF/closed on sockets and files. Should TTY >>> behave >>> > > > differently? >>> > > >>> > > man read(2) >>> > > >>> > > EBADF fd is not a valid file descriptor or is not open for >>> reading. >>> > > >>> > > Klaas is describing a condition which isn't in the manpage - ymmv. >>> > > >>> > > -- >>> > > Thomas E. Dickey <[email protected]> >>> > > https://invisible-island.net >>> > > >>> >>> -- >>> Thomas E. Dickey <[email protected]> >>> https://invisible-island.net >>> >>
