0

Why is it a standard stream can receive input from a terminal in canonical mode, but you put it in raw mode and suddenly this method is no longer valid? I'm well aware of POSIX serial programming, and typically you use read. I'm trying to understand standard streams better.

#include <termios.h>
#include <unistd.h>
#include <iostream>

termios original;

void enableRawMode() {
  tcgetattr(STDIN_FILENO, &original);
  termios raw = original;
  cfmakeraw(&raw);
  raw.c_cc[VMIN] = 0;
  raw.c_cc[VTIME] = 1;
  tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

int main() {
  enableRawMode();

  char c;

  // This works as expected.
  //int nread;
  //while ((nread = read(STDIN_FILENO, &c, 1)) != 1) {
  //  if (nread == -1 && errno != EAGAIN) {
  //    break;
  //  }
  //}

  // This loops forever, the failbit is always true, gcount is always 0.
  while (!(std::cin.get(c))) {
    if (std::cin.bad() || std::cin.eof()) {
      break;
    }

    std::cin.clear();
  }

  tcsetattr(STDIN_FILENO, TCSAFLUSH, &original);
}
Matthew Reddington
  • 1,409
  • 3
  • 13
  • 24
  • The short answer is no: this is completely impractical. You must use read/write and add all the special logic, like EAGAIN, to deal with it. The input/output stream library knows nothing about EAGAIN, and offers no means of configuring itself accordingly. – Sam Varshavchik Sep 03 '20 at 00:54
  • Have you tried different values for VIM and VTIME? See https://stackoverflow.com/questions/25996171/linux-blocking-vs-non-blocking-serial-read/26006680#26006680 If your program simply loops, then it is just polling the system buffer and wasting CPU cycles. *"This loops forever, ..."* -- **Unable to replicate this symptom on Linux 5.4; program exits without waiting for any input!** – sawdust Sep 03 '20 at 00:57
  • @sawdust This example is just a relevant sampling from the greater whole, I'm aware that this spins like crazy. Rest assured there is more care given toward scheduling and process management in application. – Matthew Reddington Sep 03 '20 at 05:31
  • @SamVarshavchik But why does data arrive on the standard file descriptor but not the standard stream, if the two are tied and synchronized? – Matthew Reddington Sep 03 '20 at 05:36
  • So you have an excuse for using inefficient code. What about the different result I get on Linux versus whatever *"POSIX"* OS that you're using? FYI when `VTIME` is increased to `20`, the program terminates 2 seconds (i.e. 20 deciseconds) after initiation. IOW the program does seem to respond to termios settings. You need to debug your program more thoroughly. – sawdust Sep 03 '20 at 07:26
  • Because, as I wrote, "the input/output stream library knows nothing about EAGAIN", so which part of that is unclear? The stream library: a) attempts to buffer when reading data, b) if read() returns 0 or -1 for any reason, the stream is considered to be in eof/failed state. If you want the input stream library to work with the input file descriptor, in this case, you'll have to implement a custom subclass of `std::streambuf` that knows how to deal with what `read()` tells it, each time, and in which way; then use this custom streambuf subclass to construct your input stream. – Sam Varshavchik Sep 03 '20 at 10:49
  • @SamVarshavchik I expected input to be queued. Did I miss the part where RAW mode disables that? I expect the standard stream to fail so long as there is no data in the queue, sure, fine, but once I bang on the keyboard I expected the loop to break. Yes, this is basically a form of polling, I just want to understand why the stream can't do it. This is an exercise in understanding streams. I can read about serial programming for POSIX systems all day, but on the topic of interacting with the C++ runtime, documentation is sparse. – Matthew Reddington Sep 03 '20 at 16:20
  • Not sure why you "expected input to be queued". Setting `VTIME` explicitly "unqueue"s input. It times out, then `istream` sees that `read()` failed, because that's what happened (the timeout expired), and puts the stream into bad state. If you want input to be "queued", that's what happens by default. – Sam Varshavchik Sep 03 '20 at 18:36
  • @SamVarshavchik See, that's what I'm talking about. I have not found it stated in any documentation that this would be the case, and how else could I have known? I understand how and why the stream enters the failed state, that's not as important to me. – Matthew Reddington Sep 03 '20 at 20:38
  • @SamVarshavchik So I set `VTIME` to 10 and it allowed me time to press a key and get input. I reduced the code to `std::cin.rdbuf()->sbumpc()` which succeeds so long as input arrives within the timeout, but if the timer lapses, it will forever return `EOF` instantly. This is again unexpected, that what will likely resolve to a call to `underflow` can put the `streambuf` into an unrecoverable state. I know this particular stream buffer is implementation defined, but do you have any insights as to why this is the case? – Matthew Reddington Sep 03 '20 at 22:01
  • There's nothing in the C++ standard about raw try mode, so I would not expect this to be documented or defined behavior. It should be possible to clear() the stream and reset it to known state. If not there are two basic option: sink a massive amount of time into debugging the stream library; or implement your own streambuf subclass, that handles reading from raw ttys. I know what choice I would make. – Sam Varshavchik Sep 03 '20 at 22:30
  • @SamVarshavchik My code here does clear the stream but that's only a bit field in the basic_stream, it doesn't effect the stream buffer, which does not retain state. Well, the whole point to this exercise was to the extents of the given standard streams, and I think I've about done that with your help. It's really disappointing that the spec doesn't seem to say anything about raw or cooked mode of the underlying device, even though it's apparent that's actually very significant. I at least like the idea of the standard stream interface, but I suddenly like the given implementation a lot less. – Matthew Reddington Sep 03 '20 at 22:51
  • `cin` could be some [pipe(7)](https://man7.org/linux/man-pages/man7/pipe.7.html) so is not always a tty – Basile Starynkevitch Sep 03 '20 at 23:47

1 Answers1

0

After much discussion and a bit of investigation, I've discovered this is a limitation to the implementation of the given standard streams. Basically, I've deduced that standard streams work fine when your terminal is in cooked mode, but unreliable if in raw mode.

The code can be reduced to using std::cin.rdbuf()->sbumpc(), which in this case is going to call uflow, which will likely call underflow, which will fetch data from the underlying device. The stream buffer associated with the standard streams are implementation defined.

The first call to sbumpc and more specifically and ultimately underflow will initiate the timer associated with VTIME when it reads from standard input. Provided input is received within the timeout, cin will work as expected. But if the timeout occurs, the stream buffer will enter an undefined state, and sbumpc will forever return EOF.

State is associated with the stream, not the stream buffer, so calling std::cin.clear() clears the state flags but does not correct the underlying issue within the stream buffer, which remains broken.

There may be a solution in resetting the seek position, but again, the given standard streams are implementation defined and there is no knowing what internal state they may possess, or if successful on one platform if it would be successful on another platform. The documentation just doesn't exist to cover this scenario, insofar as I can tell.

Two possible solutions would be to just use the POSIX API directly and like it, or write your own stream implementation that uses the POSIX API.

Matthew Reddington
  • 1,409
  • 3
  • 13
  • 24