【问题标题】:attach a terminal to a process running as a daemon (to run an ncurses UI)将终端附加到作为守护进程运行的进程(以运行 ncurses UI)
【发布时间】:2020-12-20 00:29:57
【问题描述】:

我有一个(旧版)程序,它充当守护进程(从某种意义上说,它永远运行等待服务请求),但它有一个在主机上运行的基于 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。

我一直在尝试这个,但我还没有让它工作,因为终端和 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();
  } 

通常我要么得到segfault inside fileno_unlocked() in newterm() 或在调用终端而不是新的不可见终端上输出。

问题

  • 上面的伪代码有什么问题?
  • 我的主从端是否正确?
  • login_tty 在这里实际上做了什么?
  • openpty() + login_tty() 与 posix_openpt() + grantpt() 之间有什么实际区别吗?
  • 是否必须始终存在与主 tty 关联的运行进程或从属主 tty?

注意:这是与 ncurses-newterm-following-openpty 不同的问题,它描述了此用例的特定错误/不完整实现,并询问它有什么问题。

【问题讨论】:

标签: c ncurses pty


【解决方案1】:

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


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

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

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

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

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

但是,这种行为也符合第二种情况。

换一种说法,这是可行的:

  1. 我们不是直接启动守护进程,而是使用自定义程序,比如“看门人”,它创建一个伪终端并在该伪终端内运行守护进程。

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

  3. Janitor 为其他进程“连接”到伪终端对的主端提供了一个接口。

    这并不一定意味着数据的 1:1 代理。通常提供给守护程序的输入(按键)未经修改,但是伪终端“帧缓冲区”的内容(基于字符的虚拟窗口内容)的传输方式却有所不同。这完全在我们自己的控制之下。

  4. 要连接到管理员,我们需要第二个帮助程序。

    在'screen'的情况下,这两个程序实际上是同一个二进制文件;该行为仅由命令行参数控制,并且按键由“屏幕”本身“消耗”以控制“屏幕”行为,而不是传递给在伪终端中运行的基于 ncurses 的实际进程。

到目前为止,我们可以只检查tmuxscreen 来源,看看他们是如何做到以上几点的;这是非常简单的终端多路复用的东西。

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

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

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

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

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

但是请注意,这需要守护程序与用于连接到守护程序提供的基于 ncurses 的 UI 的进程之间的明确合作。对于任意基于 ncurses 的进程或守护进程,没有标准的方法来执行此操作。比如,据我所知nanotop没有提供这样的接口;他们只使用与标准流相关的伪终端。

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

但是,现在,让我们回到之前提出的问题。

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

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

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

newterm() 连接并准备一个新终端。这两个描述符通常都指一个伪终端对的同一个从端; infd 可以是其他一些接收用户按键的描述符。

一次只能在 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() 创建伪终端对,为双方提供打开的文件描述符,并可选择设置伪终端 termios 设置和窗口大小。它大致对应于 POSIX posix_openpt() + grantpt() + unlockpt() + 打开伪终端对的从端 + 可选设置 termios 设置和终端窗口大小。

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

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

openpty() + login_tty() vs posix_openpt() + grantpt() [ + unlockpt() + 打开从端] 有什么实际区别吗?

不,不是。

Linux 和大多数 BSD 都倾向于同时提供两者。 (在 Linux 中,使用 BSD 接口时,需要在 libutil 库中链接(-lutil gcc 选项),但它是由提供标准 C 库的同一包提供的,可以假设始终可用。 )

我倾向于更喜欢 POSIX 接口,尽管它更冗长,但除了有点喜欢 POSIX 接口而不是 BSD 接口之外,我什至不知道为什么我更喜欢它而不是 BSD 接口。 BSD forkpty() 基本上可以在一次调用中为最常见的用例完成所有工作!

另外,我不依赖ptsname()(或 GNU ptsname_r() 扩展),而是倾向于先尝试 Linux 特定的 ioctl(如果它看起来可用),如果可用则回退到 ptsname()无法使用。所以,如果有的话,我可能更喜欢 BSD 界面.. 但是libutil 有点让我烦恼,我猜,所以我不喜欢。

我绝对不反对其他人更喜欢 BSD 界面。如果有的话,我对我的偏好如何存在感到有点困惑;通常我更喜欢更简单、更健壮的接口,而不是冗长、复杂的接口。

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

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

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

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

(slave 端不同;因为字符设备节点通常是可见的,进程可以暂时关闭它与伪终端的连接,稍后再重新打开它。在 Linux 中,当没有进程对从端有打开的描述符时, 读取或写入主控端的进程将出现 EIO 错误(read() 和 write() 返回 -1 且 errno==EIO)。不过,我不确定这是否是有保证的行为;还没有到目前为止一直依赖它,并且我最近才注意到它(在实现示例时)。

【讨论】:

  • 如果您认为该问题有效,您可以投票赞成。这是避免它被关闭的好方法。
  • 您是否设法创建了一个示例?我仍在为此苦苦挣扎。
【解决方案2】:

这是一个 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 了解这个新终端。

    inout 是两个 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 中,只要窗口大小发生变化,我们就会得到KEY_RESIZE,只要终端是我们的控制终端,或者我们调用_nc_update_screensize()。)

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

【讨论】:

  • 1.你和其他答案一样吗?你的名声不一样? 2.您的回答似乎与问题无关。它使用 ncurses 但不使用伪终端对。
  • 其实我可以告诉你,你是同一个 Glarbo,因为你有同一个 identicon。
  • 我认为你的意思是这比直接使用伪终端要好。我想作为这个问题的答案是有道理的,而不会像其他问题那样提高我对伪终端的理解。
  • @BruceAdams:是的,一样(不想注册!)。不,这并不优越,这就是服务如何向需要的用户提供多个并发 ncurses UI 的方式。 (第二组目标中第 4 点的具体示例。)更好的选择是人类用户用于连接到服务的客户端程序,以便 UI 使用伪终端,因为这样客户端程序可以中断连接(并且轻松拆除伪终端对)。我正在构建一个示例,并将其发布在这里,只是假期意味着我现在很忙 IRL。耐心。
  • 圣诞快乐。我也有同样的问题。如果孩子们一天 24 小时都在我身上攀爬,我现在可能已经解决了这一切。
猜你喜欢
  • 2023-03-02
  • 2012-12-03
  • 2011-08-25
  • 1970-01-01
  • 1970-01-01
  • 2011-07-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多