Ncurses newterm после openpty

Я пытаюсь понять, как сделать следующее:

  1. создать новый псевдотерминал

  2. откройте экран ncurses, работающий внутри (подчиненного) псевдотерминала

  3. вилка

  4. A) перенаправить ввод-вывод с терминала, в котором выполняется программа (bash), на новый (подчиненный) терминал ИЛИ

    B) выйти, оставив программу ncurses запущенной в новом pty.

Может ли кто-нибудь предоставить указатели на то, что я мог делать неправильно, или это имело бы смысл в этом или даже лучше в примере программы, использующей newterm() с posix_openpt(), openpty() или forkpty().

Код, который у меня есть, примерно (детали упрощены или опущены):

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

}

У меня есть различные процедуры отладки, записывающие результаты от родительского и дочернего элементов в файлы.

Есть несколько вещей, связанных с терминалами, которых я не понимаю. Я видел несколько вариантов поведения, которые я не понимаю, в зависимости от того, какие варианты я пробую.

Вещи, которые я не понимаю:

  • Если я приказываю родительскому процессу выйти, дочерний процесс завершается, и дочерний процесс не регистрирует ничего интересного.

  • Если я попытаюсь закрыть stdin, stdout и использовать dup() или dup2(), чтобы сделать pty заменой stdin окно curses использует исходный stdin и stdout и использует исходный pty, а не новый, основанный на выводе ptsname(). (родительский процесс успешно выполняет ввод-вывод с дочерним, но в терминале он был запущен не из нового pty)

  • Если я открою новый pty с помощью open(), я получу segfault внутри вызова ncurses newterm(), как показано ниже:

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

Я пытаюсь понять системные вызовы pty здесь. Использование такой программы, как screen или tmux, не помогает в этом (также источник недостаточно аннотирован, чтобы заполнить пробелы в моем понимании).

Некоторые другие данные:

  • Я ориентируюсь на GNU/Linux

  • Я также пытался использовать forkpty

  • Я просмотрел исходный код для openpty, forkpty, login_tty, openpt, grantpt и posix_openpt.

    (например, https://github.com/coreutils/gnulib/blob/master/lib/posix_openpt.c)

  • У меня нет доступа к копии APUE хотя я посмотрел пример pty.

  • Хотя в документации ncurses для newterm() упоминается одновременное взаимодействие с несколькими терминалами, я не нашел примера программы, которая это делает.

Мне до сих пор непонятно:

  • что на самом деле делают login_tty/grantpt.

    Если вы открыли pty сами, почему бы вам уже не иметь нужных возможностей?

  • почему я предпочитаю openpty posix_openpt или наоборот.


Примечание. Это вопрос, отличный от вопроса прикрепить-терминал-к-процессу, работающему в качестве демона, для запуска-ncurses-ui, который описывает вариант использования и ищет решение. где этот вопрос предполагает конкретную, но неправильную/неполную реализацию для этого варианта использования.

Смотрите также stackoverflow.com/questions/65175134/what-can-you-do-with-a-‌​pty

Bruce Adams 14.12.2020 02:34

Программа то же самое в примерах ncurses открывает несколько xterms. Учебники не по теме.

Thomas Dickey 14.12.2020 10:11

Я не понимаю, как использовать то же самое. У меня есть openpty, но для него все еще требуется параметр. Если я даю ему случайную строку, такую ​​как «A», я получаю segfault от запуска процесса «xterm -S/dev/pts/25/4 -title A».

Bruce Adams 18.12.2020 11:35

Это должно работать; последнее исправление ошибки в этой области было 6 лет назад.

Thomas Dickey 18.12.2020 21:51

Что касается интерфейсов, то единого pty API, который работал бы везде, не существует. xterm имеет несколько вариантов, как и luit. Вы можете найти источник luit немного легче следовать. Но то же самое является наиболее подходящим примером для этого вопроса.

Thomas Dickey 18.12.2020 21:56
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
1 079
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Давайте рассмотрим одну из возможных реализаций pseudoterminal_run(), которая создает новый псевдотерминал, разветвляет дочерний процесс для работы с этим псевдотерминалом в качестве управляющего терминала со стандартным вводом, выводом и ошибкой, направленными на этот псевдотерминал, и выполняет указанный двоичный файл.

Вот заголовочный файл псевдотерминала.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 */

Вот соответствующая реализация, псевдотерминал.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;
}

