在 openpty 之后 ncurses newterm
ncurses newterm following openpty
我正在尝试弄清楚如何执行以下操作:
创建一个新的伪终端
在(从属)伪终端内打开一个 ncurses 屏幕 运行
分叉
A) 从终端转发 I/O 程序是 运行 in (bash) 到新的(从)终端 OR
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);
}
}
}
}
我有各种调试例程将父项和子项的结果记录到文件中。
有几件与终端有关的事情我不明白。
根据我尝试的变化,我看到了几种我不理解的行为。
不明白的地方:
如果我指示父进程退出,子进程将终止,而子进程不会记录任何有趣的内容。
如果我尝试关闭标准输入、标准输出并使用 dup() 或 dup2() 使 pty 替换标准输入
curses window 使用原始的 stdin 和 stdout,并使用原始的 pty 而不是基于 ptsname() 输出的新 pty。
(父进程成功地与子进程执行 IO 但在终端中它不是从新的 pty 启动的)
如果我使用 open() 打开新的 pty,那么我会在 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 示例。
尽管 newterm() 的 ncurses 文档提到同时与多个终端对话,但我还没有找到执行此操作的示例程序。
我还不清楚:
login_tty / grantpt 实际做了什么。
如果您自己开设了 pty,为什么您不具备正确的能力?
为什么我可能更喜欢 openpty 而不是 posix_openpt 或者反之亦然。
注意:这是一个与 不同的问题,它描述了一个用例并寻找一个解决方案,其中这个问题假设了一个特定的但 incorrect/incomplete 该用例的实现。
让我们看一下 pseudoterminal_run()
的一种可能实现方式,它创建一个新的伪终端,将 child 进程分叉到 运行 并将该伪终端作为具有标准输入、输出的控制终端, 并将错误定向到该伪终端,并执行指定的二进制文件。
这是头文件,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 */
这里是对应的实现,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;
}
为了在执行二进制文件之前检测 child 进程中的错误(包括执行二进制文件时的错误),上面在 child 和 parent 传递错误。在成功的情况下,当开始执行新的二进制文件时,内核会关闭管道写入端。
否则上面是一个简单的实现。
特别是:
posix_openpt(O_RDWR | O_NOCTTY) 创建伪终端对,returns 主端的描述符。使用 O_NOCTTY 标志是因为我们不希望当前进程将该伪终端作为控制终端。
在child进程中,setsid()用于启动一个新会话,会话ID和进程组ID都与child进程ID匹配。这样,parent 进程可以向该组中的每个进程发送信号;当 child 打开伪终端从属端时,它应该成为 child 进程的控制终端。 (代码确实执行了 ioctl(slave_fd, TIOCSCTTY, 0) 以确保如果定义了 TIOCSCTTY。)
g运行tpt(masterfd) 改变slave伪终端的owner用户匹配当前真实用户,这样只有当前真实用户(以及root等特权用户)可以访问伪终端的从属端。
unlockpt(masterfd) 允许访问伪终端的从属端。必须先调用,slave端才能打开
slavefd = ioctl(masterfd, TIOCGPTPEER, O_RDWR) 用于打开从属端伪终端(如果可用)。如果不可用或失败,则使用 slavefd = open(ptsname(masterfd), O_RDWR) 代替。
下面的example.c是使用上面pseudoterminal.h的例子,其中运行是一个新的伪终端中指定的二进制文件,代理child进程伪终端之间的数据和 parent 进程终端。它将所有读取和写入记录到您指定为第一个命令行参数的日志文件中。 child进程中的其余命令行参数组成命令运行。
#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;
}
每当parent进程收到一个WINCH(window大小改变)信号,新的终端window大小从parent终端获取,然后设置为child 伪终端。
为简单起见(并且不提供可以使用的代码 as-is),该示例尽可能尝试非阻塞读取和写入,并且仅进行轮询(等待输入可用或可以写入缓冲数据)如果四个都失败了。此外,如果缓冲区为空,它会将终端设置从 child 伪终端复制到 parent 终端。
使用例如
编译
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
。 sub-pseudoterminal 中的 运行s nano
,将其中的所有内容反映到 parent 终端,本质上就好像您只是 运行 nano test-file
. (按Ctrl+X退出。)
但是,每次读写都会记录到 nano.log 文件中。为简单起见,目前仅记录长度,但您当然可以编写转储函数来记录内容。 (因为它们包含控制字符,您需要转义所有控制字符,或者以十六进制格式转储数据。)
值得注意的是,当 child 进程(最后一个以伪终端作为其控制终端的进程)退出时,试图从伪终端主机 returns -1 读取 errno == EIO
.这意味着在将其视为致命错误之前,应该在 child 进程进程组 (waitpid(-child_pid, &status, WNOHANG)
) 中获取进程;如果 returns -1 和 errno = ECHILD
,这意味着 EIO 是由没有打开伪终端从属的进程引起的。
如果我们将其与 tmux 或屏幕进行比较,当“附加”到 运行ning 会话时,我们只实现了该部分的粗略版本。当用户(parent 进程,parent 终端中的 运行ning)从会话“分离”时,tmux 和 screen 都会留下一个收集 运行 输出的进程宁命令。 (他们不只是缓冲一切,他们倾向于将 运行ning 命令的效果记录到虚拟终端缓冲区 - 可打印字形及其属性的行×列数组 - 因此 limited/fixed 数量稍后 re-attaching 时需要内存来恢复终端内容。)
当re-attaching到会话时,screen/tmux命令连接到现有进程(通常使用Unix域套接字,允许验证他对等用户ID,还在进程之间传递描述符(给伪终端主机),所以新进程可以取代旧进程,旧进程可以退出。
如果我们在执行 child 二进制文件之前将 TERM
环境变量设置为 xterm-256color
,我们可以根据 256- color xterm 可以,例如使用例如绘制屏幕GTK+ – 这就是我们编写自己的终端仿真器的方式。
I am trying to figure out how to do the following:
create a new pseudo-terminal
open a ncurses screen running inside the (slave) pseudo terminal
fork
A) forward I/O from the terminal the program is running in (bash) to the new (slave) terminal OR
B) exit leaving the ncurses program running in the new pty.
您似乎对伪终端对存在根本性的误解,尤其是进程作为伪终端主机的重要性。没有主机和管理主机端的进程,实际上就没有伪终端对:当主机关闭时,内核也强行删除从机,使从机打开的文件描述符对伪终端对的从机端无效。
以上,你完全忽略了master的作用,想知道为什么你想要的不灵
我的 显示完成 4.A),以任何二进制文件 运行ning 作为从站,程序本身是主站,在从站伪终端和主终端。
颠倒角色,用你的“主程序”告诉其他一些二进制文件为主终端,很简单:把你自己的“主程序”写成一个普通的 ncurses 程序,但是 运行 它使用我的示例程序来管理伪终端对的主端。这样信号传播等就可以正常工作了。
如果你想互换角色,伪终端slave是父进程,伪终端master是子进程,你需要解释清楚为什么,因为整个界面都是为相反的设计的。
不,没有“只有一个通用的主伪终端程序或库可以用于此”。原因是这样没有意义。每当你需要一对伪终端时,master 就是你想要它的原因。任何使用人类可读文本生成或消费程序的标准流都是有效的客户端,使用从端。他们不重要,重要的是师父。
Can anyone provide pointers to what I might be doing wrong or that would make sense of some of this
我试过了,但你并不欣赏我的努力。对不起,我试过了。
or even better an example program using newterm() with either posix_openpt(), openpty() or forkpty().
不,因为你的 newterm() 完全没有意义。
Glärbo 的回答帮助我充分理解了问题,经过一些实验后我相信我可以直接回答剩下的问题。
重点是:
- pty 的主端必须保持打开状态
- 从站的文件描述符必须以与最初创建的模式相同的模式打开。
- 在从机上没有 setsid() 它仍然连接到原始控制终端。
- 在使用 newterm 而不是 initscr 时需要小心 ncurses 调用
pty 的主端必须保持打开状态
我:“如果我指示父进程退出,子进程将终止,而子进程不会记录任何有趣的信息。”
Glärbo:“没有主控和管理主控端的进程,实际上就没有伪终端对:当主控关闭时,内核也强行删除从属,使从属打开的文件描述符无效伪终端对的从属端。"
从站的文件描述符必须以与最初创建的模式相同的模式打开。
我错误的伪代码(分叉的子端):
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
我认为一个错误的文件或文件描述符一定是在没有被检查的情况下悄悄通过某个地方。这解释了 fileno_unlocked() 中的段错误。
我也曾在一些实验中尝试过两次打开奴隶。一次用于阅读,一次用于写作。 mode会和原来fd的mode冲突
在子端(使用从属 pty)没有 setsid() 子进程仍然有原来的控制终端。
- setsid() 使进程成为会话领导者。只有会话负责人可以更改其控制终端。
- ioctl(slave(TIOCTTY,0) - 让slave成为控制终端
使用 newterm() 而不是 initscr() 时需要小心 ncurses 调用
许多 ncurses 函数都有一个隐含的“intscr”参数,它指的是为控制终端 STDIN 和 STDOUT 创建的屏幕或 window。除非替换为指定 WINDOW 的等效 ncurses() 函数,否则它们不起作用。你需要调用newwin()来创建一个WINDOW,newterm()只给你一个屏幕。
事实上,我仍在努力解决此类问题,例如调用 subwin() 在使用从属 pty 而不是使用普通终端时失败。
另外值得注意的是:
您需要在连接到实际终端的进程中处理 SIGWINCH,如果需要知道终端大小已更改,则将其传递给从站。
您可能需要一个到守护进程的管道来传递附加信息。
为了调试方便,我将stderr连接到上面的原始终端。那实际上会被关闭。
在描述用例方面比在此处解决的具体问题做得更好。
我正在尝试弄清楚如何执行以下操作:
创建一个新的伪终端
在(从属)伪终端内打开一个 ncurses 屏幕 运行
分叉
A) 从终端转发 I/O 程序是 运行 in (bash) 到新的(从)终端 OR
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);
}
}
}
}
我有各种调试例程将父项和子项的结果记录到文件中。
有几件与终端有关的事情我不明白。 根据我尝试的变化,我看到了几种我不理解的行为。
不明白的地方:
如果我指示父进程退出,子进程将终止,而子进程不会记录任何有趣的内容。
如果我尝试关闭标准输入、标准输出并使用 dup() 或 dup2() 使 pty 替换标准输入 curses window 使用原始的 stdin 和 stdout,并使用原始的 pty 而不是基于 ptsname() 输出的新 pty。 (父进程成功地与子进程执行 IO 但在终端中它不是从新的 pty 启动的)
如果我使用 open() 打开新的 pty,那么我会在 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 示例。
尽管 newterm() 的 ncurses 文档提到同时与多个终端对话,但我还没有找到执行此操作的示例程序。
我还不清楚:
login_tty / grantpt 实际做了什么。
如果您自己开设了 pty,为什么您不具备正确的能力?
为什么我可能更喜欢 openpty 而不是 posix_openpt 或者反之亦然。
注意:这是一个与
让我们看一下 pseudoterminal_run()
的一种可能实现方式,它创建一个新的伪终端,将 child 进程分叉到 运行 并将该伪终端作为具有标准输入、输出的控制终端, 并将错误定向到该伪终端,并执行指定的二进制文件。
这是头文件,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 */
这里是对应的实现,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;
}
为了在执行二进制文件之前检测 child 进程中的错误(包括执行二进制文件时的错误),上面在 child 和 parent 传递错误。在成功的情况下,当开始执行新的二进制文件时,内核会关闭管道写入端。 否则上面是一个简单的实现。
特别是:
posix_openpt(O_RDWR | O_NOCTTY) 创建伪终端对,returns 主端的描述符。使用 O_NOCTTY 标志是因为我们不希望当前进程将该伪终端作为控制终端。
在child进程中,setsid()用于启动一个新会话,会话ID和进程组ID都与child进程ID匹配。这样,parent 进程可以向该组中的每个进程发送信号;当 child 打开伪终端从属端时,它应该成为 child 进程的控制终端。 (代码确实执行了 ioctl(slave_fd, TIOCSCTTY, 0) 以确保如果定义了 TIOCSCTTY。)
g运行tpt(masterfd) 改变slave伪终端的owner用户匹配当前真实用户,这样只有当前真实用户(以及root等特权用户)可以访问伪终端的从属端。
unlockpt(masterfd) 允许访问伪终端的从属端。必须先调用,slave端才能打开
slavefd = ioctl(masterfd, TIOCGPTPEER, O_RDWR) 用于打开从属端伪终端(如果可用)。如果不可用或失败,则使用 slavefd = open(ptsname(masterfd), O_RDWR) 代替。
下面的example.c是使用上面pseudoterminal.h的例子,其中运行是一个新的伪终端中指定的二进制文件,代理child进程伪终端之间的数据和 parent 进程终端。它将所有读取和写入记录到您指定为第一个命令行参数的日志文件中。 child进程中的其余命令行参数组成命令运行。
#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;
}
每当parent进程收到一个WINCH(window大小改变)信号,新的终端window大小从parent终端获取,然后设置为child 伪终端。
为简单起见(并且不提供可以使用的代码 as-is),该示例尽可能尝试非阻塞读取和写入,并且仅进行轮询(等待输入可用或可以写入缓冲数据)如果四个都失败了。此外,如果缓冲区为空,它会将终端设置从 child 伪终端复制到 parent 终端。
使用例如
编译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
。 sub-pseudoterminal 中的 运行s nano
,将其中的所有内容反映到 parent 终端,本质上就好像您只是 运行 nano test-file
. (按Ctrl+X退出。)
但是,每次读写都会记录到 nano.log 文件中。为简单起见,目前仅记录长度,但您当然可以编写转储函数来记录内容。 (因为它们包含控制字符,您需要转义所有控制字符,或者以十六进制格式转储数据。)
值得注意的是,当 child 进程(最后一个以伪终端作为其控制终端的进程)退出时,试图从伪终端主机 returns -1 读取 errno == EIO
.这意味着在将其视为致命错误之前,应该在 child 进程进程组 (waitpid(-child_pid, &status, WNOHANG)
) 中获取进程;如果 returns -1 和 errno = ECHILD
,这意味着 EIO 是由没有打开伪终端从属的进程引起的。
如果我们将其与 tmux 或屏幕进行比较,当“附加”到 运行ning 会话时,我们只实现了该部分的粗略版本。当用户(parent 进程,parent 终端中的 运行ning)从会话“分离”时,tmux 和 screen 都会留下一个收集 运行 输出的进程宁命令。 (他们不只是缓冲一切,他们倾向于将 运行ning 命令的效果记录到虚拟终端缓冲区 - 可打印字形及其属性的行×列数组 - 因此 limited/fixed 数量稍后 re-attaching 时需要内存来恢复终端内容。)
当re-attaching到会话时,screen/tmux命令连接到现有进程(通常使用Unix域套接字,允许验证他对等用户ID,还在进程之间传递描述符(给伪终端主机),所以新进程可以取代旧进程,旧进程可以退出。
如果我们在执行 child 二进制文件之前将 TERM
环境变量设置为 xterm-256color
,我们可以根据 256- color xterm 可以,例如使用例如绘制屏幕GTK+ – 这就是我们编写自己的终端仿真器的方式。
I am trying to figure out how to do the following:
create a new pseudo-terminal
open a ncurses screen running inside the (slave) pseudo terminal
fork
A) forward I/O from the terminal the program is running in (bash) to the new (slave) terminal OR
B) exit leaving the ncurses program running in the new pty.
您似乎对伪终端对存在根本性的误解,尤其是进程作为伪终端主机的重要性。没有主机和管理主机端的进程,实际上就没有伪终端对:当主机关闭时,内核也强行删除从机,使从机打开的文件描述符对伪终端对的从机端无效。
以上,你完全忽略了master的作用,想知道为什么你想要的不灵
我的
颠倒角色,用你的“主程序”告诉其他一些二进制文件为主终端,很简单:把你自己的“主程序”写成一个普通的 ncurses 程序,但是 运行 它使用我的示例程序来管理伪终端对的主端。这样信号传播等就可以正常工作了。
如果你想互换角色,伪终端slave是父进程,伪终端master是子进程,你需要解释清楚为什么,因为整个界面都是为相反的设计的。
不,没有“只有一个通用的主伪终端程序或库可以用于此”。原因是这样没有意义。每当你需要一对伪终端时,master 就是你想要它的原因。任何使用人类可读文本生成或消费程序的标准流都是有效的客户端,使用从端。他们不重要,重要的是师父。
Can anyone provide pointers to what I might be doing wrong or that would make sense of some of this
我试过了,但你并不欣赏我的努力。对不起,我试过了。
or even better an example program using newterm() with either posix_openpt(), openpty() or forkpty().
不,因为你的 newterm() 完全没有意义。
Glärbo 的回答帮助我充分理解了问题,经过一些实验后我相信我可以直接回答剩下的问题。
重点是:
- pty 的主端必须保持打开状态
- 从站的文件描述符必须以与最初创建的模式相同的模式打开。
- 在从机上没有 setsid() 它仍然连接到原始控制终端。
- 在使用 newterm 而不是 initscr 时需要小心 ncurses 调用
pty 的主端必须保持打开状态
我:“如果我指示父进程退出,子进程将终止,而子进程不会记录任何有趣的信息。”
Glärbo:“没有主控和管理主控端的进程,实际上就没有伪终端对:当主控关闭时,内核也强行删除从属,使从属打开的文件描述符无效伪终端对的从属端。"
从站的文件描述符必须以与最初创建的模式相同的模式打开。
我错误的伪代码(分叉的子端):
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
我认为一个错误的文件或文件描述符一定是在没有被检查的情况下悄悄通过某个地方。这解释了 fileno_unlocked() 中的段错误。 我也曾在一些实验中尝试过两次打开奴隶。一次用于阅读,一次用于写作。 mode会和原来fd的mode冲突
在子端(使用从属 pty)没有 setsid() 子进程仍然有原来的控制终端。
- setsid() 使进程成为会话领导者。只有会话负责人可以更改其控制终端。
- ioctl(slave(TIOCTTY,0) - 让slave成为控制终端
使用 newterm() 而不是 initscr() 时需要小心 ncurses 调用
许多 ncurses 函数都有一个隐含的“intscr”参数,它指的是为控制终端 STDIN 和 STDOUT 创建的屏幕或 window。除非替换为指定 WINDOW 的等效 ncurses() 函数,否则它们不起作用。你需要调用newwin()来创建一个WINDOW,newterm()只给你一个屏幕。
事实上,我仍在努力解决此类问题,例如调用 subwin() 在使用从属 pty 而不是使用普通终端时失败。
另外值得注意的是:
您需要在连接到实际终端的进程中处理 SIGWINCH,如果需要知道终端大小已更改,则将其传递给从站。
您可能需要一个到守护进程的管道来传递附加信息。
为了调试方便,我将stderr连接到上面的原始终端。那实际上会被关闭。