Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does reading from my pseudo-terminal fail?

Tags:

c

linux

pty

I've created a pseudo terminal (/dev/pts/N) from a process A and I am writing random integers to that on a certain interval. I can open that pts from screen and check its output.
But cat /dev/pts/N fails: it infinitely blocks and doesn't return.

I am trying to read that from another process using open()/read() functions and also there read() never returns.

int main(){
  int source_fd = open("/dev/pts/4", O_RDONLY);

  while(1){
      char buffer[READ_BUFFER_SIZE] = {0};
      char* buff_ptr = buffer;
      int r = read(source_fd, (void*)buff_ptr, 1);
      // !!!! never comes here
      while(r > 0){
        ++buff_ptr;
        r = read(source_fd, (void*)buff_ptr, 1);
      }
  }
}
like image 258
Neel Basu Avatar asked Feb 10 '23 12:02

Neel Basu


1 Answers

Short answer: You're not handling the pseudoterminal correctly. Observing strange or even random results by having an outside process read from the pseudoterminal is normal; you're not supposed to do that. It's like having two people write on the same keyboard at the same time. (Just because you can see that in some TV shows, does not mean it makes any sense whatsoever.)


Long answer: Alter your approach, and you'll have much better results.

Consider the following task you can do, to acquint yourself with pseudoterminal behaviour:

  1. Create a pseudoterminal master, and allow slave access to it

    (Use posix_openpt(), grantpt(), and unlockpt() to create the pseudoterminal. Use ptsname() to find out the device name of the slave end.)

  2. Fork a child process.

    (Use fork() to fork the child process, then setsid() to detach from the controlling terminal. It also creates a new process group, so your master process can send signals to all processes started by the child process by sending the signals to the entire group.)

  3. In the child process, open standard input (STDIN_FILENO) for reading from slave pseudoterminal, and standard output (STDOUT_FILENO) and standard error (STDERR_FILENO) for writing to the slave end of the pseudoterminal. Execute nano.

    (Use dup2() to copy descriptors to their correct places, close() to close the extra ones, and e.g. execlp("nano", "nano", NULL) to execute nano. Note that the first "nano" is the file name of the nano command, and the second is the argv[0] parameter the command sees. It does not supply any actual command-line parameters; it acts as if you ran nano in your favourite shell.)

  4. In the parent process, you can now read and write to the master end of the pseudoterminal.

    Note that you may have to do so concurrently; there is no way to know when you can/need/must read (more), and when writing might block.

    I cannot stress enough how important it is to be full-duplex or nonblocking here. If you never read from your pseudoterminal, do not expect it to work, either.

  5. In the parent process, remove file foobar.txt.

    (Use remove() or unlink().)

    This is just so that nano will not pop up a "File already exists" dialog.

  6. In the parent process, while reading any output the slave process might write to the pseudoterminal,

    • Wait a fraction of a second (while nano draws the editor screen)

    • Write Some text and a carriage return \r,

    • Wait a fraction of a second,

    • Write Ctrl+O (\017, often visualized as ^O)

    • Write foobar.txt and a carriage return \r,

    • Wait a fraction of a second,

    • Write Ctrl+X (\030, often visualized as ^X),

    • Wait

    and nano should exit.

  7. In the parent process, wait for the child (nano) process to exit.

    (Use a loop and waitpid() for this.)