Чтобы обнаружить ошибки в дочернем процессе до выполнения двоичного файла (включая ошибки при выполнении двоичного файла), в приведенном выше примере используется канал закрытия при выполнении между дочерним процессом и родителем для передачи ошибок. В случае успеха конец записи канала закрывается ядром, когда начинается выполнение нового двоичного файла. В противном случае приведенное выше является простой реализацией.

В частности:

  • posix_openpt(O_RDWR | O_NOCTTY) создает пару псевдотерминалов и возвращает дескриптор для главной стороны. Флаг O_NOCTTY используется, потому что мы не хотим, чтобы текущий процесс имел этот псевдотерминал в качестве управляющего терминала.

  • в дочернем процессе setsid() используется для запуска нового сеанса с идентификатором сеанса и идентификатором группы процессов, совпадающим с идентификатором дочернего процесса. Таким образом, родительский процесс может, например, отправить сигнал каждому процессу в этой группе; и когда дочерний процесс открывает подчиненную сторону псевдотерминала, он должен стать управляющим терминалом для дочернего процесса. (Код выполняет ioctl(slave_fd, TIOCSCTTY, 0), чтобы гарантировать, что если TIOCSCTTY определен.)

  • grantpt(masterfd) изменяет пользователя-владельца подчиненного псевдотерминала, чтобы он соответствовал текущему реальному пользователю, так что только текущий реальный пользователь (и привилегированные пользователи, такие как root) могут получить доступ к подчиненной стороне псевдотерминала.

  • unlockpt(masterfd) разрешает доступ к подчиненной стороне псевдотерминала. Она должна быть вызвана до открытия ведомой стороны.

  • slavefd = ioctl(masterfd, TIOCGPTPEER, O_RDWR) используется для открытия псевдотерминала подчиненной стороны, если он доступен. Если он недоступен или не работает, вместо него используется slavefd = open(ptsname(masterfd), O_RDWR).

В следующем примере example.c используется приведенный выше псевдотерминал.h, который запускает указанный двоичный файл на новом псевдотерминале, передавая данные между псевдотерминалом дочернего процесса и терминалом родительского процесса. Он записывает все операции чтения и записи в файл журнала, который вы указываете в качестве первого параметра командной строки. Остальные параметры командной строки формируют выполнение команды в дочернем процессе.

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

Всякий раз, когда родительский процесс получает сигнал WINCH (изменение размера окна), новый размер окна терминала получается из родительского терминала, а затем устанавливается для дочернего псевдотерминала.

Для простоты (и не предоставляя код, который можно использовать как есть) в примере предпринимаются попытки неблокирующего чтения и записи, когда это возможно, и только опрос (ожидание, пока ввод станет доступным или буферизованные данные могут быть записаны), если все четыре не работают. Также, если буферы пусты, он копирует настройки терминала с дочернего псевдотерминала на родительский терминал.

Скомпилируйте, используя, например.

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

и запустить, например. ./example nano.log nano test-file. Это запускается nano в суб-псевдотерминале, отражая все в нем на родительском терминале, и по существу действует так, как если бы вы просто запустили nano test-file. (Нажмите Ctrl+X, чтобы выйти.) Однако каждое чтение и запись регистрируются в файле nano.log. Для простоты в настоящее время регистрируется только длина, но вы, безусловно, можете написать функцию дампера, чтобы также регистрировать содержимое. (Поскольку они содержат управляющие символы, вам нужно либо экранировать все управляющие символы, либо вывести данные в шестнадцатеричном формате.)

Интересно отметить, что когда дочерний процесс (последний процесс с псевдотерминалом в качестве управляющего терминала) завершается, попытка чтения с главного псевдотерминала возвращает -1 с errno == EIO. Это означает, что прежде чем рассматривать это как фатальную ошибку, следует собрать процессы в группе дочерних процессов (waitpid(-child_pid, &status, WNOHANG)); и если это возвращает -1 с errno = ECHILD, это означает, что EIO был вызван тем, что ни один процесс не открыл подчиненный псевдотерминал.

