c-将终端连接到作为守护进程运行的进程(以运行ncurses UI)



我有一个(遗留)程序,它充当守护进程(从某种意义上说,它永远运行,等待服务请求),但它有一个在主机上运行的基于ncurses的用户界面。

我想更改程序,这样,如果我通过ssh连接到主机,我就可以根据需要启用用户界面。我知道至少有一种方法可以使用伪终端,但我不太确定如何实现。我认为有两种应用程序行为很有趣:

仅当应用程序在终端的前台运行时才运行UI

  1. 如果应用程序在终端的前台运行-显示UI
  2. 如果应用程序在后台运行,则不显示UI
  3. 如果应用程序被移到后台-关闭UI
  4. 如果应用程序被移动到终端的前台-打开UI

当有人连接到服务器时按需创建新的UI

  1. 应用程序正在后台运行
  2. 新用户登录到计算机
  3. 他们运行一些东西,导致UI的实例在他们的终端中打开
  4. 多个用户可以拥有自己的UI实例

备注

使用屏幕有一个简单的方法。因此:

原件:

screen mydaemon etc...

新的ssh会话:

screen -d     
screen -r

这将分离屏幕,使其在后台运行,然后将其重新连接到当前终端。关闭终端后,屏幕会话将分离,因此这一操作效果非常好。

我想了解屏幕在幕后的作用,这既是为了我自己的教育,也是为了了解如何将其中的一些功能融入应用程序本身。

我知道如何为通过套接字连接的服务器执行此操作。我想了解的是,原则上如何使用伪终端来实现这一点。这确实是一种让应用程序工作的奇怪方式,但我认为这将有助于深入探索使用伪终端的功能和局限性。

对于第一种情况,我假设我希望ncurses UI在从属终端中运行,主端向它传递输入和从它传递输入

主进程将使用类似isatty()的东西来检查它当前是否在终端的前台,并使用newterm()和endwin()激活或停用UI。

我一直在尝试这一点,但我还没有让它发挥作用,因为终端和网络的某些方面我最好还没有掌握,最坏的情况是根本性的误解。

伪代码是:

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

通常,我要么在newterm()中的filenounlocked()中得到一个segfault或者在调用终端而不是新的不可见终端上输出。

问题

  • 上面的伪代码出了什么问题
  • 我有正确的主人和奴隶的结局吗
  • login_tty在这里实际做什么
  • openpty()+login_ty()与posix_openpt()+grantpt()之间有实际区别吗
  • 是否必须始终有一个与tty相关联的运行进程或从机tty

注意:这是一个不同于ncurses的新术语openpty的问题,它描述了这个用例的一个特定的不正确/不完整的实现,并询问它有什么问题。

这是一个很好的问题,也是为什么我们有伪终端的一个很不错的例子。


为了让守护进程能够使用ncurses接口,它需要一个伪终端(伪终端对的从端),从守护进程开始执行时起,直到守护进程退出,该终端都可用。

为了使伪终端存在,必须有一个进程在伪终端对的主端具有开放描述符。此外,它必须消耗伪终端从端的所有输出(ncurses输出的可见内容)。通常,像vterm这样的库用于将该输出解释为";draw";将实际的文本帧缓冲区转换为一个数组(通常是两个数组,一个用于每个单元格中显示的宽字符(特定行和簇),另一个用于颜色等属性)。

为了使伪终端对正确工作,要么主端的进程是在从端运行ncurses的进程的父进程或祖先,要么两者完全无关。在从端运行ncurses的进程应该处于一个新的会话中,伪终端作为其控制终端。这是最容易实现的,如果我们使用一个小的伪终端";服务器";启动子进程中的守护进程;事实上,这就是通常与伪终端一起使用的模式。

第一种情况实际上并不可行,因为没有维护伪终端的父/主进程。

我们可以通过添加一个小的伪终端来提供第一种场景的行为;看门人;进程,其任务是维护存在的伪终端对,并消耗在伪终端对中运行的进程生成的任何ncurses输出。

