ncurses newterm following openpty

依然范特西╮ 提交于 2020-12-27 06:16:20

问题


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.


回答1:


Let's look at one possible implementation of pseudoterminal_run(), which creates a new pseudoterminal, forks a child process to run with that pseudoterminal as the controlling terminal with standard input, output, and error directed to that pseudoterminal, and executes a specified binary.

Here's the header file, pseudoterminal.h:

#ifndef   PSEUDOTERMINAL_H
#define   PSEUDOTERMINAL_H

int pseudoterminal_run(pid_t *const,        /* Pointer to where child process ID (= session and process group ID also) is saved */
                       int   *const,        /* Pointer to where pseudoterminal master descriptor is saved */
                       const char *const,   /* File name or path of binary to be executed */
                       char *const [],      /* Command-line arguments to binary */
                       const struct termios *const,  /* NULL or pointer to termios settings for the pseudoterminal */
                       const struct winsize *const); /* NULL or pointer to pseudoterminal size */

#endif /* PSEUDOTERMINAL_H */

Here is the corresponding implementation, pseudoterminal.c:

#define  _POSIX_C_SOURCE  200809L
#define  _XOPEN_SOURCE    600
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <termios.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

/* Helper function: Moves fd so that it does not overlap standard streams.
 *                  If an error occurs, will close fd.
*/
static int  not_stdin_stdout_stderr(int fd)
{
    unsigned int  close_mask = 0;

    if (fd == -1) {
        errno = EBADF;
        return -1;
    }

    while (1) {
        if (fd == STDIN_FILENO)
            close_mask |= 1;
        else
        if (fd == STDOUT_FILENO)
            close_mask |= 2;
        else
        if (fd == STDERR_FILENO)
            close_mask |= 4;
        else
            break;

        fd = dup(fd);
        if (fd == -1) {
            const int  saved_errno = errno;
            if (close_mask & 1) close(STDIN_FILENO);
            if (close_mask & 2) close(STDOUT_FILENO);
            if (close_mask & 4) close(STDERR_FILENO);
            errno = saved_errno;
            return -1;
        }
    }

    if (close_mask & 1) close(STDIN_FILENO);
    if (close_mask & 2) close(STDOUT_FILENO);
    if (close_mask & 4) close(STDERR_FILENO);

    return fd;
}


static int run_slave(int                   master,
                     const char *          binary,
                     char *const           args[],
                     const struct termios *termp,
                     const struct winsize *sizep)
{
    int  slave;

    /* Close standard streams. */
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    /* Fix ownership and permissions for the slave side. */
    if (grantpt(master) == -1)
        return errno;

    /* Unlock the pseudoterminal pair */
    if (unlockpt(master) == -1)
        return errno;

    /* Obtain a descriptor to the slave end of the pseudoterminal */
    do {

#if defined(TIOCGPTPEER)
        slave = ioctl(master, TIOCGPTPEER, O_RDWR);
        if (slave == -1) {
            if (errno != EINVAL &&
#if defined(ENOIOCTLCMD)
                errno != ENOIOCTLCMD &&
#endif
                errno != ENOSYS)
                return errno;
        } else
            break;
#endif

        const char *slave_pts = ptsname(master);
        if (!slave_pts)
            return errno;
        slave = open(slave_pts, O_RDWR);
        if (slave == -1)
            return errno;
        else
            break;

    } while (0);

#if defined(TIOCSCTTY)
    /* Make sure slave is our controlling terminal. */
    ioctl(slave, TIOCSCTTY, 0);
#endif

    /* Master is no longer needed. */
    close(master);

    /* Duplicate slave to standard streams. */
    if (slave != STDIN_FILENO)
        if (dup2(slave, STDIN_FILENO) == -1)
            return errno;
    if (slave != STDOUT_FILENO)
        if (dup2(slave, STDOUT_FILENO) == -1)
            return errno;
    if (slave != STDERR_FILENO)
        if (dup2(slave, STDERR_FILENO) == -1)
            return errno;

    /* If provided, set the termios settings. */
    if (termp)
        if (tcsetattr(STDIN_FILENO, TCSANOW, termp) == -1)
            return errno;

    /* If provided, set the terminal window size. */
    if (sizep)
        if (ioctl(STDIN_FILENO, TIOCSWINSZ, sizep) == -1)
            return errno;

    /* Execute the specified binary. */
    if (strchr(binary, '/'))
        execv(binary, args);    /* binary is a path */
    else
        execvp(binary, args);   /* binary is a filename */

    /* Failed! */
    return errno;
}