Если мы сравним это с tmux или screen, мы реализовали только грубую версию части, когда она «привязана» к работающему сеансу. Когда пользователь (родительский процесс, работающий в родительском терминале) «отсоединяется» от сеанса, tmux и screen покидают процесс, собирающий выходные данные запущенной команды. (Они не просто буферизуют все, они имеют тенденцию записывать эффекты выполняемой команды в буфер виртуального терминала — строки × столбцы, массив печатных символов и их атрибутов — так что для восстановления требуется ограниченный/фиксированный объем памяти. содержимое терминала при повторном подключении к нему позже.)

При повторном подключении к сеансу команда screen/tmux подключается к существующему процессу (обычно с использованием доменного сокета Unix, что позволяет проверить идентификатор однорангового пользователя, а также передавать дескриптор (хозяину псевдотерминала) между процессами, поэтому новый процесс может занять место старого процесса, а старый процесс может завершиться.

Если мы установим переменную среды TERM в значение xterm-256color перед выполнением дочернего двоичного файла, мы сможем интерпретировать все, что мы читаем с главной стороны псевдотерминала, с точки зрения того, как работает 256-цветный xterm, и, например. нарисуйте экран, используя, например. GTK+ — именно так мы бы написали собственный эмулятор терминала.

Это очень хороший ответ, но на несколько другой вопрос. В вашем ответе нет смысла использовать ncurses. Вместо этого вы выполняете новый процесс. Хотя должно работать так же.

Bruce Adams 17.12.2020 16:48

Зачем использовать семейство вызовов posix_openpt, а не openpty и login_tty? Это еще один выбор, который мне неясен.

Bruce Adams 17.12.2020 16:49

Я попробовал ваш пример, но заменил подчиненный exec на newterm (NULL, STDIN_FILENO, STDOUT_FILENO). Я получил segfault в fileno_unlocked(), как я описал в своем вопросе.

Bruce Adams 18.12.2020 11:08

newterm() завершается успешно, если вместо этого я использую подчиненный FD. Однако printw("hello\n") ничего не делает. Если заменить newterm() на fprintf(slave,"hello\n") или fprintf(STDOUT_FILENO, "hello\n"), я получаю segfault в fwrite(). Это проблемы, которые я на самом деле пытаюсь понять.

Bruce Adams 18.12.2020 11:20

Рождественская награда за приложенные усилия, хотя у меня еще нет ответа в голове.

Bruce Adams 25.12.2020 01:57

Является ли этот шаг «Убедитесь, что мастер не затеняет стандартные потоки». действительно необходимо? При каких обстоятельствах новому pty может быть присвоен тот же fd, что и stdin, stdout или stderr? Конечно, это могло произойти только в том случае, если они были намеренно закрыты приложением?

Bruce Adams 29.12.2020 10:32

Я пытаюсь понять, как сделать следующее:

  1. создать новый псевдотерминал

  2. откройте экран ncurses, работающий внутри (подчиненного) псевдотерминала

  3. вилка

  4. A) перенаправить ввод-вывод с терминала, в котором выполняется программа (bash), на новый (подчиненный) терминал ИЛИ

    B) выйти, оставив программу ncurses запущенной в новом pty.

Похоже, у вас есть фундаментальное заблуждение о парах псевдотерминалов и особенно о важности процесса, являющегося мастером псевдотерминала. Без ведущего и процесса, управляющего ведущей стороной, пары псевдотерминалов буквально не существует: когда ведущее устройство закрывается, ядро ​​также принудительно удаляет ведомое устройство, делая недействительными файловые дескрипторы, открытые ведомым устройством для ведомой стороны пары псевдотерминалов.

Выше вы полностью игнорируете роль мастера и удивляетесь, почему то, что вы хотите, не работает.

Мой ответ показывает выполнение 4.A), когда любой двоичный файл работает как ведомый, а сама программа является ведущей, передавая данные между ведомым псевдотерминалом и главным терминалом.

