Saya mencoba mencari cara untuk melakukan hal berikut:

  1. buat terminal semu baru

  2. buka layar ncurses yang berjalan di dalam terminal semu (slave)

  3. garpu

  4. A) meneruskan I/O dari terminal program berjalan di (bash) ke terminal (slave) baru ATAU

    B) keluar meninggalkan program ncurses berjalan di pty baru.

Adakah yang bisa memberikan petunjuk tentang apa yang mungkin saya lakukan salah atau yang akan masuk akal dari beberapa ini atau bahkan lebih baik contoh program menggunakan newterm() dengan posix_openpt(), openpty() atau forkpty().

Kode yang saya miliki kira-kira (detail disederhanakan atau dihilangkan):

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

}

Saya memiliki berbagai rutinitas debug yang mencatat hasil dari induk dan anak ke file.

Ada beberapa hal yang berkaitan dengan terminal yang saya tidak mengerti. Saya telah melihat beberapa perilaku yang tidak saya mengerti tergantung pada variasi apa yang saya coba.

Hal-hal yang saya tidak mengerti:

  • Jika saya menginstruksikan proses induk keluar, anak berakhir tanpa sesuatu yang menarik dicatat oleh anak.

  • Jika saya mencoba menutup stdin, stdout dan menggunakan dup() atau dup2() untuk menjadikan pty sebagai pengganti stdin jendela kutukan menggunakan stdin dan stdout asli dan menggunakan pty asli bukan yang baru berdasarkan output dari ptsname(). (proses induk berhasil melakukan IO dengan anak tetapi di terminal itu diluncurkan dari bukan pty baru)

  • Jika saya membuka pty baru menggunakan open() maka saya mendapatkan segfault di dalam panggilan ncurses newterm() seperti di bawah ini:

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

Saya mencoba memahami panggilan sistem pty di sini. Menggunakan program seperti screen atau tmux tidak membantu dengan ini (juga sumbernya tidak cukup dijelaskan untuk mengisi kesenjangan dalam pemahaman saya).

Beberapa datum lainnya:

  • Saya menargetkan GNU/Linux

  • Saya juga sudah mencoba menggunakan forkpty

  • Saya melihat sumber untuk openpty, forkpty, login_tty, openpt, grantpt & posix_openpt

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

  • Saya tidak memiliki akses ke salinan APUE meskipun saya telah melihat contoh pty.

  • Meskipun dokumentasi ncurses untuk newterm() menyebutkan berbicara dengan beberapa terminal secara bersamaan, saya belum menemukan contoh program yang melakukan ini.

Saya masih belum jelas tentang:

  • apa yang sebenarnya dilakukan login_tty / grantpt.

    Jika Anda membuka pty sendiri mengapa Anda belum memiliki kemampuan yang benar?

  • mengapa saya lebih suka openpty daripada posix_openpt atau sebaliknya.


Catatan: Ini adalah pertanyaan yang berbeda untuk attach-a-terminal-to-a-process-running-as-a-daemon-to-run-an-ncurses-ui yang menjelaskan kasus penggunaan dan mencari solusi di mana pertanyaan ini mengasumsikan implementasi tertentu tetapi salah/tidak lengkap untuk kasus penggunaan itu.

-1
Bruce Adams 14 Desember 2020, 04:24

3 jawaban

Jawaban Terbaik

Jawaban Glärbo telah membantu saya memahami masalah dengan cukup sehingga setelah beberapa percobaan saya yakin saya dapat menjawab pertanyaan saya yang tersisa secara langsung.

Poin-poin penting adalah:

  • Sisi master pty harus tetap terbuka
  • Deskriptor file untuk budak harus dibuka dalam mode yang sama seperti yang dibuat semula.
  • tanpa setsid() pada slave, ia tetap terhubung ke terminal pengontrol asli.
  • Anda harus berhati-hati dengan panggilan ncurses saat menggunakan istilah baru daripada initscr

Sisi master pty harus tetap terbuka

