Search code examples
c++posixiostreamtermios

How to get `cin` to read a raw mode terminal


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);
}

Solution

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