If you accomplish the above, your master terminal control program just emulated a local or remote "human" running a very short nano session, writing just Some text and a newline, saving it to foobar.txt, and exiting. (The file should contain "Some text\n\n", because that's how nano works.)

Step 6 is easiest to achieve, if you create a helper thread that does nothing but reads from the master pseudoterminal file descriptor. In a very clear sense, it acts like an automatic drain. After all, we're not really interested in what nano outputs to the terminal here. After the step 7, you simply close that descriptor, causing the helper thread to error out (read() returns -1 with errno == EBADF) and return, so the main thread can use pthread_join() to reap it.

You can implement step 6 using nonblocking I/O, of course. Any way you do it, it is imperative you always read() from the master pseudoterminal, and do not get deadlocked by write()ing to it while the slave process is also writing to the terminal. This is the situation the OP is struggling with, I bet.

A typical sequence of communications flowing through the pseudoterminal in the above scenario is:

Slave -> Master: "\e[?1049h\e[1;24r\e(B\e[m\e[4l\e[?7h\e[?12l\e[?25h"
Slave -> Master: "\e[?1h\e=\e[?1h\e=\e[?1h\e="
Slave -> Master: "\e[39;49m\e[39;49m\e(B\e[m\e[H\e[2J\e(B\e[0;7m"
                 "  GNU nano 2.2.6              "
                 "  New Buffer                                      "
                 "\e[23;1H^G\e(B\e[m Get Help  "
                 "\e(B\e[0;7m^O\e(B\e[m WriteOut  "
                 "\e(B\e[0;7m^R\e(B\e[m Read File "
                 "\e(B\e[0;7m^Y\e(B\e[m Prev Page "
                 "\e(B\e[0;7m^K\e(B\e[m Cut Text  "
                 "\e(B\e[0;7m^C\e(B\e[m Cur Pos"
                 "\015\e[24d\e(B\e[0;7m^X\e(B\e[m Exit"
                 "\e[14G\e(B\e[0;7m^J\e(B\e[m Justify   "
                 "\e(B\e[0;7m^W\e(B\e[m Where Is  "
                 "\e(B\e[0;7m^V\e(B\e[m Next Page "
                 "\e(B\e[0;7m^U\e(B\e[m UnCut Text"
                 "\e(B\e[0;7m^T\e(B\e[m To Spell\015\e[3d"
Master -> Slave: "Some text\015"
Slave -> Master: "\e[1;71H\e(B\e[0;7mModified\015\e[3d\e(B\e[mSome text\015\e[4d"
Master -> Slave: "\017"
Slave -> Master: "\e[22d\e(B\e[0;7mFile Name to Write: "
                 "                              "
                 "                              "
                 "\e[23;14H\e(B\e[m       "
                 "\e(B\e[0;7mM-D\e(B\e[m DOS Format      "
                 "\e(B\e[0;7mM-A\e(B\e[m Append          "
                 "\e(B\e[0;7mM-B\e(B\e[m Backup File"
                 "\e[24;2H\e(B\e[0;7mC\e(B\e[m Cancel           "
                 "\e(B\e[0;7mM-M\e(B\e[m Mac Format      "
                 "\e(B\e[0;7mM-P\e(B\e[m Prefix\e[K\e[22;21H"
Master -> Slave: "foobar.txt\015"
Slave -> Master: "\e[1;31H\e[39;49m\e(B\e[0;7mFile: foobar.txt"
                 "\e[1;71H        \e[22;31H\e(B\e[m\e[1K "
                 "\e(B\e[0;7m[ Wrote 2 lines ]"
                 "\e(B\e[m\e[K\e[23;14H\e(B\e[0;7m^O\e(B\e[m WriteOut  "
                 "\e(B\e[0;7m^R\e(B\e[m Read File "
                 "\e(B\e[0;7m^Y\e(B\e[m Prev Page "
                 "\e(B\e[0;7m^K\e(B\e[m Cut Text  "
                 "\e(B\e[0;7m^C\e(B\e[m Cur Pos"
                 "\e[24;2H\e(B\e[0;7mX\e(B\e[m Exit      "
                 "\e(B\e[0;7m^J\e(B\e[m Justify   "
                 "\e(B\e[0;7m^W\e(B\e[m Where Is  "
                 "\e(B\e[0;7m^V\e(B\e[m Next Page "
                 "\e(B\e[0;7m^U\e(B\e[m UnCut Text"
                 "\e(B\e[0;7m^T\e(B\e[m To Spell\015\e[4d"
Master -> Slave: "\030"
Slave -> Master: "\e[23d\e[J\e[24;80H"
Slave -> Master: "\e[24;1H\e[?1049l\015\e[?1l\e>"

where \e is shorthand for \033 or \x1B, ie. the ASCII ESC character.

Especially note how the slave nano process spews all kinds of output, just to draw a fancy editor screen. If there was a clock or some such that changed regularly, it would basically spew those updates every second.

The reason for Master->Slave using \r instead of \n as newline is the default termios settings.

like image 152
Nominal Animal Avatar answered Feb 13 '23 03:02

Nominal Animal