Обратную роль, когда ваша «основная программа» указывает другому двоичному файлу быть главным терминалом, очень просто: напишите свою собственную «основную программу» как обычную программу ncurses, но запустите ее, используя мой пример программы для управления главной стороной терминала. псевдотерминальная пара. Таким образом, распространение сигнала и так далее работает корректно.

Если вы хотите поменяться ролями, при этом ведомый псевдотерминал является родительским процессом, а мастер псевдотерминала — дочерним процессом, вам нужно точно объяснить, почему, когда весь интерфейс был разработан для противоположного.

Нет, не существует «просто универсальной основной псевдотерминальной программы или библиотеки, которую вы можете использовать для этого». Причина в том, что в этом нет смысла. Всякий раз, когда вам нужна пара псевдотерминалов, мастер — это причина, по которой вам это нужно. Любой стандартный поток, использующий удобочитаемый текст, производящий или потребляющий программу, является действительным клиентом, использующим ведомый конец. Они не важны, важен только хозяин.

Может ли кто-нибудь дать указания на то, что я могу делать неправильно, или это имело бы смысл в некоторых из этих

Я пробовал, но ты не оценил усилий. Я сожалею, что пытался.

или, что еще лучше, пример программы, использующей newterm() с posix_openpt(), openpty() или forkpty().

Нет, потому что ваш newterm() не имеет абсолютно никакого смысла.

Вы правы в том, что у меня есть фундаментальные заблуждения, поэтому я спрашиваю здесь. Но почему один и тот же процесс не может породить ведущего и ведомого? Запускаю ли я execp или продолжаю процесс в той же кодовой базе, не должно иметь значения. Я не вижу причин, по которым newterm() не должен работать.

Bruce Adams 19.12.2020 14:11

Самая близкая к подходящей библиотеке, на которую мне указали, это libvterm.

Bruce Adams 19.12.2020 14:12

Вы подтвердили, что закрытие мастера завершает работу терминала. Я подозревал, что да, но не был уверен. Я ценю усилия, но не могу принять ответ, пока не докажу себе, что понимаю, что происходит иначе.

Bruce Adams 19.12.2020 14:22

Да, libvterm — это один из способов, с помощью которого главная сторона может интерпретировать различные escape-коды (которые клиент записывает на подчиненную сторону, которыми, возможно, манипулирует слой termios, а приложение, использующее libvterm, читает с главной стороны) и «рисовать» их в текст на основе фреймбуфера. Вы можете использовать libvterm для реализации собственного эмулятора терминала.

Glärbo 19.12.2020 15:30

@BruceAdams: Мое разочарование связано с тем, что я не могу понять то, чего не понимаешь ты. Рассмотрение практического примера должно (я думал!) быть эффективным способом прояснить и выяснить, на какие вопросы вы ищете ответы, но это не сработало. Это проблема со связью, она не имеет ничего общего со способностями или интеллектом, но я сам не очень хорош в общении. Не могли бы вы представить реальный вариант использования с двумя сторонами: одна (дочерняя) работает в псевдотерминале, а другая (родительская) управляет псевдотерминалом?

Glärbo 19.12.2020 15:34

@BruceAdams: важной частью является НЕ дочерний элемент, работающий в псевдотерминале, потому что это нормальное состояние и действия; важной частью является то, что родительская, главная сторона псевдотерминала делает для предоставления псевдотерминала. Я пытался ломать голову над тем, как найти пример, который поможет вам задавать вопросы, на которые можно ответить, но я не знаю, как это сделать. Должен ли я просто отредактировать свой пример, чтобы вместо дочернего процесса сеанс ncurses просто повторял ввод пользователя, пока пользователь не нажмет клавишу (возможно, .)? Основная сторона по-прежнему будет просто проксировать родительский терминал, поэтому ввод-вывод будет виден.

Glärbo 19.12.2020 15:37