/* Internal exit status used to verify child failure. */
#ifndef  PSEUDOTERMINAL_EXIT_FAILURE
#define  PSEUDOTERMINAL_EXIT_FAILURE  127
#endif

int pseudoterminal_run(pid_t *const                 childp,
                       int   *const                 masterp,
                       const char *const            binary,
                       char *const                  args[],
                       const struct termios *const  termp,
                       const struct winsize *const  sizep)
{
    int    control[2] = { -1, -1 };
    int    master;
    pid_t  child;

    int         cause;
    char *const cause_end = (char *)(&cause) + sizeof cause;
    char       *cause_ptr = (char *)(&cause);

    /* Verify required parameters exist. */
    if (!childp || !masterp || !binary || !*binary || !args || !args[0]) {
        errno = EINVAL;
        return -1;
    }

    /* Acquire a new pseudoterminal */
    master = posix_openpt(O_RDWR | O_NOCTTY);
    if (master == -1)
        return -1;

    /* Make sure master does not shadow standard streams. */
    master = not_stdin_stdout_stderr(master);
    if (master == -1)
        return -1;

    /* Control pipe passes exec error back to this process. */
    if (pipe(control) == -1) {
        const int  saved_errno = errno;
        close(master);
        errno = saved_errno;
        return -1;
    }

    /* Write end of the control pipe must not shadow standard streams. */
    control[1] = not_stdin_stdout_stderr(control[1]);
    if (control[1] == -1) {
        const int  saved_errno = errno;
        close(control[0]);
        close(master);
        errno = saved_errno;
        return -1;
    }

    /* Write end of the control pipe must be close-on-exec. */
    if (fcntl(control[1], F_SETFD, FD_CLOEXEC) == -1) {
        const int  saved_errno = errno;
        close(control[0]);
        close(control[1]);
        close(master);
        errno = saved_errno;
        return -1;
    }

    /* Fork the child process. */
    child = fork();
    if (child == -1) {
        const int  saved_errno = errno;
        close(control[0]);
        close(control[1]);
        close(master);
        errno = saved_errno;
        return -1;
    } else
    if (!child) {
        /*
         * Child process
        */

        /* Close read end of control pipe. */
        close(control[0]);

        /* Note: This is the point where one would change real UID,
                 if one wanted to change identity for the child process. */

        /* Child runs in a new session. */
        if (setsid() == -1)
            cause = errno;
        else
            cause = run_slave(master, binary, args, termp, sizep);

        /* Pass the error back to parent process. */
        while (cause_ptr < cause_end) {
            ssize_t  n = write(control[1], cause_ptr, (size_t)(cause_end - cause_ptr));
            if (n > 0)
                cause_ptr += n;
            else
            if (n != -1 || errno != EINTR)
                break;
        }
        exit(PSEUDOTERMINAL_EXIT_FAILURE);
    }

    /*
     * Parent process
    */

    /* Close write end of control pipe. */
    close(control[1]);

    /* Read from the control pipe, to see if child exec failed. */
    while (cause_ptr < cause_end) {
        ssize_t  n = read(control[0], cause_ptr, (size_t)(cause_end - cause_ptr));
        if (n > 0) {
            cause_ptr += n;
        } else
        if (n == 0) {
            break;
        } else
        if (n != -1) {
            cause = EIO;
            cause_ptr = cause_end;
            break;
        } else
        if (errno != EINTR) {
            cause = errno;
            cause_ptr = cause_end;
        }
    }

    /* Close read end of control pipe as well. */
    close(control[0]);

    /* Any data received indicates an exec failure. */
    if (cause_ptr != (const char *)(&cause)) {
        int    status;
        pid_t  p;

        /* Partial error report is an I/O error. */
        if (cause_ptr != cause_end)
            cause = EIO;

        /* Make sure the child process is dead, and reap it. */
        kill(child, SIGKILL);
        do {
            p = waitpid(child, &status, 0);
        } while (p == -1 && errno == EINTR);

        /* If it did not exit with PSEUDOTERMINAL_EXIT_FAILURE, cause is I/O error. */
        if (!WIFEXITED(status) || WEXITSTATUS(status) != PSEUDOTERMINAL_EXIT_FAILURE)
            cause = EIO;

        /* Close master pseudoterminal. */
        close(master);

        errno = cause;
        return -1;
    }

    /* Success. Save master fd and child PID. */
    *masterp = master;
    *childp = child;
    return 0;
}

