curses在endwin上哪里注入KEY_RESIZE并刷新

Where does curses inject KEY_RESIZE on endwin and refresh

运行 此 Python 编码并调整大小 window。它将获得 KEY_RESIZE 并退出。

import curses
import signal

stdscr = None

def handler(num, _):
    curses.endwin()
    stdscr.refresh()

stdscr = curses.initscr()
curses.cbreak()
stdscr.keypad(1)
stdscr.refresh()
signal.signal(signal.SIGWINCH, handler)
while True:
    ch = stdscr.getch()
    if ch == curses.KEY_RESIZE: break
curses.endwin()

这个KEY_RESIZE注入的位置?


我也用C代码测试过:

#include <ncurses.h>
#include <signal.h>

WINDOW *stdscr = NULL;

void handler(int num) {
    endwin();
    wrefresh(stdscr);
}

int main()
{
    stdscr = initscr();
    cbreak();
    keypad(stdscr, 1);
    wrefresh(stdscr);
    signal(SIGWINCH, handler);
    while (1) {
        int ch = wgetch(stdscr);
        if (ch == KEY_RESIZE) break;
    }
    endwin();
    return 0;
}

运行 它并调整它的大小,然后 按一个键 ,它会 KEY_RESIZE 退出。为什么我们在C代码中必须按一个键才能得到一个KEY_RESIZE,而在Python代码中却不需要?

这是设计使然...因为 curses 没有任何类似于事件循环的东西,它必须告诉应用程序检测到 SIGWINCH,并且 curses 库已经更新了它的数据结构使用新的终端尺寸。 (在应用程序中有一个信号处理程序,应用程序可以使用它来 告诉 curses 库不会更好地工作,无论如何让 curses 库做这件事更简单)。

initscr and getch 手册页提到了此 SIGWINCH 功能。

至于 "where",它是这样做的:在 _nc_update_screensize 中,它检查信号处理程序设置的标志,并从多个地方调用,包括 doupdaterefreshgetch 调用)。如果屏幕尺寸已更改,这将注入一个 KEY_RESIZE,无论实际上是否存在 SIGWINCH

现在...可以通过从新建立的处理程序调用原始处理程序来拥有信号处理程序 。 (在 C 程序中调用 signal returns 当前处理程序的地址)。 ncurses 只会在初始化时添加它的处理程序,所以一种(不太可能)的可能性是 python 代码在添加自己的处理程序时可能正在重用底层处理程序。

但是,示例中存在一个更大的问题:它们在信号处理程序中进行 curses 调用。这在 C 中是不安全的(ncurses 的信号处理程序只设置一个标志的原因)。也许在 python 中,这些处理程序被包装了——或者只是由于时间的原因,你会得到意想不到的行为。

Thomas 回答了 KEY_RESIZE 的来源。这对我调试 C 代码和回答关于按键的第二个问题来说是一个很好的介绍。

简短的答案作为工作代码给出(在信号处理程序中调用 ncurses 函数可能会引起问题,请参阅 Thomas 更新的答案,但似乎不是问题的根本原因):

#include <ncurses.h>
#include <signal.h>

WINDOW *stdscr = NULL;

void handler(int num) {
    endwin();
    wrefresh(stdscr);
}

int main()
{
    stdscr = initscr();
    cbreak();
    keypad(stdscr, 1);
    wrefresh(stdscr);

    struct sigaction old_action;
    struct sigaction new_action;
    new_action.sa_handler = handler;
    new_action.sa_flags = 0;        //  !
    sigemptyset(&new_action.sa_mask);
    sigaction(SIGWINCH, &new_action, &old_action);

    while (1) {
        int ch = wgetch(stdscr);
        if (ch == KEY_RESIZE) break;
    }
    endwin();
    return 0;
}

冗长的回答很乏味。

基本上,ncurses 希望在没有 SA_RESTART 标志的情况下安装 SIGWINCH 处理程序。

ncurses 库调用 ncurses/base/lib_getch.c 中定义的 fifo_push 来读取输入流。当您阻塞 getch.

时,该函数具有阻塞 read

SIGWINCH 上,此调用被中断,并且 returns -1errno 设置为 EINTR

ncurses 库将在 _nc_wgetch 中处理此问题,它调用 _nc_handle_sigwinch 来检查 SIGWINCH 是否发生。如果是这样,它会调用 _nc_update_screensizeungetch a KEY_RESIZE.

到目前为止一切顺利。但是,如果我们在安装 SIGWINCH 处理程序时使用了 SA_RESTART 怎么办? read 系统调用将在中断时重新启动。这就是为什么 C 程序在 window 调整大小后不会立即退出,而是必须读取另一个按键。

更有趣的是,ncurses 希望在安装信号处理程序时设置 SA_RESTART(在 ncurses/tty/lib_tstp.c 中):

Note: This code is fragile! Its problem is that different OSs handle restart of system calls interrupted by signals differently. The ncurses code needs signal-call restart to happen -- otherwise, interrupted wgetch() calls will return FAIL, probably making the application think the input stream has ended and it should terminate. In particular, you know you have this problem if, when you suspend an ncurses-using lynx with ^Z and resume, it dies * immediately.

但是SIGWINCH是个例外...

#ifdef SA_RESTART
#ifdef SIGWINCH
    if (sig != SIGWINCH)
#endif
    new_act.sa_flags |= SA_RESTART;
#endif /* SA_RESTART */

原始 C 代码失败,因为 man signal 说:

By default, in glibc 2 and later, the signal() wrapper function does not invoke the kernel system call. Instead, it calls sigaction(2) using flags that supply BSD semantics.

BSD 语义是:

sa.sa_flags = SA_RESTART;

Python? Python 不理会 SA_RESTART:

PyOS_sighandler_t
PyOS_setsig(int sig, PyOS_sighandler_t handler)
{
#ifdef HAVE_SIGACTION
    ...
    struct sigaction context, ocontext;
    context.sa_handler = handler;
    sigemptyset(&context.sa_mask);
    context.sa_flags = 0;
    if (sigaction(sig, &context, &ocontext) == -1)
        return SIG_ERR;
    return ocontext.sa_handler;
#else
    ...
#endif
}