然而,这种行为也符合第二种情况。

换句话说,以下是可行的方法:

  1. 我们没有直接启动守护程序,而是使用一个自定义程序,比如"janitor",创建一个伪终端并在该伪终端内运行守护程序。

  2. 只要守护进程运行,Janitor就会一直运行。

  3. Janitor为其他过程提供了一个接口;"连接";到伪终端对的主端。

    这并不一定意味着数据的1:1代理。通常向守护进程提供未经修改的输入(按键);帧缓冲区";,传输的基于字符的虚拟窗口内容确实有所不同。这完全在我们自己的控制之下。

  4. 要连接到看门人,我们需要第二个助手程序。

    在"屏幕"的情况下,这两个程序实际上是相同的二进制程序;行为仅由命令行参数控制;消耗的";通过"screen"本身,控制"screen"行为,而不是传递给在伪终端中运行的实际基于ncurses的进程。

到目前为止,我们只需检查tmux或筛选源代码,就可以了解它们是如何完成上述操作的;它是非常简单的终端多路复用的东西。

然而,这里有一个我以前没有考虑过的非常有趣的地方;这一点点让我明白了这个问题的核心:

多个用户可以拥有自己的UI实例。

一个进程只能有一个控制终端。这指定了某种关系。例如,当控制终端的主端闭合时,伪终端对消失,并且向伪终端对的从端开放的描述符变得不起作用(如果我回忆正确的话,所有操作都会产生EIO);但更重要的是进程组中的每个进程都接收HUP信号。

ncurses newterm()函数允许进程在运行时连接到现有的终端或伪终端。该终端不需要是控制终端,使用进程的ncurses也不需要属于该会话。重要的是要认识到,在这种情况下,标准流(标准输入、输出和错误)被重定向到终端,而不是

因此,如果有一种方法可以告诉守护程序它有一个新的伪终端可用,并且应该打开它,因为有用户想要使用守护程序提供的接口,我们可以让守护程序根据需要打开和关闭伪终端!

但是,请注意,这需要守护进程和用于连接到守护进程提供的基于ncurses的UI的进程之间的明确合作。对于基于任意ncurses的进程或守护进程,没有标准的方法可以做到这一点。例如,据我所知,nanotop没有提供这样的接口;它们仅使用与标准流相关联的伪终端。

在发布这个答案之后——希望在问题结束之前足够快,因为其他人没有看到这个问题的有效性,以及它对其他服务器端POSIXy开发人员的有用性——我将构建一个示例程序对来举例说明以上内容;可能使用Unix域套接字作为";请为该用户提供新的UI;通信通道,因为文件描述符可以使用Unix域套接字作为辅助数据传递,并且可以验证套接字两端用户的身份(凭证辅助数据)。

然而,现在,让我们回到问题上来。

上面的伪代码出了什么问题?[通常,我要么在newterm()中的filenounlocked()中得到一个segfault,要么在调用终端上输出,而不是在一个新的不可见终端上输出。]

newinfdnewoutfd应该与伪终端从端文件描述符slavefd相同(或dup(s))。

我认为还应该有一个显式的set_term(),用newterm()返回的SCREEN指针作为参数。(它可能是为newterm()提供的第一个终端自动调用的,但我宁愿显式调用它。)

CCD_ 7连接到并准备新的终端。这两个描述符通常都指伪终端对的同一从端;CCD_ 8可以是从其中接收用户按键的某个其它描述符。

一次只能有一个终端在ncurses中处于活动状态。您需要使用set_term()来选择哪个会受到以下printw()等调用的影响。(它返回以前激活的终端,这样就可以对另一个终端进行更新,然后返回到原始终端。)