Saya: "Jika saya menginstruksikan proses induk keluar, anak berakhir tanpa sesuatu yang menarik dicatat oleh anak."

Glärbo: "Tanpa master, dan proses yang mengelola sisi master, secara harfiah tidak ada pasangan terminal semu: ketika master ditutup, kernel secara paksa menghapus budak juga, membatalkan deskriptor file yang telah dibuka budak ke sisi budak dari pasangan terminal semu."

Deskriptor file untuk budak harus dibuka dalam mode yang sama seperti yang dibuat semula.

Kode semu saya yang salah (untuk sisi anak garpu):

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

Berfungsi jika diganti dengan (pemeriksaan kesalahan dihilangkan):

 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

Saya pikir file atau deskriptor file yang buruk pasti telah merayap ke suatu tempat tanpa diperiksa. Ini menjelaskan segfault di dalam fileno_unlocked(). Saya juga telah mencoba dalam beberapa percobaan membuka budak dua kali. Sekali untuk membaca dan sekali untuk menulis. Mode akan bertentangan dengan mode fd asli.

Tanpa setsid() di sisi anak (dengan budak pty) proses anak masih memiliki terminal pengendali asli.

  • setsid() menjadikan proses sebagai pemimpin sesi. Hanya pemimpin sesi yang dapat mengubah terminal pengontrolnya.
  • ioctl(slave(TIOCTTY,0) - jadikan slave sebagai terminal pengontrol

Anda harus berhati-hati dengan panggilan ncurses saat menggunakan newterm() daripada initscr()

Banyak fungsi ncurses memiliki argumen "intscr" implisit yang merujuk ke layar atau jendela yang dibuat untuk terminal pengontrol STDIN dan STDOUT. Mereka tidak berfungsi kecuali diganti dengan fungsi ncurses() yang setara untuk WINDOW yang ditentukan. Anda perlu memanggil newwin() untuk membuat WINDOW, newterm() hanya memberi Anda layar.

Sebenarnya saya masih bergulat dengan masalah seperti ini seperti panggilan ke subwin() yang gagal ketika slave pty digunakan tetapi tidak dengan terminal normal.


Juga perlu diperhatikan bahwa:

  • Anda perlu menangani SIGWINCH dalam proses yang terhubung ke terminal aktual dan meneruskannya ke slave jika perlu mengetahui bahwa ukuran terminal telah berubah.

  • Anda mungkin memerlukan pipa ke daemon untuk menyampaikan informasi tambahan.

  • Saya membiarkan stderr terhubung ke terminal asli di atas untuk kenyamanan debugging. Itu akan ditutup dalam praktik.

lampirkan terminal ke proses yang berjalan sebagai daemon (untuk menjalankan UI ncurses) melakukan pekerjaan yang lebih baik dalam menjelaskan kasus penggunaan daripada masalah spesifik yang dipecahkan di sini.

0
Bruce Adams 1 Januari 2021, 01:59

Mari kita lihat satu kemungkinan implementasi pseudoterminal_run(), yang membuat terminal semu baru, memotong proses anak untuk dijalankan dengan terminal semu itu sebagai terminal pengontrol dengan input, output, dan kesalahan standar yang diarahkan ke terminal semu itu, dan mengeksekusi biner yang ditentukan .

Berikut file header, 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 */

Berikut adalah implementasi yang sesuai, 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;
}

Untuk mendeteksi kesalahan dalam proses anak sebelum biner dieksekusi (termasuk kesalahan dalam mengeksekusi biner), di atas menggunakan pipa close-on-exec antara anak dan orang tua untuk meneruskan kesalahan. Dalam kasus sukses, ujung penulisan pipa ditutup oleh kernel ketika eksekusi biner baru dimulai. Kalau tidak, di atas adalah implementasi langsung.

