诅咒在哪里赢取并刷新KEY_RESIZE

时间:2018-12-17 20:10:18

标签: ncurses python-curses

运行此Python代码并调整大小窗口。它将得到一个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代码中却不需要?

2 个答案:

答案 0 :(得分:1)

这是设计使然...由于curses没有事件循环之类的东西,它必须告诉应用程序检测到SIGWINCH,并且curses库已更新其数据结构以使其正常工作新的终端尺寸。 (在应用程序中有一个信号处理程序,该应用程序可以用来对curses库进行 tell 的工作不会更好,而让curses库执行此操作更简单。)

initscrgetch手册页提到了此SIGWINCH功能。

对于“ where”,它是这样做的:位于_nc_update_screensize中,它检查信号处理程序设置的标志,并从包括doupdate(其中refresh和{ getch个电话)。如果屏幕尺寸已更改,那么无论实际上是否存在KEY_RESIZE,这都会注入一个SIGWINCH

现在...通过从新建立的处理程序中调用原始处理程序,可以有信号处理程序 chain 。 (在C程序中调用signal将返回当前处理程序的地址)。 ncurses只会在初始化时添加其处理程序,因此一种(不太可能)的可能性是python代码在添加自己的处理程序时可能正在重用基础处理程序。

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

答案 1 :(得分:0)

托马斯回答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期望安装SIGWINCH处理程序时不带有SA_RESTART标志。

ncurses库调用fifo_push中定义的ncurses/base/lib_getch.c来读取输入流。而且,当您阻止read时,该函数就会阻止getch

SIGWINCH上,此调用被中断,并返回-1errno设置为EINTR的情况。

ncurses库将在_nc_wgetch中处理此问题,该库将调用_nc_handle_sigwinch来检查是否发生了SIGWINCH。如果是这样,它将调用_nc_update_screensizeungetchKEY_RESIZE

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

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

  

注意:此代码易碎!问题是不同的操作系统     处理信号中断的系统调用的重新启动的方式有所不同。     ncurses代码需要重新启动信号调用-否则,     中断的wgetch()调用将返回FAIL,可能是     应用程序认为输入流已经结束,应该     终止。特别是,您知道如果在以下情况下会遇到此问题     你用^ Z挂起一个使用ncurses的天猫并继续,它死了      *立即。

但是SIGWINCH是一个例外...

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

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

  

默认情况下,在glibc 2和更高版本中,signal()包装函数不会调用内核系统调用。相反,它使用提供BSD语义的标志调用sigaction(2)。

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
}