将终端作为守护进程附加到进程 运行ning(附加到 运行 和 ncurses UI)
attach a terminal to a process running as a daemon (to run an ncurses UI)
我有一个(遗留)程序,它充当守护进程(从某种意义上说,它 运行 永远等待服务请求)但它有一个基于 ncurses 的用户界面 运行在主机上。
我想修改程序,如果我通过 ssh 连接到主机,我可以按需启用用户界面。
我知道至少有一种使用伪终端的方法,但我不太确定如何实现它。
我认为有两种应用程序行为很有趣:
运行 UI 仅当应用程序 运行 在终端
的前台运行时
- 如果应用程序 运行 在终端的前台 - 显示 UI
- 如果应用程序 运行 在后台 - 不显示 UI
- 如果应用程序移至后台 - 关闭 UI
- 如果应用程序移动到终端的前台 - 打开 UI
当有人连接到服务器时按需创建一个新的UI
- 应用程序运行正在后台运行
- 新用户登录本机
- 他们 运行 导致 UI 的一个实例在他们的终端中打开
- 多个用户可以拥有自己的UI实例。
备注
有一种简单的方法可以使用 screen 来做到这一点。所以:
原文:
screen mydaemon etc...
新的 ssh 会话:
screen -d
screen -r
这会分离屏幕,将其留在后台 运行ning,然后将其重新连接到当前终端。关闭终端时,屏幕会话会分离,因此效果很好。
我想了解 screen 在幕后做了什么,既是为了我自己的教育,也是为了了解您如何将这些功能放入应用程序本身。
我知道如何为通过套接字连接的服务器执行此操作。我想了解的是原则上如何使用伪终端完成此操作。这确实是一种使应用程序运行的奇怪方式,但我认为它有助于深入探索使用伪终端的功能和局限性。
对于第一种情况,我假设我希望 ncurses UI 运行ning 在主端传递输入的从属终端中。
主进程会使用 isatty() 之类的东西来检查它当前是否在终端的前台,并使用 newterm() 和 endwin() 激活或停用 UI。
我一直在尝试这个,但我还没有让它工作,因为终端和 ncurses 的某些方面我充其量还没有掌握,最坏的情况是根本误解。
伪代码是:
openpty(masterfd,slavefd)
login_tty();
fork();
ifslave
close(stdin)
close(stdout)
dup_a_new_stdin_from_slavefd();
newterm(NULL, newinfd, newoutfd); (
printw("hello world");
insert_uiloop_here();
endwin();
else ifmaster
catchandforwardtoslave(SIGWINCH);
while(noexit)
{
docommswithslave();
forward_output_as_appropriate();
}
通常我会得到
或在调用终端而不是新的不可见终端上输出。
问题
- 上面的伪代码有什么问题?
- 我的主从端是否正确?
- login_tty 在这里实际做什么?
- openpty() + login_tty() 与 posix_openpt() + grantpt() 之间有什么实际区别吗?
- 是否必须始终有一个 运行ning 进程关联或从主 tty?
注意:这是与 不同的问题,它描述了此用例的特定 incorrect/incomplete 实现并询问它有什么问题。
这是一个很好的问题,也是为什么我们有伪终端的一个很好的例子。
为了使守护程序能够使用 ncurses 接口,它需要一个伪终端(伪终端对的从属端),它从守护程序开始执行时一直可用,直到守护程序退出。
要使伪终端存在,必须有一个进程对伪终端对的主控方具有打开的描述符。此外,它必须消耗伪终端从属端的所有输出(ncurses 输出的可见内容)。通常,像 vterm 这样的库用于解释该输出以将实际文本帧缓冲区“绘制”到一个数组中(好吧,通常是两个数组 - 一个用于每个单元格(特定行和列)中显示的宽字符,另一个用于颜色等属性)。
为了使伪终端对正常工作,主端的进程是 parent 或从端进程 运行ning ncurses 的祖先,或者两者完全无关. slave端的进程运行ning ncurses应该在一个新的会话中,以伪终端作为其控制终端。如果我们使用在 child 进程中启动守护进程的小型伪终端“服务器”,这是最容易实现的;事实上,这是通常用于伪终端的模式。
第一种情况不太可行,因为没有 parent/master 维护伪终端的进程。
我们可以提供第一种情况的行为,通过添加一个小的pseudoterminal-providing“看门人”进程,其任务是维护存在的伪终端对,并使用伪终端对中进程 运行ning 生成的任何 ncurses 输出。
但是,这种行为也符合第二种情况。
换句话说,这是可行的:
我们使用一个自定义程序,比如 'janitor',而不是直接启动守护进程,它创建一个伪终端并 运行 在该伪终端中设置守护进程。
只要守护进程 运行s.
Janitor 就会停留 运行ning
Janitor 为其他进程提供了一个接口来“连接”到伪终端对的主端。
这并不一定意味着 1:1 数据代理。通常提供给守护进程的输入(按键)未经修改,但是伪终端“framebuffer”的内容,character-based 虚拟 window 内容,如何传输确实有所不同。这完全在我们自己的掌控之中。
要连接到管理员,我们需要第二个帮助程序。
在'screen'的情况下,这两个程序实际上是同一个二进制文件;该行为仅由 command-line 参数控制,按键由 'screen' 本身“消耗”,以控制 'screen' 行为,而不传递给实际的 ncurses-based 进程 运行在伪终端中。
到目前为止,我们只能检查 tmux or screen 来源,看看他们是如何做到上述的;这是非常简单的终端多路复用的东西。
但是,这里有一个非常有趣的地方,我以前没有考虑过;这点小事让我明白了这个问题相当重要的核心:
Multiple users can have their own instances of the UI.
一个进程只能有一个控制终端。这指定了某种关系。例如,当控制终端的主控端关闭时,伪终端对消失,伪终端对的从属端打开的描述符变得无效(如果我没记错的话,所有操作都会产生 EIO);但不仅如此,进程组中的每个进程都会收到一个 HUP 信号。
ncurses newterm() 函数允许进程在 运行 时间连接到现有终端或伪终端。该终端不需要是控制终端,ncurses-using 进程也不需要属于该会话。重要的是要认识到,在这种情况下,标准流(标准输入、输出和错误)不会重定向到终端。
所以,如果有办法告诉守护进程它有一个可用的新伪终端,并且应该打开它,因为有一个用户想要使用守护进程提供的界面,我们可以打开守护进程并按需关闭伪终端!
但是请注意,这需要在守护进程和用于连接到守护进程提供的 ncurses-based UI 的进程之间显式 co-operation。对于任意 ncurses-based 进程或守护进程,没有标准的方法可以做到这一点。例如,据我所知, nano
和 top
没有提供这样的接口;他们只使用与标准流关联的伪终端。
发布此答案后 – 希望在问题结束之前速度足够快因为其他人看不到问题的有效性及其对其他 server-side POSIXy 开发人员的用处 – 我将构建一个示例程序对来举例说明上述内容;可能使用 Unix 域套接字作为“请为该用户新建 UI”通信通道,因为文件描述符可以使用 Unix 域套接字作为辅助数据传递,并且套接字两端的用户身份可以待验证(凭证辅助数据)。
不过,现在让我们回到提出的问题。
What is wrong with the above pseudo code? [Typically I either get a segfault inside fileno_unlocked() in newterm() or output on the invoking terminal rather than a new invisible terminal.]
newinfd
和 newoutfd
应该与伪终端从端文件描述符相同(或 dup()s),slavefd
.
我认为还应该有一个明确的 set_term()
,其中 SCREEN 指针 return 由 newterm() 作为参数编辑。 (它可能会自动调用 newterm() 提供的第一个终端,但我宁愿明确调用它。)
newterm()
连接并准备一个新终端。这两个描述符通常都指的是伪终端对的同一个从端; infd
可以是从中接收用户按键的一些其他描述符。
ncurses 中一次只能激活一个终端。您需要使用 set_term()
到 select 哪个会受到以下 printw()
等调用的影响。 (它 return 是以前活动的终端,因此可以对另一个终端进行更新,然后 return 回到原来的终端。)
(这也意味着如果一个程序提供了多个终端,它必须在它们之间循环,检查输入,并以相对较高的频率更新每个终端,这样人类用户就会觉得 UI反应灵敏,而不是“滞后”。狡猾的 POSIX 程序员可以 select 或轮询底层描述符,并且只在输入待处理的终端之间循环。)
Do I have the master and slave ends the right way around?
是的,我相信你会。从端是看到终端的,可以使用 ncurses。主端是提供按键的端,并使用 ncurses 输出做 某事(比如,将它们绘制到 text-based 帧缓冲区,或代理到远程终端)。
What does login_tty actually do here?
有两种常用的伪终端接口:UNIX98(在POSIX中标准化)和BSD。
使用 POSIX 接口,posix_openpt()
创建一个新的伪终端对,并且 return 将描述符发送到它的主端。关闭此描述符(最后打开的副本)会破坏该对。在 POSIX 模型中,从属端最初是“锁定的”,无法打开。 unlockpt()
移除此锁,允许打开从属端。 grantpt()
更新字符设备(对应伪终端对的从端)所有权和模式以匹配当前真实用户。 unlockpt()
和 grantpt()
可以按任何顺序调用,但先调用 grantpt()
是有意义的;这样,在正确设置其所有权和访问模式之前,其他进程不能“意外地”打开从属端。 POSIX 通过 ptsname()
提供了对应于伪终端对的从端的字符设备的路径,但是 Linux 提供了一个 TIOCGPTPEER ioctl (在内核 4.13 和更高版本中)允许即使字符设备节点未显示在当前挂载命名空间中,也打开从端。
通常,grantpt()
、unlockpt()
和打开伪终端对的从端是在 child 进程中完成的(仍然可以访问 master-side描述符)已使用 setsid()
启动新会话。 child 进程将标准流(标准输入、输出和错误)重定向到伪终端的从属端,关闭它的 master-side 描述符副本,并确保伪终端是它的控制终端。通常这之后是执行将使用伪终端(通常通过 ncurses)作为其用户界面的二进制文件。
使用 BSD 接口,openpty()
创建伪终端对,为双方提供打开的文件描述符,并可选择设置伪终端 termios 设置和 window 大小。它大致对应于 POSIX posix_openpt()
+ grantpt()
+ unlockpt()
+ 打开伪终端对的从属端 + 可选地设置 termios 设置和终端 window 大小。
使用BSD接口,login_tty
在child进程中是运行。 运行s setsid()
创建一个新会话,使从端成为控制终端,将标准流重定向到控制终端的从端,并关闭主端描述符的副本。
通过 BSD 接口,forkpty()
结合了 openpty()
、fork()
和 login_tty()
。它 returns 两次;一次在 parent(returning child 进程的 PID),一次在 child(returning 零)。 child 正在 运行 正在新会话中,伪终端从属端作为其控制终端,已经重定向到标准流。
Is there any practical difference between openpty() + login_tty() vs posix_openpt() + grantpt() [ + unlockpt() + opening the slave side]?
不,不是真的。
两者都有Linux 并且大多数 BSD 倾向于同时提供两者。 (在Linux中,使用BSD接口时,需要在libutil库中link(-lutil
gcc选项),但它是由提供标准C库的同一个包提供的, 并且可以假定始终可用。)
我更喜欢 POSIX 界面,尽管它更冗长,但除了有点喜欢 POSIX 界面而不是 BSD 界面之外,我什至不知道为什么我更喜欢它通过 BSD 接口。 BSD forkpty()
一次调用即可完成最常见用例的所有工作!
此外,我倾向于先尝试 Linux-specific ioctl,而不是依赖 ptsname()
(或 GNU ptsname_r() 扩展),如果它看起来可用的话,如果不可用,则回退到 ptsname()
。所以,如果有的话,我可能应该更喜欢 BSD 界面。但是 libutil
有点让我烦恼,我想,所以我没有。
我绝对不反对其他人更喜欢BSD界面。如果有的话,我对我的偏好是如何存在的感到有点困惑;通常我更喜欢更简单、更健壮的界面,而不是冗长、复杂的界面。
Does there have to be a running process associated with or slave master tty at all times?
必须有一个进程打开伪终端的主端。当描述符的最后一个副本关闭时,内核会销毁该对。
此外,如果具有主端描述符的进程不从中读取,伪终端中的进程 运行ning 将意外阻塞某些 ncurses 调用。通常,调用不会阻塞(或者只阻塞很短的时间,比人类注意到的要短)。如果进程只是读取但丢弃了输入,那么我们实际上并不知道ncurses终端的内容!
因此,我们可以说,绝对需要有一个从伪终端对主端读取的进程,并保持对主端开放的描述符。
(slave端不同,因为字符设备节点通常是可见的,进程可以暂时关闭与伪终端的连接,稍后再重新打开。在Linux中,当没有进程有打开从属端的描述符,从主控端读取或写入的进程将出现 EIO 错误(read() 和 write() returning -1 with errno==EIO)。我不是很确定不过,如果这是有保证的行为;到目前为止还没有依赖它,我自己最近才注意到它(在实施示例时)。
这是一个 ncurses 应用程序的示例,它在作为参数提供的每个终端上动画一个弹跳的 X:
// SPDX-License-Identifier: CC0-1.0
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <sys/ioctl.h>
#include <locale.h>
#include <curses.h>
#include <time.h>
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>
#ifndef FRAMES_PER_SECOND
#define FRAMES_PER_SECOND 25
#endif
#define FRAME_DURATION (1.0 / (double)(FRAMES_PER_SECOND))
/* Because the terminals are not the controlling terminal for this process,
* this process may not receive the SIGWINCH signal whenever a screen size
* changes. Therefore, we call this function to update it whenever we switch
* between terminals.
*/
extern void _nc_update_screensize(SCREEN *);
/*
* Signal handler to notice if this program - all its terminals -- should exit.
*/
static volatile sig_atomic_t done = 0;
static void handle_done(int signum)
{
done = signum;
}
static int install_done(int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = 0;
return sigaction(signum, &act, NULL);
}
/* Difference in seconds between to timespec structures.
*/
static inline double difftimespec(const struct timespec after, const struct timespec before)
{
return (double)(after.tv_sec - before.tv_sec)
+ (double)(after.tv_nsec - before.tv_nsec) / 1000000000.0;
}
/* Sleep the specified number of seconds using nanosleep().
*/
static inline double nsleep(const double seconds)
{
if (seconds <= 0.0)
return 0.0;
const long sec = (long)seconds;
long nsec = (long)(1000000000.0 * (seconds - (double)sec));
if (nsec < 0)
nsec = 0;
if (nsec > 999999999)
nsec = 999999999;
if (sec == 0 && nsec < 1)
return 0.0;
struct timespec req = { .tv_sec = (time_t)sec, .tv_nsec = nsec };
struct timespec rem = { .tv_sec = 0, .tv_nsec = 0 };
if (nanosleep(&req, &rem) == -1 && errno == EINTR)
return (double)(rem.tv_sec) + (double)(rem.tv_nsec) / 1000000000.0;
return 0.0;
}
/*
* Structure describing each client (terminal) state.
*/
struct client {
SCREEN *term;
FILE *in;
FILE *out;
int col; /* Ball column */
int row; /* Ball row */
int dcol; /* Ball direction in column axis */
int drow; /* Ball direction in row axis */
};
static size_t clients_max = 0;
static size_t clients_num = 0;
static struct client *clients = NULL;
/* Add a new terminal, based on device path, and optionally terminal type.
*/
static int add_client(const char *ttypath, const char *term)
{
if (!ttypath || !*ttypath)
return errno = EINVAL;
if (clients_num >= clients_max) {
const size_t temps_max = (clients_num | 15) + 13;
struct client *temps;
temps = realloc(clients, temps_max * sizeof clients[0]);
if (!temps)
return errno = ENOMEM;
clients_max = temps_max;
clients = temps;
}
clients[clients_num].term = NULL;
clients[clients_num].in = NULL;
clients[clients_num].out = NULL;
clients[clients_num].col = 0;
clients[clients_num].row = 0;
clients[clients_num].dcol = +1;
clients[clients_num].drow = +1;
clients[clients_num].in = fopen(ttypath, "r+");
if (!clients[clients_num].in)
return errno;
clients[clients_num].out = fopen(ttypath, "r+");
if (!clients[clients_num].out) {
const int saved_errno = errno;
fclose(clients[clients_num].in);
return errno = saved_errno;
}
clients[clients_num].term = newterm(term, clients[clients_num].in,
clients[clients_num].out);
if (!clients[clients_num].term) {
fclose(clients[clients_num].out);
fclose(clients[clients_num].in);
return errno = ENOMEM;
}
set_term(clients[clients_num].term);
start_color();
cbreak();
noecho();
nodelay(stdscr, TRUE);
keypad(stdscr, TRUE);
scrollok(stdscr, FALSE);
curs_set(0);
clear();
refresh();
clients_num++;
return 0;
}
static void close_all_clients(void)
{
while (clients_num > 0) {
clients_num--;
if (clients[clients_num].term) {
set_term(clients[clients_num].term);
endwin();
delscreen(clients[clients_num].term);
clients[clients_num].term = NULL;
}
if (clients[clients_num].in) {
fclose(clients[clients_num].in);
clients[clients_num].in = NULL;
}
if (clients[clients_num].out) {
fclose(clients[clients_num].out);
clients[clients_num].out = NULL;
}
}
}
int main(int argc, char *argv[])
{
struct timespec curr, prev;
int arg;
if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", arg0);
fprintf(stderr, " %s TERMINAL [ TERMINAL ... ]\n", arg0);
fprintf(stderr, "\n");
fprintf(stderr, "This program displays a bouncing ball animation in each terminal.\n");
fprintf(stderr, "Press Q or . in any terminal, or send this process an INT, HUP,\n");
fprintf(stderr, "QUIT, or TERM signal to quit.\n");
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
setlocale(LC_ALL, "");
for (arg = 1; arg < argc; arg++) {
if (add_client(argv[arg], NULL)) {
fprintf(stderr, "%s: %s.\n", argv[arg], strerror(errno));
close_all_clients();
return EXIT_FAILURE;
}
}
if (install_done(SIGINT) == -1 ||
install_done(SIGHUP) == -1 ||
install_done(SIGQUIT) == -1 ||
install_done(SIGTERM) == -1) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
close_all_clients();
return EXIT_FAILURE;
}
clock_gettime(CLOCK_MONOTONIC, &curr);
while (!done && clients_num > 0) {
size_t n;
/* Wait until it is time for the next frame. */
prev = curr;
clock_gettime(CLOCK_MONOTONIC, &curr);
nsleep(FRAME_DURATION - difftimespec(curr, prev));
/* Update each terminal. */
n = 0;
while (n < clients_num) {
int close_this_terminal = 0;
int ch, rows, cols;
set_term(clients[n].term);
/* Because the terminal is not our controlling terminal,
we may miss SIGWINCH window size change signals.
To work around that, we explicitly check it here. */
_nc_update_screensize(clients[n].term);
/* Process inputs - if we get any */
while ((ch = getch()) != ERR)
if (ch == 'x' || ch == 'X' || ch == 'h' || ch == 'H')
clients[n].dcol = -clients[n].dcol;
else
if (ch == 'y' || ch == 'Y' || ch == 'v' || ch == 'V')
clients[n].drow = -clients[n].drow;
else
if (ch == '.' || ch == 'q' || ch == 'Q')
close_this_terminal = 1;
if (close_this_terminal) {
endwin();
delscreen(clients[n].term);
fclose(clients[n].in);
fclose(clients[n].out);
/* Remove from array. */
clients_num--;
clients[n] = clients[clients_num];
clients[clients_num].term = NULL;
clients[clients_num].in = NULL;
clients[clients_num].out = NULL;
continue;
}
/* Obtain current terminal size. */
getmaxyx(stdscr, rows, cols);
/* Leave a trace of dots. */
if (clients[n].row >= 0 && clients[n].row < rows &&
clients[n].col >= 0 && clients[n].col < cols)
mvaddch(clients[n].row, clients[n].col, '.');
/* Top edge bounce. */
if (clients[n].row <= 0) {
clients[n].row = 0;
clients[n].drow = +1;
}
/* Left edge bounce. */
if (clients[n].col <= 0) {
clients[n].col = 0;
clients[n].dcol = +1;
}
/* Bottom edge bounce. */
if (clients[n].row >= rows - 1) {
clients[n].row = rows - 1;
clients[n].drow = -1;
}
/* Right edge bounce. */
if (clients[n].col >= cols - 1) {
clients[n].col = cols - 1;
clients[n].dcol = -1;
}
clients[n].row += clients[n].drow;
clients[n].col += clients[n].dcol;
mvaddch(clients[n].row, clients[n].col, 'X');
refresh();
/* Next terminal. */
n++;
}
}
close_all_clients();
return EXIT_SUCCESS;
}
它不包含伪终端,唯一真正的怪癖是使用 _nc_update_screensize()
来检测是否有任何终端发生了变化。 (因为它们不是我们的控制终端,我们没有收到 SIGWINCH 信号,因此 ncurses 错过了 window 更改。)
我建议用 gcc -Wall -Wextra -O2 bounce.c -lncurses -o bounce
编译它。
打开几个终端 windows 和 运行 tty
以查看到它们的控制终端的路径(通常是伪终端的从端,/dev/pts/N
)。
运行 ./bounce
将这些路径中的一个或多个作为参数,然后开始弹跳。
如果您不希望 window 中的 shell 消耗您的输入,并希望上面的程序看到它,运行 例如sleep 6000
在终端 windows 之前 运行 执行上述命令。
这个程序简单地向每个终端打开两个流,并让 ncurses 控制它们;基本上,它是一个多终端 ncurses 应用程序的示例,以及如何使用 newterm()
、set_term()
等来处理它们。
如果您多次提供同一个终端,按 Q 将以随机顺序关闭它们,因此 ncurses 可能无法将终端正确恢复到原始状态。 (您可能需要盲目地键入 reset
,以将终端重置为可用状态;它是 clear
的伴随命令,它只是清除终端。他们不做任何其他事情,只是终端的东西.)
该程序可以一直 运行 而不是将终端设备的路径作为命令行参数提供,而是监听传入的 Unix 域数据报, SOL_SOCKET-level SCM_RIGHTS-type 辅助数据,可用于在不相关进程之间复制文件描述符。
但是,如果像这样放弃对终端的控制(通过打开终端,或将终端文件描述符传递给另一个进程),问题是无法撤销该访问。我们可以通过在两者之间使用一个伪终端,并在伪终端和我们的真实终端之间代理数据来避免这种情况。要断开连接,我们只需停止代理数据并销毁伪终端对,并将我们的终端恢复到初始状态。
检查上面的程序,我们看到控制新终端的伪代码过程是
获取终端的两个FILE流句柄。
以上程序使用 fopen()
正常打开它们。其他程序可以使用 dup()
复制单个描述符,并使用 fdopen()
将它们转换为 stdio FILE 流句柄。
致电 SCREEN *term = newterm(NULL, in, out)
让 ncurses 知道这个新终端。
in
和 out
是两个 FILE 流句柄。第一个参数是终端类型字符串;如果为 NULL,则使用 TERM 环境变量。今天的典型值是 xterm-256color
,但 ncurses 也支持许多其他类型的终端。
调用 set_term(term)
使新终端成为当前活动终端。
此时,我们可以进行正常的 ncurses 设置,例如 cbreak(); noecho();
等等。
解除终端控制也很简单:
调用 set_term(term)
使该终端成为当前活动终端。
调用 endwin()
和 delscreen(term)
.
关闭两个FILE流到终端。
更新终端内容需要一个循环,每次迭代处理一个终端,从 set_term(term)
调用开始(如果我们希望对 window 做出反应,则随后是 _nc_update_screensize(term)
调用这些终端的尺寸发生变化)。
上面的示例程序使用 nodelay()
模式,因此 getch()
将 return 按键,或者 ERR
如果当前终端没有待处理的输入. (至少在 Linux 中,每当 window 大小发生变化时,只要终端是我们的控制终端,或者我们调用 _nc_update_screensize()
,我们就会得到 KEY_RESIZE
。)
但请注意:如果还有其他进程也从该终端读取数据,比如 shell,则任何进程都可以读取输入。
我有一个(遗留)程序,它充当守护进程(从某种意义上说,它 运行 永远等待服务请求)但它有一个基于 ncurses 的用户界面 运行在主机上。
我想修改程序,如果我通过 ssh 连接到主机,我可以按需启用用户界面。 我知道至少有一种使用伪终端的方法,但我不太确定如何实现它。 我认为有两种应用程序行为很有趣:
运行 UI 仅当应用程序 运行 在终端
的前台运行时- 如果应用程序 运行 在终端的前台 - 显示 UI
- 如果应用程序 运行 在后台 - 不显示 UI
- 如果应用程序移至后台 - 关闭 UI
- 如果应用程序移动到终端的前台 - 打开 UI
当有人连接到服务器时按需创建一个新的UI
- 应用程序运行正在后台运行
- 新用户登录本机
- 他们 运行 导致 UI 的一个实例在他们的终端中打开
- 多个用户可以拥有自己的UI实例。
备注
有一种简单的方法可以使用 screen 来做到这一点。所以:
原文:
screen mydaemon etc...
新的 ssh 会话:
screen -d
screen -r
这会分离屏幕,将其留在后台 运行ning,然后将其重新连接到当前终端。关闭终端时,屏幕会话会分离,因此效果很好。
我想了解 screen 在幕后做了什么,既是为了我自己的教育,也是为了了解您如何将这些功能放入应用程序本身。
我知道如何为通过套接字连接的服务器执行此操作。我想了解的是原则上如何使用伪终端完成此操作。这确实是一种使应用程序运行的奇怪方式,但我认为它有助于深入探索使用伪终端的功能和局限性。
对于第一种情况,我假设我希望 ncurses UI 运行ning 在主端传递输入的从属终端中。
主进程会使用 isatty() 之类的东西来检查它当前是否在终端的前台,并使用 newterm() 和 endwin() 激活或停用 UI。
我一直在尝试这个,但我还没有让它工作,因为终端和 ncurses 的某些方面我充其量还没有掌握,最坏的情况是根本误解。
伪代码是:
openpty(masterfd,slavefd)
login_tty();
fork();
ifslave
close(stdin)
close(stdout)
dup_a_new_stdin_from_slavefd();
newterm(NULL, newinfd, newoutfd); (
printw("hello world");
insert_uiloop_here();
endwin();
else ifmaster
catchandforwardtoslave(SIGWINCH);
while(noexit)
{
docommswithslave();
forward_output_as_appropriate();
}
通常我会得到
问题
- 上面的伪代码有什么问题?
- 我的主从端是否正确?
- login_tty 在这里实际做什么?
- openpty() + login_tty() 与 posix_openpt() + grantpt() 之间有什么实际区别吗?
- 是否必须始终有一个 运行ning 进程关联或从主 tty?
注意:这是与
这是一个很好的问题,也是为什么我们有伪终端的一个很好的例子。
为了使守护程序能够使用 ncurses 接口,它需要一个伪终端(伪终端对的从属端),它从守护程序开始执行时一直可用,直到守护程序退出。
要使伪终端存在,必须有一个进程对伪终端对的主控方具有打开的描述符。此外,它必须消耗伪终端从属端的所有输出(ncurses 输出的可见内容)。通常,像 vterm 这样的库用于解释该输出以将实际文本帧缓冲区“绘制”到一个数组中(好吧,通常是两个数组 - 一个用于每个单元格(特定行和列)中显示的宽字符,另一个用于颜色等属性)。
为了使伪终端对正常工作,主端的进程是 parent 或从端进程 运行ning ncurses 的祖先,或者两者完全无关. slave端的进程运行ning ncurses应该在一个新的会话中,以伪终端作为其控制终端。如果我们使用在 child 进程中启动守护进程的小型伪终端“服务器”,这是最容易实现的;事实上,这是通常用于伪终端的模式。
第一种情况不太可行,因为没有 parent/master 维护伪终端的进程。
我们可以提供第一种情况的行为,通过添加一个小的pseudoterminal-providing“看门人”进程,其任务是维护存在的伪终端对,并使用伪终端对中进程 运行ning 生成的任何 ncurses 输出。
但是,这种行为也符合第二种情况。
换句话说,这是可行的:
我们使用一个自定义程序,比如 'janitor',而不是直接启动守护进程,它创建一个伪终端并 运行 在该伪终端中设置守护进程。
只要守护进程 运行s.
Janitor 就会停留 运行ningJanitor 为其他进程提供了一个接口来“连接”到伪终端对的主端。
这并不一定意味着 1:1 数据代理。通常提供给守护进程的输入(按键)未经修改,但是伪终端“framebuffer”的内容,character-based 虚拟 window 内容,如何传输确实有所不同。这完全在我们自己的掌控之中。
要连接到管理员,我们需要第二个帮助程序。
在'screen'的情况下,这两个程序实际上是同一个二进制文件;该行为仅由 command-line 参数控制,按键由 'screen' 本身“消耗”,以控制 'screen' 行为,而不传递给实际的 ncurses-based 进程 运行在伪终端中。
到目前为止,我们只能检查 tmux or screen 来源,看看他们是如何做到上述的;这是非常简单的终端多路复用的东西。
但是,这里有一个非常有趣的地方,我以前没有考虑过;这点小事让我明白了这个问题相当重要的核心:
Multiple users can have their own instances of the UI.
一个进程只能有一个控制终端。这指定了某种关系。例如,当控制终端的主控端关闭时,伪终端对消失,伪终端对的从属端打开的描述符变得无效(如果我没记错的话,所有操作都会产生 EIO);但不仅如此,进程组中的每个进程都会收到一个 HUP 信号。
ncurses newterm() 函数允许进程在 运行 时间连接到现有终端或伪终端。该终端不需要是控制终端,ncurses-using 进程也不需要属于该会话。重要的是要认识到,在这种情况下,标准流(标准输入、输出和错误)不会重定向到终端。
所以,如果有办法告诉守护进程它有一个可用的新伪终端,并且应该打开它,因为有一个用户想要使用守护进程提供的界面,我们可以打开守护进程并按需关闭伪终端!
但是请注意,这需要在守护进程和用于连接到守护进程提供的 ncurses-based UI 的进程之间显式 co-operation。对于任意 ncurses-based 进程或守护进程,没有标准的方法可以做到这一点。例如,据我所知, nano
和 top
没有提供这样的接口;他们只使用与标准流关联的伪终端。
发布此答案后 – 希望在问题结束之前速度足够快因为其他人看不到问题的有效性及其对其他 server-side POSIXy 开发人员的用处 – 我将构建一个示例程序对来举例说明上述内容;可能使用 Unix 域套接字作为“请为该用户新建 UI”通信通道,因为文件描述符可以使用 Unix 域套接字作为辅助数据传递,并且套接字两端的用户身份可以待验证(凭证辅助数据)。
不过,现在让我们回到提出的问题。
What is wrong with the above pseudo code? [Typically I either get a segfault inside fileno_unlocked() in newterm() or output on the invoking terminal rather than a new invisible terminal.]
newinfd
和 newoutfd
应该与伪终端从端文件描述符相同(或 dup()s),slavefd
.
我认为还应该有一个明确的 set_term()
,其中 SCREEN 指针 return 由 newterm() 作为参数编辑。 (它可能会自动调用 newterm() 提供的第一个终端,但我宁愿明确调用它。)
newterm()
连接并准备一个新终端。这两个描述符通常都指的是伪终端对的同一个从端; infd
可以是从中接收用户按键的一些其他描述符。
ncurses 中一次只能激活一个终端。您需要使用 set_term()
到 select 哪个会受到以下 printw()
等调用的影响。 (它 return 是以前活动的终端,因此可以对另一个终端进行更新,然后 return 回到原来的终端。)
(这也意味着如果一个程序提供了多个终端,它必须在它们之间循环,检查输入,并以相对较高的频率更新每个终端,这样人类用户就会觉得 UI反应灵敏,而不是“滞后”。狡猾的 POSIX 程序员可以 select 或轮询底层描述符,并且只在输入待处理的终端之间循环。)
Do I have the master and slave ends the right way around?
是的,我相信你会。从端是看到终端的,可以使用 ncurses。主端是提供按键的端,并使用 ncurses 输出做 某事(比如,将它们绘制到 text-based 帧缓冲区,或代理到远程终端)。
What does login_tty actually do here?
有两种常用的伪终端接口:UNIX98(在POSIX中标准化)和BSD。
使用 POSIX 接口,posix_openpt()
创建一个新的伪终端对,并且 return 将描述符发送到它的主端。关闭此描述符(最后打开的副本)会破坏该对。在 POSIX 模型中,从属端最初是“锁定的”,无法打开。 unlockpt()
移除此锁,允许打开从属端。 grantpt()
更新字符设备(对应伪终端对的从端)所有权和模式以匹配当前真实用户。 unlockpt()
和 grantpt()
可以按任何顺序调用,但先调用 grantpt()
是有意义的;这样,在正确设置其所有权和访问模式之前,其他进程不能“意外地”打开从属端。 POSIX 通过 ptsname()
提供了对应于伪终端对的从端的字符设备的路径,但是 Linux 提供了一个 TIOCGPTPEER ioctl (在内核 4.13 和更高版本中)允许即使字符设备节点未显示在当前挂载命名空间中,也打开从端。
通常,grantpt()
、unlockpt()
和打开伪终端对的从端是在 child 进程中完成的(仍然可以访问 master-side描述符)已使用 setsid()
启动新会话。 child 进程将标准流(标准输入、输出和错误)重定向到伪终端的从属端,关闭它的 master-side 描述符副本,并确保伪终端是它的控制终端。通常这之后是执行将使用伪终端(通常通过 ncurses)作为其用户界面的二进制文件。
使用 BSD 接口,openpty()
创建伪终端对,为双方提供打开的文件描述符,并可选择设置伪终端 termios 设置和 window 大小。它大致对应于 POSIX posix_openpt()
+ grantpt()
+ unlockpt()
+ 打开伪终端对的从属端 + 可选地设置 termios 设置和终端 window 大小。
使用BSD接口,login_tty
在child进程中是运行。 运行s setsid()
创建一个新会话,使从端成为控制终端,将标准流重定向到控制终端的从端,并关闭主端描述符的副本。
通过 BSD 接口,forkpty()
结合了 openpty()
、fork()
和 login_tty()
。它 returns 两次;一次在 parent(returning child 进程的 PID),一次在 child(returning 零)。 child 正在 运行 正在新会话中,伪终端从属端作为其控制终端,已经重定向到标准流。
Is there any practical difference between openpty() + login_tty() vs posix_openpt() + grantpt() [ + unlockpt() + opening the slave side]?
不,不是真的。
两者都有Linux 并且大多数 BSD 倾向于同时提供两者。 (在Linux中,使用BSD接口时,需要在libutil库中link(-lutil
gcc选项),但它是由提供标准C库的同一个包提供的, 并且可以假定始终可用。)
我更喜欢 POSIX 界面,尽管它更冗长,但除了有点喜欢 POSIX 界面而不是 BSD 界面之外,我什至不知道为什么我更喜欢它通过 BSD 接口。 BSD forkpty()
一次调用即可完成最常见用例的所有工作!
此外,我倾向于先尝试 Linux-specific ioctl,而不是依赖 ptsname()
(或 GNU ptsname_r() 扩展),如果它看起来可用的话,如果不可用,则回退到 ptsname()
。所以,如果有的话,我可能应该更喜欢 BSD 界面。但是 libutil
有点让我烦恼,我想,所以我没有。
我绝对不反对其他人更喜欢BSD界面。如果有的话,我对我的偏好是如何存在的感到有点困惑;通常我更喜欢更简单、更健壮的界面,而不是冗长、复杂的界面。
Does there have to be a running process associated with or slave master tty at all times?
必须有一个进程打开伪终端的主端。当描述符的最后一个副本关闭时,内核会销毁该对。
此外,如果具有主端描述符的进程不从中读取,伪终端中的进程 运行ning 将意外阻塞某些 ncurses 调用。通常,调用不会阻塞(或者只阻塞很短的时间,比人类注意到的要短)。如果进程只是读取但丢弃了输入,那么我们实际上并不知道ncurses终端的内容!
因此,我们可以说,绝对需要有一个从伪终端对主端读取的进程,并保持对主端开放的描述符。
(slave端不同,因为字符设备节点通常是可见的,进程可以暂时关闭与伪终端的连接,稍后再重新打开。在Linux中,当没有进程有打开从属端的描述符,从主控端读取或写入的进程将出现 EIO 错误(read() 和 write() returning -1 with errno==EIO)。我不是很确定不过,如果这是有保证的行为;到目前为止还没有依赖它,我自己最近才注意到它(在实施示例时)。
这是一个 ncurses 应用程序的示例,它在作为参数提供的每个终端上动画一个弹跳的 X:
// SPDX-License-Identifier: CC0-1.0
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <sys/ioctl.h>
#include <locale.h>
#include <curses.h>
#include <time.h>
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>
#ifndef FRAMES_PER_SECOND
#define FRAMES_PER_SECOND 25
#endif
#define FRAME_DURATION (1.0 / (double)(FRAMES_PER_SECOND))
/* Because the terminals are not the controlling terminal for this process,
* this process may not receive the SIGWINCH signal whenever a screen size
* changes. Therefore, we call this function to update it whenever we switch
* between terminals.
*/
extern void _nc_update_screensize(SCREEN *);
/*
* Signal handler to notice if this program - all its terminals -- should exit.
*/
static volatile sig_atomic_t done = 0;
static void handle_done(int signum)
{
done = signum;
}
static int install_done(int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = 0;
return sigaction(signum, &act, NULL);
}
/* Difference in seconds between to timespec structures.
*/
static inline double difftimespec(const struct timespec after, const struct timespec before)
{
return (double)(after.tv_sec - before.tv_sec)
+ (double)(after.tv_nsec - before.tv_nsec) / 1000000000.0;
}
/* Sleep the specified number of seconds using nanosleep().
*/
static inline double nsleep(const double seconds)
{
if (seconds <= 0.0)
return 0.0;
const long sec = (long)seconds;
long nsec = (long)(1000000000.0 * (seconds - (double)sec));
if (nsec < 0)
nsec = 0;
if (nsec > 999999999)
nsec = 999999999;
if (sec == 0 && nsec < 1)
return 0.0;
struct timespec req = { .tv_sec = (time_t)sec, .tv_nsec = nsec };
struct timespec rem = { .tv_sec = 0, .tv_nsec = 0 };
if (nanosleep(&req, &rem) == -1 && errno == EINTR)
return (double)(rem.tv_sec) + (double)(rem.tv_nsec) / 1000000000.0;
return 0.0;
}
/*
* Structure describing each client (terminal) state.
*/
struct client {
SCREEN *term;
FILE *in;
FILE *out;
int col; /* Ball column */
int row; /* Ball row */
int dcol; /* Ball direction in column axis */
int drow; /* Ball direction in row axis */
};
static size_t clients_max = 0;
static size_t clients_num = 0;
static struct client *clients = NULL;
/* Add a new terminal, based on device path, and optionally terminal type.
*/
static int add_client(const char *ttypath, const char *term)
{
if (!ttypath || !*ttypath)
return errno = EINVAL;
if (clients_num >= clients_max) {
const size_t temps_max = (clients_num | 15) + 13;
struct client *temps;
temps = realloc(clients, temps_max * sizeof clients[0]);
if (!temps)
return errno = ENOMEM;
clients_max = temps_max;
clients = temps;
}
clients[clients_num].term = NULL;
clients[clients_num].in = NULL;
clients[clients_num].out = NULL;
clients[clients_num].col = 0;
clients[clients_num].row = 0;
clients[clients_num].dcol = +1;
clients[clients_num].drow = +1;
clients[clients_num].in = fopen(ttypath, "r+");
if (!clients[clients_num].in)
return errno;
clients[clients_num].out = fopen(ttypath, "r+");
if (!clients[clients_num].out) {
const int saved_errno = errno;
fclose(clients[clients_num].in);
return errno = saved_errno;
}
clients[clients_num].term = newterm(term, clients[clients_num].in,
clients[clients_num].out);
if (!clients[clients_num].term) {
fclose(clients[clients_num].out);
fclose(clients[clients_num].in);
return errno = ENOMEM;
}
set_term(clients[clients_num].term);
start_color();
cbreak();
noecho();
nodelay(stdscr, TRUE);
keypad(stdscr, TRUE);
scrollok(stdscr, FALSE);
curs_set(0);
clear();
refresh();
clients_num++;
return 0;
}
static void close_all_clients(void)
{
while (clients_num > 0) {
clients_num--;
if (clients[clients_num].term) {
set_term(clients[clients_num].term);
endwin();
delscreen(clients[clients_num].term);
clients[clients_num].term = NULL;
}
if (clients[clients_num].in) {
fclose(clients[clients_num].in);
clients[clients_num].in = NULL;
}
if (clients[clients_num].out) {
fclose(clients[clients_num].out);
clients[clients_num].out = NULL;
}
}
}
int main(int argc, char *argv[])
{
struct timespec curr, prev;
int arg;
if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", arg0);
fprintf(stderr, " %s TERMINAL [ TERMINAL ... ]\n", arg0);
fprintf(stderr, "\n");
fprintf(stderr, "This program displays a bouncing ball animation in each terminal.\n");
fprintf(stderr, "Press Q or . in any terminal, or send this process an INT, HUP,\n");
fprintf(stderr, "QUIT, or TERM signal to quit.\n");
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
setlocale(LC_ALL, "");
for (arg = 1; arg < argc; arg++) {
if (add_client(argv[arg], NULL)) {
fprintf(stderr, "%s: %s.\n", argv[arg], strerror(errno));
close_all_clients();
return EXIT_FAILURE;
}
}
if (install_done(SIGINT) == -1 ||
install_done(SIGHUP) == -1 ||
install_done(SIGQUIT) == -1 ||
install_done(SIGTERM) == -1) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
close_all_clients();
return EXIT_FAILURE;
}
clock_gettime(CLOCK_MONOTONIC, &curr);
while (!done && clients_num > 0) {
size_t n;
/* Wait until it is time for the next frame. */
prev = curr;
clock_gettime(CLOCK_MONOTONIC, &curr);
nsleep(FRAME_DURATION - difftimespec(curr, prev));
/* Update each terminal. */
n = 0;
while (n < clients_num) {
int close_this_terminal = 0;
int ch, rows, cols;
set_term(clients[n].term);
/* Because the terminal is not our controlling terminal,
we may miss SIGWINCH window size change signals.
To work around that, we explicitly check it here. */
_nc_update_screensize(clients[n].term);
/* Process inputs - if we get any */
while ((ch = getch()) != ERR)
if (ch == 'x' || ch == 'X' || ch == 'h' || ch == 'H')
clients[n].dcol = -clients[n].dcol;
else
if (ch == 'y' || ch == 'Y' || ch == 'v' || ch == 'V')
clients[n].drow = -clients[n].drow;
else
if (ch == '.' || ch == 'q' || ch == 'Q')
close_this_terminal = 1;
if (close_this_terminal) {
endwin();
delscreen(clients[n].term);
fclose(clients[n].in);
fclose(clients[n].out);
/* Remove from array. */
clients_num--;
clients[n] = clients[clients_num];
clients[clients_num].term = NULL;
clients[clients_num].in = NULL;
clients[clients_num].out = NULL;
continue;
}
/* Obtain current terminal size. */
getmaxyx(stdscr, rows, cols);
/* Leave a trace of dots. */
if (clients[n].row >= 0 && clients[n].row < rows &&
clients[n].col >= 0 && clients[n].col < cols)
mvaddch(clients[n].row, clients[n].col, '.');
/* Top edge bounce. */
if (clients[n].row <= 0) {
clients[n].row = 0;
clients[n].drow = +1;
}
/* Left edge bounce. */
if (clients[n].col <= 0) {
clients[n].col = 0;
clients[n].dcol = +1;
}
/* Bottom edge bounce. */
if (clients[n].row >= rows - 1) {
clients[n].row = rows - 1;
clients[n].drow = -1;
}
/* Right edge bounce. */
if (clients[n].col >= cols - 1) {
clients[n].col = cols - 1;
clients[n].dcol = -1;
}
clients[n].row += clients[n].drow;
clients[n].col += clients[n].dcol;
mvaddch(clients[n].row, clients[n].col, 'X');
refresh();
/* Next terminal. */
n++;
}
}
close_all_clients();
return EXIT_SUCCESS;
}
它不包含伪终端,唯一真正的怪癖是使用 _nc_update_screensize()
来检测是否有任何终端发生了变化。 (因为它们不是我们的控制终端,我们没有收到 SIGWINCH 信号,因此 ncurses 错过了 window 更改。)
我建议用 gcc -Wall -Wextra -O2 bounce.c -lncurses -o bounce
编译它。
打开几个终端 windows 和 运行 tty
以查看到它们的控制终端的路径(通常是伪终端的从端,/dev/pts/N
)。
运行 ./bounce
将这些路径中的一个或多个作为参数,然后开始弹跳。
如果您不希望 window 中的 shell 消耗您的输入,并希望上面的程序看到它,运行 例如sleep 6000
在终端 windows 之前 运行 执行上述命令。
这个程序简单地向每个终端打开两个流,并让 ncurses 控制它们;基本上,它是一个多终端 ncurses 应用程序的示例,以及如何使用 newterm()
、set_term()
等来处理它们。
如果您多次提供同一个终端,按 Q 将以随机顺序关闭它们,因此 ncurses 可能无法将终端正确恢复到原始状态。 (您可能需要盲目地键入 reset
,以将终端重置为可用状态;它是 clear
的伴随命令,它只是清除终端。他们不做任何其他事情,只是终端的东西.)
该程序可以一直 运行 而不是将终端设备的路径作为命令行参数提供,而是监听传入的 Unix 域数据报, SOL_SOCKET-level SCM_RIGHTS-type 辅助数据,可用于在不相关进程之间复制文件描述符。
但是,如果像这样放弃对终端的控制(通过打开终端,或将终端文件描述符传递给另一个进程),问题是无法撤销该访问。我们可以通过在两者之间使用一个伪终端,并在伪终端和我们的真实终端之间代理数据来避免这种情况。要断开连接,我们只需停止代理数据并销毁伪终端对,并将我们的终端恢复到初始状态。
检查上面的程序,我们看到控制新终端的伪代码过程是
获取终端的两个FILE流句柄。
以上程序使用
fopen()
正常打开它们。其他程序可以使用dup()
复制单个描述符,并使用fdopen()
将它们转换为 stdio FILE 流句柄。致电
SCREEN *term = newterm(NULL, in, out)
让 ncurses 知道这个新终端。in
和out
是两个 FILE 流句柄。第一个参数是终端类型字符串;如果为 NULL,则使用 TERM 环境变量。今天的典型值是xterm-256color
,但 ncurses 也支持许多其他类型的终端。调用
set_term(term)
使新终端成为当前活动终端。此时,我们可以进行正常的 ncurses 设置,例如
cbreak(); noecho();
等等。
解除终端控制也很简单:
调用
set_term(term)
使该终端成为当前活动终端。调用
endwin()
和delscreen(term)
.关闭两个FILE流到终端。
更新终端内容需要一个循环,每次迭代处理一个终端,从 set_term(term)
调用开始(如果我们希望对 window 做出反应,则随后是 _nc_update_screensize(term)
调用这些终端的尺寸发生变化)。
上面的示例程序使用 nodelay()
模式,因此 getch()
将 return 按键,或者 ERR
如果当前终端没有待处理的输入. (至少在 Linux 中,每当 window 大小发生变化时,只要终端是我们的控制终端,或者我们调用 _nc_update_screensize()
,我们就会得到 KEY_RESIZE
。)
但请注意:如果还有其他进程也从该终端读取数据,比如 shell,则任何进程都可以读取输入。