(这也意味着,如果一个程序提供了多个终端,它必须在它们之间循环,检查输入,并以相对较高的频率更新每个终端,这样人类用户就会感觉到UI是响应的,而不是"滞后"。不过,一个狡猾的POSIX程序员可以选择或轮询底层描述符,并且只在有待输入的终端之间循环。)

我有正确的主端和从端吗?

是的,我相信你是这样做的。从端是看到终端的,可以使用ncurses。主端提供按键,并对ncurses输出执行操作(例如,将它们绘制到基于文本的帧缓冲区,或代理到远程终端)。

login_tty在这里实际做什么?

有两个常用的伪终端接口:UNIX98(在POSIX中标准化)和BSD。

使用POSIX接口,posix_openpt()创建一个新的伪终端对,并将描述符返回到其主端。关闭此描述符(最后一个打开的重复项)会破坏对。在POSIX模型中;锁定";,不可处罚。unlockpt()移除该锁,允许从侧打开。grantpt()更新角色设备(对应于伪终端对的从端)所有权和模式以匹配当前真实用户。unlockpt()grantpt()可以按任意顺序调用,但先调用grantpt()是有意义的;这样就不能打开从属侧";意外地";在正确设置其所有权和访问模式之前,由其他进程执行。POSIX通过ptsname()提供到与伪终端对的从端相对应的角色设备的路径,但Linux提供了一个TIOCGPTPEER ioctl(在内核4.13及更高版本中),即使角色设备节点未显示在当前装载命名空间中,也允许打开从端。

通常,grantpt()unlockpt()和打开伪终端对的从端是在已经使用setsid()开始新会话的子进程(仍然可以访问主端描述符)中完成的。子进程将标准流(标准输入、输出和错误)重定向到伪终端的从端,关闭其主端描述符的副本,并确保伪终端是其控制终端。通常,这之后执行二进制文件,该二进制文件将使用伪终端(通常通过ncurses)作为其用户界面。

使用BSD接口,openpty()创建伪终端对,向双方提供打开的文件描述符,并可选地设置伪终端终端设置和窗口大小。它大致对应于POSIXposix_openpt()+grantpt()+unlockpt()+打开伪终端对的从端+可选地设置终端设置和终端窗口大小。

通过BSD接口,login_tty在子进程中运行。它运行setsid()创建一个新的会话,使从端成为控制终端,将标准流重定向到控制终端的从端,并关闭主端描述符的副本。

通过BSD接口,forkpty()组合了openpty()fork()login_tty()。它返回两次;一次在父进程(返回子进程的PID),一次在子进程(返回零)。子进程正在一个新的会话中运行,伪终端从端作为其控制终端,已经重定向到标准流。

openpty()+login_ty()与posix_openpt()+grantpt()[+unlockpt()+打开从端]之间有实际区别吗?

不,不是真的。

Linux和大多数BSD都倾向于同时提供这两种功能。(在Linux中,当使用BSD接口时,您需要在libutil库(-lutilgcc选项)中进行链接,但它是由提供标准C库的同一个包提供的,可以假设它始终可用。)

我倾向于喜欢POSIX接口,尽管它要详细得多,但除了有点喜欢POSIX而不是BSD接口之外,我甚至不知道为什么我更喜欢它而非BSD接口。BSDforkpty()基本上在一次调用中完成了最常见用例的所有操作!

此外,与其依赖ptsname()(或GNU ptsname_r()扩展),我倾向于首先尝试Linux特定的ioctl(如果它看起来可用),如果它不可用,则返回ptsname()。所以,如果有什么不同的话,我可能更喜欢BSD接口。。但我想libutil有点让我恼火,所以我没有。

我绝对不反对其他人更喜欢BSD接口。如果有什么不同的话,我有点困惑于我的偏好是如何存在的;通常,比起冗长复杂的接口,我更喜欢更简单、更健壮的接口。

是否必须始终有一个与tty关联的运行进程或从主机tty?

必须有一个进程打开伪终端的主端。当描述符的最后一个副本被关闭时,内核会销毁这对描述符。