Khususnya:

  • posix_openpt(O_RDWR | O_NOCTTY) membuat pasangan pseudoterminal, dan mengembalikan deskriptor untuk sisi master. Bendera O_NOCTTY digunakan karena kita tidak ingin proses saat ini memiliki terminal semu sebagai terminal pengendali.

  • dalam proses anak, setsid() digunakan untuk memulai sesi baru, dengan ID sesi dan ID grup proses yang cocok dengan ID proses anak. Dengan cara ini, proses induk misalnya dapat mengirim sinyal ke setiap proses dalam grup itu; dan ketika anak membuka sisi budak pseudoterminal, itu harus menjadi terminal pengontrol untuk proses anak. (Kode ini melakukan ioctl(slave_fd, TIOCSCTTY, 0) untuk memastikan bahwa, jika TIOCSCTTY didefinisikan.)

  • grantpt(masterfd) mengubah pengguna pemilik dari terminal semu budak agar sesuai dengan pengguna nyata saat ini, sehingga hanya pengguna nyata saat ini (dan pengguna yang memiliki hak istimewa seperti root) yang dapat mengakses sisi budak dari terminal semu.

  • unlockpt(masterfd) memungkinkan akses ke sisi budak dari terminal semu. Itu harus dipanggil sebelum sisi budak dapat dibuka.

  • slavefd = ioctl(masterfd, TIOCGPTPEER, O_RDWR) digunakan untuk membuka pseudoterminal sisi budak jika tersedia. Jika tidak tersedia, atau gagal, maka slavefd = open(ptsname(masterfd), O_RDWR) digunakan sebagai gantinya.

Contoh berikut.c adalah contoh menggunakan pseudoterminal.h di atas, yang menjalankan biner tertentu di terminal semu baru, mem-proksi data antara terminal semu proses anak dan terminal proses induk. Ini mencatat semua pembacaan dan penulisan ke file log yang Anda tentukan sebagai parameter baris perintah pertama. Parameter baris perintah lainnya membentuk perintah yang dijalankan dalam proses anak.

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

Setiap kali proses induk menerima sinyal WINCH (perubahan ukuran jendela), ukuran jendela terminal baru diperoleh dari terminal induk, kemudian diatur ke terminal semu anak.

Untuk kesederhanaan (dan tidak menyediakan kode yang dapat digunakan apa adanya), contoh mencoba membaca dan menulis tanpa pemblokiran bila memungkinkan, dan hanya polling (menunggu hingga input tersedia, atau data buffer dapat ditulis) jika keempatnya gagal. Juga, jika buffer kosong maka, itu menyalin pengaturan terminal dari terminal semu anak ke terminal induk.

Kompilasi menggunakan mis.

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

Dan jalankan mis. ./example nano.log nano test-file. Ini menjalankan nano di sub-pseudoterminal, mencerminkan semua yang ada di dalamnya ke terminal induk, dan pada dasarnya bertindak seolah-olah Anda baru saja menjalankan nano test-file. (Tekan Ctrl+X untuk keluar.) Namun, setiap membaca dan menulis dicatat ke file nano.log. Untuk mempermudah, hanya panjangnya yang saat ini dicatat, tetapi Anda pasti dapat menulis fungsi dumper untuk juga mencatat isinya. (Karena ini berisi karakter kontrol, Anda ingin keluar dari semua karakter kontrol, atau membuang data dalam format heksadesimal.)

Sangat menarik untuk dicatat bahwa ketika proses anak (proses terakhir dengan terminal semu sebagai terminal pengendalinya) keluar, mencoba membaca dari master terminal semu mengembalikan -1 dengan errno == EIO. Ini berarti bahwa sebelum memperlakukannya sebagai kesalahan fatal, seseorang harus menuai proses di grup proses anak (waitpid(-child_pid, &status, WNOHANG)); dan jika itu mengembalikan -1 dengan errno = ECHILD, itu berarti EIO disebabkan oleh tidak ada proses yang membuka budak pseudoterminal.

