Skip to content
Advertisement

ncurses newterm following openpty

I am trying to figure out how to do the following:

  1. create a new pseudo-terminal

  2. open a ncurses screen running inside the (slave) pseudo terminal

  3. fork

  4. A) forward I/O from the terminal the program is running in (bash) to the new (slave) terminal OR

    B) exit leaving the ncurses program running in the new pty.

Can anyone provide pointers to what I might be doing wrong or that would make sense of some of this or even better an example program using newterm() with either posix_openpt(), openpty() or forkpty().

The code I have is roughly (details simplified or omitted):

openpty(master,slave,NULL,NULL,NULL);    
pid_t res = fork();
if(res == -1) 
   std::exit(1);
if(res == 0) //child
{ 
   FILE* scrIn = open(slave,O_RDWR|O_NONBLOCK);
   FILE* scrOut = open(slave,O_RDWR|O_NONBLOCK);
   SCREEN* scr = newterm(NULL,scrIn,scrOut);
}
else //parent
{
   if (!optionA) 
       exit(0); // but leave the child running and using the slave
   for(;;) 
   {
      // forward IO to slave
      fd_set          read_fd;
      fd_set          write_fd;
      fd_set          except_fd;
      FD_ZERO(&read_fd);
      FD_ZERO(&write_fd);
      FD_ZERO(&except_fd);

      FD_SET(masterTty, &read_fd);
      FD_SET(STDIN_FILENO, &read_fd);

      select(masterTty+1, &read_fd, &write_fd, &except_fd, NULL);
      char input[2];
      char output[2];
      input[1]=0;
      output[1]=0;
      if (FD_ISSET(masterTty, &read_fd))
      {
         if (read(masterTty, &output, 1) != -1)
         {
            write(STDOUT_FILENO, &output, 1);
         }
      }

      if (FD_ISSET(STDIN_FILENO, &read_fd))
      {
        read(STDIN_FILENO, &input, 1);
        write(masterTty, &input, 1);
      }
    }
  }

}

I have various debug routines logging results from the parent and child to files.

There are several things relating to terminals that I do not understand. I have seen several behaviours I don’t understand depending on what variations I try.

Things I don’t understand:

  • If I instruct the parent process exits the child terminates without anything interesting being logged by the child.

  • If I try closing stdin, stdout and using dup() or dup2() to make the pty the replace stdin the curses window uses the original stdin and stdout and uses the original pty not the new one based on the output of ptsname(). (the parent process successful performs IO with the child but in the terminal it was lauched from not the new pty)

  • If I open the new pty using open() then I get a segfault inside the ncurses newterm() call as below:

    Program terminated with signal 11, Segmentation fault.
    #0  0x00007fbd0ff580a0 in fileno_unlocked () from /lib64/libc.so.6
    Missing separate debuginfos, use: debuginfo-install glibc-2.17-317.el7.x86_64 ncurses-libs-5.9-14.20130511.el7_4.x86_64
    (gdb) where
    #0  0x00007fbd0ff580a0 in fileno_unlocked () from /lib64/libc.so.6
    #1  0x00007fbd106eced9 in newterm () from /lib64/libncurses.so.5
    ...  now in my program...

I am trying to understand the pty system calls here. Using a program like screen or tmux does not help with this (also the source is not sufficiently annotated to fill in the gaps in my understanding).

Some other datums:

  • I am targeting GNU/Linux

  • I have also tried using forkpty

  • I looked at source for openpty, forkpty, login_tty, openpt, grantpt & posix_openpt

    (e.g. https://github.com/coreutils/gnulib/blob/master/lib/posix_openpt.c)

  • I don’t have access to a copy of APUE though I have looked at the pty example.

  • Although the ncurses documentation for newterm() mentions talking to multiple terminals simultaneously I have not found an example program that does this.

I am still not clear on:

  • what login_tty / grantpt actually do.

    If you opened the pty yourself why wouldn’t you already have the correct capabilities?

  • why I might prefer openpty to posix_openpt or visa-versa.


Note: This is a different question to attach-a-terminal-to-a-process-running-as-a-daemon-to-run-an-ncurses-ui which describes a use case and looks for a solution where this question assumes a particular but incorrect/incomplete implementation for that use case.

Advertisement

Answer

Glärbo’s answers have helped me understand the problems enough that after some experimentation I believe I can answer my remaining questions directly.

The important points are:

  • The master side of the pty must remain opened
  • The file descriptor for the slave must be opened in the same mode as originally created.
  • without setsid() on the slave it remains connected to the original controlling terminal.
  • You need to be careful with ncurses calls when using newterm rather tha initscr

The master side of the pty must remain opened

Me: “If I instruct the parent process exits the child terminates without anything interesting being logged by the child.”

Glärbo: “Without the master, and a process managing the master side, there is literally no pseudoterminal pair: when the master is closed, the kernel forcibly removes the slave too, invalidating the file descriptors the slave has open to the slave side of the pseudoterminal pair.”

The file descriptor for the slave must be opened in the same mode as originally created.

My incorrect pseudo code (for the child side of the fork):

 FILE* scrIn = open(slave,O_RDWR|O_NONBLOCK);
 FILE* scrOut = open(slave,O_RDWR|O_NONBLOCK);
 SCREEN* scr = newterm(NULL,scrIn,scrOut);

Works if replaced with (error checking omitted):

 setsid();
 close(STDIN_FILENO);
 close(STDOUT_FILENO);
 const char* slave_pts = pstname(master);
 int slave = open(slave_pts, O_RDWR);
 ioctl(slave(TIOCTTY,0);
 close(master);
 dup2(slave,STDIN_FILENO);
 dup2(slave,STDOUT_FILENO);
 FILE* slaveFile = fdopen(slavefd,"r+");
 SCREEN* scr = newterm(NULL,slaveFile,slaveFile);
 (void)set_term(scr);
 printw("hello worldn"); // print to the in memory represenation of the curses window
refresh(); // copy the in mem rep to the actual terminal

I think a bad file or file descriptor must have crept through somewhere without being checked. This explains the segfault inside fileno_unlocked(). Also I had tried in some experiments opening the slave twice. Once for reading and once for writing. The mode would have conflicted with the mode of the original fd.

Without setsid() on the child side (with the slave pty) the child process still has the original controlling terminal.

  • setsid() makes the process a session leader. Only the session leader can change its controlling terminal.
  • ioctl(slave(TIOCTTY,0) – make slave the controlling terminal

You need to be careful with ncurses calls when using newterm() rather tha initscr()

Many ncurses functions have an implicit “intscr” argument which refers to a screen or window created for the controlling terminals STDIN and STDOUT. They doen’t work unless replaced with the equivalent ncurses() functions for a specified WINDOW. You need to call newwin() to create a WINDOW, newterm() only gives you a screen.

In fact I am still wrestling with this kind of issue such as a call to subwin() which fails when the slave pty is used but not with the normal terminal.


It is also noteworthy that:

  • You need to handle SIGWINCH in the process connected to an actual terminal and pass that to the slave if it needs to knows the terminal size has changed.

  • You probably need a pipe to daemon to pass additional information.

  • I left stderr connect to the original terminal above for convenience of debugging. That would be closed in practice.

attach a terminal to a process running as a daemon (to run an ncurses UI) does a better job of describing the use case than the specific issues troubleshooted here.

User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement