> 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
>>>
>>

Reply via email to