To detect errors in the child process before the binary is executed (including errors in executing a binary), the above uses a close-on-exec pipe between the child and the parent to pass errors. In the success case, the pipe write end is closed by the kernel when execution of a new binary starts. Otherwise the above is a straightforward implementation.

In particular:

  • posix_openpt(O_RDWR | O_NOCTTY) creates the pseudoterminal pair, and returns the descriptor for the master side. The O_NOCTTY flag is used because we do not want the current process to have that pseudoterminal as the controlling terminal.

  • in the child process, setsid() is used to start a new session, with both session ID and process group ID matching the child process ID. This way, the parent process can for example send a signal to each process in that group; and when the child opens the pseudoterminal slave side, it should become the controlling terminal for the child process. (The code does do an ioctl(slave_fd, TIOCSCTTY, 0) to ensure that, if TIOCSCTTY is defined.)

  • grantpt(masterfd) changes the owner user of the slave pseudoterminal to match current real user, so that only the current real user (and privileged users like root) can access the slave side of the pseudoterminal.

  • unlockpt(masterfd) allows access to the slave side of the pseudoterminal. It must be called before the slave side can be opened.

  • slavefd = ioctl(masterfd, TIOCGPTPEER, O_RDWR) is used to open the slave side pseudoterminal if available. If not available, or it fails, then slavefd = open(ptsname(masterfd), O_RDWR) is used instead.

The following example.c is an example using the above pseudoterminal.h, which runs a specified binary in a new pseudoterminal, proxying the data between the child process pseudoterminal and the parent process terminal. It logs all reads and writes to a log file you specify as the first command line parameter. The rest of the command line parameters form the command run in the child process.

#define  _POSIX_C_SOURCE  200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
#include <termios.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include "pseudoterminal.h"

static struct termios  master_oldterm, master_newterm, slave_newterm;
static struct winsize  slave_size;

static int    tty_fd = -1;
static int    master_fd = -1;

static void  handle_winch(int signum)
{
    /* Silence warning about signum not being used. */
    (void)signum;

    if (tty_fd != -1 && master_fd != -1) {
        const int       saved_errno = errno;
        struct winsize  temp_size;
        if (ioctl(tty_fd, TIOCGWINSZ, &temp_size) == 0)
            if (ioctl(master_fd, TIOCSWINSZ, &temp_size) == 0)
                slave_size = temp_size;
        errno = saved_errno;
    }
}

static int  install_winch(void)
{
    struct sigaction  act;
    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    act.sa_handler = handle_winch;
    act.sa_flags = SA_RESTART;
    return sigaction(SIGWINCH, &act, NULL);
}