@BruceAdams: Основная причина, по которой подчиненная сторона псевдотерминала неинтересна, заключается в том, что если она реализована правильно, процесс, работающий на псевдотерминале, не видит абсолютно никакой разницы в поведении по сравнению с работой в «настоящем» терминале. Все интересное происходит на мастере. Кроме того, нам нужно разветвить дочерний процесс для запуска на этом псевдотерминале (подчиненном конце), потому что иначе мы не сможем начать новый сеанс с псевдотерминалом в качестве управляющего терминала. На самом деле нет смысла иметь обе стороны в одном и том же процессе; Я даже не уверен, что это может работать.

Glärbo 19.12.2020 16:24

У моего исходного вопроса был вариант использования, но он был закрыт из-за отсутствия внимания из-за того, что я спросил о нескольких вещах, которые я не понимаю. Я воссоздал его здесь - stackoverflow.com/questions/65375849/… Я попытался создать более узкие вопросы, на которые вы пытались ответить, но, похоже, я все еще не могу понять некоторые основы.

Bruce Adams 20.12.2020 01:39

@BruceAdams: Думаю, теперь я могу понять.

Glärbo 20.12.2020 04:19
Ответ принят как подходящий

Ответы Глэрбо помогли мне понять проблемы настолько, что после некоторых экспериментов я верю, что смогу напрямую ответить на оставшиеся вопросы.

Важными моментами являются:

  • Главная сторона pty должна оставаться открытой
  • Файловый дескриптор подчиненного устройства должен быть открыт в том же режиме, в котором он был создан изначально.
  • без setsid() на подчиненном устройстве он остается подключенным к исходному управляющему терминалу.
  • Вы должны быть осторожны с вызовами ncurses при использовании newterm, а не initscr

Главная сторона pty должна оставаться открытой

Я: «Если я даю указание родительскому процессу выйти, дочерний процесс завершается, и дочерний процесс не регистрирует ничего интересного».

Глэрбо: «Без ведущего и процесса, управляющего ведущей стороной, буквально не существует пары псевдотерминалов: когда ведущее устройство закрывается, ядро ​​также принудительно удаляет ведомое устройство, делая недействительными файловые дескрипторы, открытые подчиненным устройством для ведомой стороны. псевдотерминальная пара».

Файловый дескриптор подчиненного устройства должен быть открыт в том же режиме, в котором он был создан изначально.

Мой неверный псевдокод (для дочерней стороны форка):

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

Работает, если заменить на (проверка ошибок опущена):

 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 world\n"); // print to the in memory represenation of the curses window
refresh(); // copy the in mem rep to the actual terminal

Я думаю, что плохой файл или файловый дескриптор, должно быть, прокрался куда-то без проверки. Это объясняет segfault внутри fileno_unlocked(). Также я пробовал в некоторых экспериментах дважды открывать раб. Один раз для чтения и один раз для записи. Режим противоречил бы режиму исходного файла fd.

Без setsid() на дочерней стороне (с ведомым pty) дочерний процесс по-прежнему имеет исходный управляющий терминал.

  • setsid() делает процесс лидером сеанса. Только лидер сеанса может изменить свой управляющий терминал.
  • ioctl(slave(TIOCTTY,0) - сделать подчиненным управляющий терминал

Вы должны быть осторожны с вызовами ncurses при использовании newterm(), а не initscr()

Многие функции ncurses имеют неявный аргумент «intscr», который относится к экрану или окну, созданному для управляющих терминалов STDIN и STDOUT. Они не работают, если их не заменить эквивалентными функциями ncurses() для указанного WINDOW. Вам нужно вызвать newwin() для создания ОКНА, newterm() дает вам только экран.

На самом деле я все еще борюсь с такими проблемами, как вызов subwin(), который терпит неудачу, когда используется ведомый pty, но не с обычным терминалом.


Примечательно также, что:

  • Вам нужно обработать SIGWINCH в процессе, подключенном к реальному терминалу, и передать его подчиненному, если ему нужно знать, что размер терминала изменился.

  • Вероятно, вам нужен канал для демона для передачи дополнительной информации.

  • Я оставил подключение stderr к исходному терминалу выше для удобства отладки. Это было бы закрыто на практике.

прикрепите терминал к процессу, работающему как демон (для запуска пользовательского интерфейса ncurses) лучше описывает вариант использования, чем конкретные проблемы, устраняемые здесь.

Другие вопросы по теме