Я пытаюсь понять, как сделать следующее:
создать новый псевдотерминал
откройте экран ncurses, работающий внутри (подчиненного) псевдотерминала
вилка
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, который описывает вариант использования и ищет решение. где этот вопрос предполагает конкретную, но неправильную/неполную реализацию для этого варианта использования.
Программа то же самое в примерах ncurses открывает несколько xterms. Учебники не по теме.
Я не понимаю, как использовать то же самое. У меня есть openpty, но для него все еще требуется параметр. Если я даю ему случайную строку, такую как «A», я получаю segfault от запуска процесса «xterm -S/dev/pts/25/4 -title A».
Это должно работать; последнее исправление ошибки в этой области было 6 лет назад.
Что касается интерфейсов, то единого pty API, который работал бы везде, не существует. xterm имеет несколько вариантов, как и luit. Вы можете найти источник luit немного легче следовать. Но то же самое является наиболее подходящим примером для этого вопроса.
Давайте рассмотрим одну из возможных реализаций 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. Вместо этого вы выполняете новый процесс. Хотя должно работать так же.
Зачем использовать семейство вызовов posix_openpt, а не openpty и login_tty? Это еще один выбор, который мне неясен.
Я попробовал ваш пример, но заменил подчиненный exec на newterm (NULL, STDIN_FILENO, STDOUT_FILENO). Я получил segfault в fileno_unlocked(), как я описал в своем вопросе.
newterm() завершается успешно, если вместо этого я использую подчиненный FD. Однако printw("hello\n") ничего не делает. Если заменить newterm() на fprintf(slave,"hello\n") или fprintf(STDOUT_FILENO, "hello\n"), я получаю segfault в fwrite(). Это проблемы, которые я на самом деле пытаюсь понять.
Рождественская награда за приложенные усилия, хотя у меня еще нет ответа в голове.
Является ли этот шаг «Убедитесь, что мастер не затеняет стандартные потоки». действительно необходимо? При каких обстоятельствах новому pty может быть присвоен тот же fd, что и stdin, stdout или stderr? Конечно, это могло произойти только в том случае, если они были намеренно закрыты приложением?
Я пытаюсь понять, как сделать следующее:
создать новый псевдотерминал
откройте экран ncurses, работающий внутри (подчиненного) псевдотерминала
вилка
A) перенаправить ввод-вывод с терминала, в котором выполняется программа (bash), на новый (подчиненный) терминал ИЛИ
B) выйти, оставив программу ncurses запущенной в новом pty.
Похоже, у вас есть фундаментальное заблуждение о парах псевдотерминалов и особенно о важности процесса, являющегося мастером псевдотерминала. Без ведущего и процесса, управляющего ведущей стороной, пары псевдотерминалов буквально не существует: когда ведущее устройство закрывается, ядро также принудительно удаляет ведомое устройство, делая недействительными файловые дескрипторы, открытые ведомым устройством для ведомой стороны пары псевдотерминалов.
Выше вы полностью игнорируете роль мастера и удивляетесь, почему то, что вы хотите, не работает.
Мой ответ показывает выполнение 4.A), когда любой двоичный файл работает как ведомый, а сама программа является ведущей, передавая данные между ведомым псевдотерминалом и главным терминалом.
Обратную роль, когда ваша «основная программа» указывает другому двоичному файлу быть главным терминалом, очень просто: напишите свою собственную «основную программу» как обычную программу ncurses, но запустите ее, используя мой пример программы для управления главной стороной терминала. псевдотерминальная пара. Таким образом, распространение сигнала и так далее работает корректно.
Если вы хотите поменяться ролями, при этом ведомый псевдотерминал является родительским процессом, а мастер псевдотерминала — дочерним процессом, вам нужно точно объяснить, почему, когда весь интерфейс был разработан для противоположного.
Нет, не существует «просто универсальной основной псевдотерминальной программы или библиотеки, которую вы можете использовать для этого». Причина в том, что в этом нет смысла. Всякий раз, когда вам нужна пара псевдотерминалов, мастер — это причина, по которой вам это нужно. Любой стандартный поток, использующий удобочитаемый текст, производящий или потребляющий программу, является действительным клиентом, использующим ведомый конец. Они не важны, важен только хозяин.
Может ли кто-нибудь дать указания на то, что я могу делать неправильно, или это имело бы смысл в некоторых из этих
Я пробовал, но ты не оценил усилий. Я сожалею, что пытался.
или, что еще лучше, пример программы, использующей newterm() с posix_openpt(), openpty() или forkpty().
Нет, потому что ваш newterm() не имеет абсолютно никакого смысла.
Вы правы в том, что у меня есть фундаментальные заблуждения, поэтому я спрашиваю здесь. Но почему один и тот же процесс не может породить ведущего и ведомого? Запускаю ли я execp или продолжаю процесс в той же кодовой базе, не должно иметь значения. Я не вижу причин, по которым newterm() не должен работать.
Самая близкая к подходящей библиотеке, на которую мне указали, это libvterm.
Вы подтвердили, что закрытие мастера завершает работу терминала. Я подозревал, что да, но не был уверен. Я ценю усилия, но не могу принять ответ, пока не докажу себе, что понимаю, что происходит иначе.
Да, libvterm — это один из способов, с помощью которого главная сторона может интерпретировать различные escape-коды (которые клиент записывает на подчиненную сторону, которыми, возможно, манипулирует слой termios, а приложение, использующее libvterm, читает с главной стороны) и «рисовать» их в текст на основе фреймбуфера. Вы можете использовать libvterm для реализации собственного эмулятора терминала.
@BruceAdams: Мое разочарование связано с тем, что я не могу понять то, чего не понимаешь ты. Рассмотрение практического примера должно (я думал!) быть эффективным способом прояснить и выяснить, на какие вопросы вы ищете ответы, но это не сработало. Это проблема со связью, она не имеет ничего общего со способностями или интеллектом, но я сам не очень хорош в общении. Не могли бы вы представить реальный вариант использования с двумя сторонами: одна (дочерняя) работает в псевдотерминале, а другая (родительская) управляет псевдотерминалом?
@BruceAdams: важной частью является НЕ дочерний элемент, работающий в псевдотерминале, потому что это нормальное состояние и действия; важной частью является то, что родительская, главная сторона псевдотерминала делает для предоставления псевдотерминала. Я пытался ломать голову над тем, как найти пример, который поможет вам задавать вопросы, на которые можно ответить, но я не знаю, как это сделать. Должен ли я просто отредактировать свой пример, чтобы вместо дочернего процесса сеанс ncurses просто повторял ввод пользователя, пока пользователь не нажмет клавишу (возможно, .
)? Основная сторона по-прежнему будет просто проксировать родительский терминал, поэтому ввод-вывод будет виден.
@BruceAdams: Основная причина, по которой подчиненная сторона псевдотерминала неинтересна, заключается в том, что если она реализована правильно, процесс, работающий на псевдотерминале, не видит абсолютно никакой разницы в поведении по сравнению с работой в «настоящем» терминале. Все интересное происходит на мастере. Кроме того, нам нужно разветвить дочерний процесс для запуска на этом псевдотерминале (подчиненном конце), потому что иначе мы не сможем начать новый сеанс с псевдотерминалом в качестве управляющего терминала. На самом деле нет смысла иметь обе стороны в одном и том же процессе; Я даже не уверен, что это может работать.
У моего исходного вопроса был вариант использования, но он был закрыт из-за отсутствия внимания из-за того, что я спросил о нескольких вещах, которые я не понимаю. Я воссоздал его здесь - stackoverflow.com/questions/65375849/… Я попытался создать более узкие вопросы, на которые вы пытались ответить, но, похоже, я все еще не могу понять некоторые основы.
@BruceAdams: Думаю, теперь я могу понять.
Ответы Глэрбо помогли мне понять проблемы настолько, что после некоторых экспериментов я верю, что смогу напрямую ответить на оставшиеся вопросы.
Важными моментами являются:
Я: «Если я даю указание родительскому процессу выйти, дочерний процесс завершается, и дочерний процесс не регистрирует ничего интересного».
Глэрбо: «Без ведущего и процесса, управляющего ведущей стороной, буквально не существует пары псевдотерминалов: когда ведущее устройство закрывается, ядро также принудительно удаляет ведомое устройство, делая недействительными файловые дескрипторы, открытые подчиненным устройством для ведомой стороны. псевдотерминальная пара».
Мой неверный псевдокод (для дочерней стороны форка):
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.
Многие функции ncurses имеют неявный аргумент «intscr», который относится к экрану или окну, созданному для управляющих терминалов STDIN и STDOUT. Они не работают, если их не заменить эквивалентными функциями ncurses() для указанного WINDOW. Вам нужно вызвать newwin() для создания ОКНА, newterm() дает вам только экран.
На самом деле я все еще борюсь с такими проблемами, как вызов subwin(), который терпит неудачу, когда используется ведомый pty, но не с обычным терминалом.
Примечательно также, что:
Вам нужно обработать SIGWINCH в процессе, подключенном к реальному терминалу, и передать его подчиненному, если ему нужно знать, что размер терминала изменился.
Вероятно, вам нужен канал для демона для передачи дополнительной информации.
Я оставил подключение stderr к исходному терминалу выше для удобства отладки. Это было бы закрыто на практике.
прикрепите терминал к процессу, работающему как демон (для запуска пользовательского интерфейса ncurses) лучше описывает вариант использования, чем конкретные проблемы, устраняемые здесь.
Смотрите также stackoverflow.com/questions/65175134/what-can-you-do-with-a-pty