int main(int argc, char *argv[])
{
    pid_t  child_pid = 0;
    int    child_status = 0;
    FILE  *log = NULL;


    if (argc < 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        const char *argv0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv0);
        fprintf(stderr, "       %s LOGFILE COMMAND [ ARGS ... ]\n", argv0);
        fprintf(stderr, "\n");
        fprintf(stderr, "This program runs COMMAND in a pseudoterminal, logging all I/O\n");
        fprintf(stderr, "to LOGFILE, and proxying them to the current terminal.\n");
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }

    if (isatty(STDIN_FILENO))
        tty_fd = STDIN_FILENO;
    else
    if (isatty(STDOUT_FILENO))
        tty_fd = STDOUT_FILENO;
    else
    if (isatty(STDERR_FILENO))
        tty_fd = STDERR_FILENO;
    else {
        fprintf(stderr, "This program only runs in a terminal or pseudoterminal.\n");
        return EXIT_FAILURE;
    }

    if (tcgetattr(tty_fd, &master_oldterm) == -1) {
        fprintf(stderr, "Cannot obtain termios settings: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    if (ioctl(tty_fd, TIOCGWINSZ, &slave_size) == -1) {
        fprintf(stderr, "Cannot obtain terminal window size: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    if (install_winch() == -1) {
        fprintf(stderr, "Cannot install SIGWINCH signal handler: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    /* For our own terminal, we want RAW (nonblocking) I/O. */
    memcpy(&master_newterm, &master_oldterm, sizeof (struct termios));
    master_newterm.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
    master_newterm.c_oflag &= ~OPOST;
    master_newterm.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    master_newterm.c_cflag &= ~(CSIZE | PARENB);
    master_newterm.c_cflag |= CS8;
    master_newterm.c_cc[VMIN] = 0;
    master_newterm.c_cc[VTIME] = 0;

    /* We'll use the same for the new terminal also. */
    memcpy(&slave_newterm, &master_newterm, sizeof (struct termios));

    /* Open log file */
    log = fopen(argv[1], "w");
    if (!log) {
        fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
        return EXIT_FAILURE;
    }

    /* Execute binary in pseudoterminal */
    if (pseudoterminal_run(&child_pid, &master_fd, argv[2], argv + 2, &slave_newterm, &slave_size) == -1) {
        fprintf(stderr, "%s: %s.\n", argv[2], strerror(errno));
        return EXIT_FAILURE;
    }

    fprintf(log, "Pseudoterminal has %d rows, %d columns (%d x %d pixels)\n",
                 slave_size.ws_row, slave_size.ws_col, slave_size.ws_xpixel, slave_size.ws_ypixel);
    fflush(log);

    /* Ensure the master pseudoterminal descriptor is nonblocking. */
    fcntl(tty_fd, F_SETFL, O_NONBLOCK);
    fcntl(master_fd, F_SETFL, O_NONBLOCK);

    /* Pseudoterminal proxy. */
    {
        struct pollfd  fds[2];

        const size_t   slavein_size = 8192;
        unsigned char  slavein_data[slavein_size];
        size_t         slavein_head = 0;
        size_t         slavein_tail = 0;

        const size_t   slaveout_size = 8192;
        unsigned char  slaveout_data[slaveout_size];
        size_t         slaveout_head = 0;
        size_t         slaveout_tail = 0;

        while (1) {
            int  io = 0;

            if (slavein_head < slavein_tail) {
                ssize_t  n = write(master_fd, slavein_data + slavein_head, slavein_tail - slavein_head);
                if (n > 0) {
                    slavein_head += n;
                    io++;
                    fprintf(log, "Wrote %zd bytes to child pseudoterminal.\n", n);
                    fflush(log);
                } else
                if (n != -1) {
                    fprintf(log, "Error writing to child pseudoterminal: write() returned %zd.\n", n);
                    fflush(log);
                } else
                if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
                    fprintf(log, "Error writing to child pseudoterminal: %s.\n", strerror(errno));
                    fflush(log);
                }
            }
            if (slavein_head > 0) {
                if (slavein_tail > slavein_head) {
                    memmove(slavein_data, slavein_data + slavein_head, slavein_tail - slavein_head);
                    slavein_tail -= slavein_head;
                    slavein_head  = 0;
                } else {
                    slavein_tail = 0;
                    slavein_head = 0;
                }
            }

            if (slaveout_head < slaveout_tail) {
                ssize_t  n = write(tty_fd, slaveout_data + slaveout_head, slaveout_tail - slaveout_head);
                if (n > 0) {
                    slaveout_head += n;
                    io++;
                    fprintf(log, "Wrote %zd bytes to parent terminal.\n", n);
                    fflush(log);
                } else
                if (n != -1) {
                    fprintf(log, "Error writing to parent terminal: write() returned %zd.\n", n);
                    fflush(log);
                } else
                if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
                    fprintf(log, "Error writing to parent terminal: %s.\n", strerror(errno));
                    fflush(log);
                }
            }
            if (slaveout_head > 0) {
                if (slaveout_tail > slaveout_head) {
                    memmove(slaveout_data, slaveout_data + slaveout_head, slaveout_tail - slaveout_head);
                    slaveout_tail -= slaveout_head;
                    slaveout_head  = 0;
                } else {
                    slaveout_tail = 0;
                    slaveout_head = 0;
                }
            }

            if (slavein_tail < slavein_size) {
                ssize_t  n = read(tty_fd, slavein_data + slavein_tail, slavein_size - slavein_tail);
                if (n > 0) {
                    slavein_tail += n;
                    io++;
                    fprintf(log, "Read %zd bytes from parent terminal.\n", n);
                    fflush(log);
                } else
                if (!n) {
                    /* Ignore */
                } else
                if (n != -1) {
                    fprintf(log, "Error reading from parent terminal: read() returned %zd.\n", n);
                    fflush(log);
                } else
                if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
                    fprintf(log, "Error reading from parent terminal: %s.\n", strerror(errno));
                    fflush(log);
                }
            }

            if (slaveout_tail < slaveout_size) {
                ssize_t  n = read(master_fd, slaveout_data + slaveout_tail, slaveout_size - slaveout_tail);
                if (n > 0) {
                    slaveout_tail += n;
                    io++;
                    fprintf(log, "Read %zd bytes from child pseudoterminal.\n", n);
                    fflush(log);
                } else
                if (!n) {
                    /* Ignore */
                } else
                if (n != -1) {
                    fprintf(log, "Error reading from child pseudoterminal: read() returned %zd.\n", n);
                    fflush(log);
                } else
                if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
                    fprintf(log, "Error reading from child pseudoterminal: %s.\n", strerror(errno));
                    fflush(log);
                }
            }

            /* If we did any I/O, retry. */
            if (io > 0)
                continue;

            /* If child process has exited and its output buffer is empty, we're done. */
            if (child_pid <= 0 && slaveout_head >= slaveout_tail)
                break;

            /* Check if the child process has exited. */
            if (child_pid > 0) {
                pid_t  p = waitpid(child_pid, &child_status, WNOHANG);
                if (p == child_pid) {
                    child_pid = -child_pid;
                    continue;
                }
            }

            /* If both buffers are empty, we proxy also the termios settings. */
            if (slaveout_head >= slaveout_tail && slavein_head >= slavein_tail)
                if (tcgetattr(master_fd, &slave_newterm) == 0)
                    if (tcsetattr(tty_fd, TCSANOW, &slave_newterm) == 0)
                        master_newterm = slave_newterm;

            /* Wait for I/O to become possible. */

            /* fds[0] is parent terminal */
            fds[0].fd      = tty_fd;
            fds[0].events  = POLLIN | (slaveout_head < slaveout_tail ? POLLOUT : 0);
            fds[0].revents = 0;

            /* fds[1] is child pseudoterminal */
            fds[1].fd      = master_fd;
            fds[1].events  = POLLIN | (slavein_head < slaveout_head ? POLLOUT : 0);
            fds[1].revents = 0;

            /* Wait up to a second */
            poll(fds, 2, 1000);
        }
    }

    /* Report child process exit status to log. */
    if (WIFEXITED(child_status)) {
        if (WEXITSTATUS(child_status) == EXIT_SUCCESS)
            fprintf(log, "Child process exited successfully.\n");
        else
            fprintf(log, "Child process exited with exit status %d.\n", WEXITSTATUS(child_status));
    } else
    if (WIFSIGNALED(child_status))
        fprintf(log, "Child process died from signal %d.\n", WTERMSIG(child_status));
    else
        fprintf(log, "Child process lost.\n");
    fflush(log);
    fclose(log);

    /* Discard pseudoterminal. */
    close(master_fd);

    /* Return original parent terminal settings. */
    tcflush(tty_fd, TCIOFLUSH);
    tcsetattr(tty_fd, TCSANOW, &master_oldterm);

    return EXIT_SUCCESS;
}

