诅咒在哪里为终局和刷新注入KEY_RESIZE



运行此 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 代码中是没有必要的?

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

initscrgetch手册页都提到了这一SIGWINCH功能。

至于"哪里",它这样做:这是在_nc_update_screensize中,它检查信号处理程序设置的标志,并从包括doupdate(refreshgetch调用)在内的多个地方调用。 如果屏幕尺寸发生了变化,这将注入一个KEY_RESIZE是否真的有SIGWINCH

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

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

托马斯回答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 库调用fifo_pushncurses/base/lib_getch.c中定义的读取输入流。当您阻止getch时,该功能具有阻止read

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

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

目前为止,一切都好。但是,如果我们在安装SIGWINCH处理程序时使用了SA_RESTART呢?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 不打扰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
}

最新更新