Jika kita membandingkan ini dengan tmux atau screen, kita hanya menerapkan versi kasar dari bagian tersebut ketika "dipasang" ke sesi yang sedang berjalan. Ketika pengguna (proses induk, berjalan di terminal induk) "melepas" dari sesi, tmux dan layar keduanya meninggalkan proses mengumpulkan output dari perintah yang sedang berjalan. (Mereka tidak hanya menyangga semuanya, mereka cenderung merekam efek dari menjalankan perintah ke buffer terminal virtual – baris × kolom larik mesin terbang yang dapat dicetak dan atributnya –, sehingga jumlah memori yang terbatas/tetap diperlukan untuk memulihkan isi terminal saat memasang kembali nanti.)

Saat melampirkan kembali ke sesi, perintah screen/tmux terhubung ke proses yang ada (biasanya menggunakan soket domain Unix, yang memungkinkan verifikasi ID pengguna rekan, dan juga meneruskan deskriptor (ke master pseudoterminal) di antara proses, jadi proses baru dapat menggantikan proses lama, dan proses lama dapat keluar.

Jika kita menetapkan variabel lingkungan TERM untuk mengatakan xterm-256color sebelum mengeksekusi biner anak, kita dapat menafsirkan semua yang kita baca dari sisi master pseudoterminal dalam hal cara xterm 256-warna, dan mis. menggambar layar menggunakan mis. GTK+ – begitulah cara kami menulis emulator terminal kami sendiri.

2
Glärbo 15 Desember 2020, 03:11

Saya mencoba mencari cara untuk melakukan hal berikut:

  1. buat terminal semu baru

  2. buka layar ncurses yang berjalan di dalam terminal semu (slave)

  3. garpu

  4. A) meneruskan I/O dari terminal tempat program berjalan (bash) ke terminal (slave) baru ATAU

    B) keluar meninggalkan program ncurses yang berjalan di pty baru.

Anda tampaknya memiliki kesalahpahaman mendasar tentang pasangan terminal semu, dan terutama pentingnya suatu proses menjadi master terminal semu. Tanpa master, dan proses yang mengelola sisi master, secara harfiah tidak ada pasangan pseudoterminal: ketika master ditutup, kernel secara paksa menghapus slave juga, membatalkan deskriptor file yang telah dibuka oleh slave ke sisi slave dari pasangan pseudoterminal.

Di atas, Anda benar-benar mengabaikan peran master, dan bertanya-tanya mengapa apa yang Anda inginkan tidak berhasil.

Jawaban saya menunjukkan untuk menyelesaikan 4.A), dengan biner apa pun yang berjalan sebagai budak, dengan program itu sendiri sebagai master, mem-proxy data antara pseudoterminal slave dan terminal master.

Membalikkan peran, dengan "program utama" Anda memberi tahu beberapa biner lain untuk menjadi terminal master, sederhana: tulis "program utama" Anda sendiri sebagai program ncurses normal, tetapi jalankan menggunakan program contoh saya untuk mengelola sisi master pasangan terminal semu. Dengan cara ini propagasi sinyal dan lain-lain bekerja dengan benar.

Jika Anda ingin menukar peran, dengan budak pseudoterminal menjadi proses induk dan master pseudoterminal menjadi proses anak, Anda perlu menjelaskan dengan tepat mengapa, ketika seluruh antarmuka telah dirancang untuk sebaliknya.

Tidak, tidak ada "hanya program atau perpustakaan pseudoterminal master generik yang dapat Anda gunakan untuk ini". Alasannya adalah hal seperti itu tidak masuk akal. Kapan pun Anda membutuhkan pasangan pseudoterminal, master adalah alasan Anda menginginkannya. Aliran standar apa pun yang menggunakan program penghasil atau konsumsi teks yang dapat dibaca manusia adalah klien yang valid, menggunakan ujung budak. Mereka tidak penting, hanya tuannya.

Adakah yang bisa memberikan petunjuk tentang apa yang mungkin saya lakukan salah atau yang masuk akal dari beberapa ini

Saya mencoba itu, tetapi Anda tidak menghargai usaha itu. Maaf saya mencoba.

atau bahkan lebih baik contoh program menggunakan newterm() dengan posix_openpt(), openpty() atau forkpty().

Tidak, karena newterm() Anda sama sekali tidak masuk akal.

2
Glärbo 19 Desember 2020, 07:18