Whenever the parent process receives a WINCH (window size change) signal, the new terminal window size is obtained from the parent terminal, then set to the child pseudoterminal.

For simplicity (and not providing code that can be used as-is), the example attempts nonblocking reads and writes whenever possible, and only polls (waits until input becomes available, or buffered data can be written) if all four fail. Also, if the buffers are empty then, it copies the terminal settings from the child pseudoterminal to the parent terminal.

Compile using e.g.

gcc -Wall -Wextra -O2 -c pseudoterminal.c
gcc -Wall -Wextra -O2 -c example.c
gcc -Wall -Wextra -O2 example.o pseudoterminal.o -o example

and run e.g. ./example nano.log nano test-file. This runs nano in a sub-pseudoterminal, reflecting everything in it to the parent terminal, and essentially acts as if you had simply ran nano test-file. (Press Ctrl+X to exit.) However, every read and write is logged to the nano.log file. For simplicity, only the length is currently logged, but you can surely write a dumper function to also log the contents. (Because these contain control characters, you'll want to either escape all control characters, or dump the data in hexadecimal format.)

It is interesting to note that when the child process (last process with the pseudoterminal as their controlling terminal) exits, trying to read from the pseudoterminal master returns -1 with errno == EIO. This means that before treating that as a fatal error, one should reap processes in the child process process group (waitpid(-child_pid, &status, WNOHANG)); and if that returns -1 with errno = ECHILD, it means the EIO was caused by no process having the pseudoterminal slave open.

If we compare this to tmux or screen, we have implemented only a crude version of the part when "attached" to a running session. When the user (parent process, running in the parent terminal) "detaches" from a session, tmux and screen both leave a process collecting the output of the running command. (They do not just buffer everything, they tend to record the effects of the running command to a virtual terminal buffer – rows × columns array of printable glyphs and their attributes –, so that a limited/fixed amount of memory is needed to recover the terminal contents when re-attaching to it later on.)

When re-attaching to a session, the screen/tmux command connects to the existing process (usually using an Unix domain socket, which allows verifying the peer user ID, and also passing the descriptor (to the pseudoterminal master) between processes, so the new process can take the place of the old process, and the old process can exit.

If we set the TERM environment variable to say xterm-256color before executing the child binary, we could interpret everything we read from the pseudoterminal master side in terms of how 256-color xterm does, and e.g. draw the screen using e.g. GTK+ – that is how we'd write our own terminal emulator.




回答2:


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.

You seem to have a fundamental misconception of pseudoterminal pairs, and especially the importance of a process being the pseudoterminal master. 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.

Above, you completely ignore the role of the master, and wonder why what you want is not working.

My answer shows to accomplish 4.A), with any binary running as the slave, with the program itself being the master, proxying data between the slave pseudoterminal and the master terminal.

Reversing the role, with your "main program" telling some other binary to be the master terminal, is simple: write your own "main program" as a normal ncurses program, but run it using my example program to manage the master side of the pseudoterminal pair. This way signal propagation et cetera works correctly.

If you want to swap the roles, with the pseudoterminal slave being the parent process and the pseudoterminal master being the child process, you need to explain exactly why, when the entire interface has been designed for the opposite.

No, there is no "just a generic master pseudoterminal program or library you can use for this". The reason is that such makes no sense. Whenever you need a pseudoterminal pair, the master is the reason you want one. Any standard stream using human-readable text producing or consuming program is a valid client, using the slave end. They are not important, only the master is.

Can anyone provide pointers to what I might be doing wrong or that would make sense of some of this

I tried that, but you didn't appreciate the effort. I am sorry I tried.

or even better an example program using newterm() with either posix_openpt(), openpty() or forkpty().

No, because your newterm() makes absolutely no sense.



来源:https://stackoverflow.com/questions/65282503/ncurses-newterm-following-openpty

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!