此外,如果具有主端描述符的进程没有从中读取,则在伪终端中运行的进程将意外地阻塞某些ncurses调用。通常情况下,呼叫不会阻塞(或者只阻塞很短的时间,比人类注意到的要短)。如果进程只是读取但丢弃了输入,那么我们实际上并不知道ncurses终端的内容!

因此,我们可以说,绝对需要一个从伪终端对主端读取的进程,保持描述符对主端开放。

(从端不同;因为字符设备节点通常是可见的,所以进程可以暂时关闭与伪终端的连接,稍后再重新打开。在Linux中,当没有进程在从端有打开的描述符时,从主端读取或写入的进程将得到EIO错误(读取()和写入()返回-1,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错过了窗口更改。)

我建议使用gcc -Wall -Wextra -O2 bounce.c -lncurses -o bounce进行编译。

打开几个终端窗口,运行tty以查看到其控制终端(通常是伪终端的从属端,/dev/pts/N)的路径。以其中一条或多条路径作为参数运行./bounce,然后开始反弹。

如果您不希望窗口中的shell使用您的输入,并且希望上面的程序看到它,请在运行上面的命令之前,在终端窗口中运行例如sleep 6000

这个程序只需向每个终端打开两个流,并让ncurses控制它们;基本上,它是一个多终端ncurses应用程序的例子,以及如何使用newterm()set_term()等来处理它们

如果您多次提供相同的终端,按Q会以随机顺序关闭它们,因此ncurses可能无法将终端正确恢复到原始状态。(您可能需要盲目地键入reset,将终端重置为可工作状态;这是clear的配套命令,只会清除终端。它们不做任何其他事情,只做终端的事情。)

该程序可以一直运行,而不是作为命令行参数提供到终端设备的路径,而是监听传入的Unix域数据报,带有SOL_SOCKET级SCM_RIGHTS类型的辅助数据,这些数据可用于在不相关的进程之间复制文件描述符。

然而,如果像这样重新屏蔽对终端的控制(通过打开终端,或者通过将终端文件描述符传递给另一个进程),问题是不可能撤销该访问。我们可以通过在两者之间使用伪终端,并在伪终端和我们的真实终端之间代理数据来避免这种情况。要断开连接,我们只需停止代理数据并销毁伪终端对,然后将终端恢复到其初始状态。

检查上面的程序,我们看到伪代码中控制新终端的过程是

  1. 获取终端的两个FILE流句柄。

    上面的程序使用fopen()像往常一样打开它们。其他程序可以使用dup()复制单个描述符,并使用fdopen()将其转换为stdio FILE流句柄。

  2. 请致电SCREEN *term = newterm(NULL, in, out),让ncurses了解这个新终端。

    CCD_ 50和CCD_ 51是两个FILE流句柄。第一个参数是终端类型字符串;如果为NULL,则使用TERM环境变量。目前的典型值是xterm-256color,但ncurses也支持许多其他类型的终端。

  3. 调用set_term(term),使新终端成为当前激活的终端。

    在这一点上,我们可以进行正常的ncurses设置,比如cbreak(); noecho();等等

重新调整终端控制也很简单:

  1. 调用set_term(term)使该终端成为当前活动的终端。

  2. 呼叫endwin()delscreen(term)

  3. 关闭到终端的两个FILE流。

更新终端内容需要一个循环,每个迭代处理一个终端,从set_term(term)调用开始(如果我们希望对这些终端中的窗口大小变化做出反应,则后面是_nc_update_screensize(term)调用)。

上述示例程序使用nodelay()模式,因此getch()将返回按键,或者如果当前终端没有未决输入,则返回ERR。(至少在Linux中,只要终端是我们的控制终端,或者我们称之为_nc_update_screensize(),只要窗口大小发生变化,我们就会得到KEY_RESIZE。)

但请注意:如果有其他进程也从该终端读取,比如shell,则任何进程都可以读取输入。

最新更新