Search code examples
pythonncursescurses

addstr causes getstr to return on signal


I have a reworked python curses code with two 'threads' basically. They are not real threads - one main subwidow processing function, and the second, a different subwindow processing function, executing on the timer. And I ran into an interesting effect:

  • The main window code is waiting for the user's input using getstr().
  • At the same time a timer interrupt would come and the interrupt code would output something in a different subwidow.
  • The output from a timer function will cause getstr() to return with empty input.

What can be causing this effect? Is there any way to avoid this effect other than checking the return string?


Sample code to reproduce the problem:

#!/usr/bin/env python
# Simple code to show timer updates

import curses
import os, signal, sys, time, traceback
import math

UPDATE_INTERVAL = 2
test_bed_windows = []
global_count = 0

def signal_handler(signum, frame):
    global test_bed_windows
    global global_count

    if (signum == signal.SIGALRM):
        # Update all the test bed windows
        # restart the timer.
        signal.alarm(UPDATE_INTERVAL)
        global_count += 1

        for tb_window in test_bed_windows:
            tb_window.addstr(1, 1, "Upd: {0}.{1}".format(global_count, test_bed_windows.index(tb_window)))
            tb_window.refresh()
    else:
        print("unexpected signal: {0}".format(signam))
        pass

def main(stdscr):
    # window setup
    screen_y, screen_x = stdscr.getmaxyx()
    stdscr.box()

    # print version
    version_str = " Timer Demo v:0 "
    stdscr.addstr(0, screen_x - len(version_str) - 1, version_str)
    stdscr.refresh()

    window = stdscr.subwin(screen_y-2,screen_x-2,1,1)

    for i in range(3):
        subwin = window.derwin(3,12,1,2 + (15*i))

        test_bed_windows.append(subwin)
        subwin.box()
        subwin.refresh()

    signal.signal(signal.SIGALRM, signal_handler)
    signal.alarm(UPDATE_INTERVAL)

    # Output the prompt and wait for the input:
    window.addstr(12, 1, "Enter Q/q to exit\n")
    window.refresh()

    the_prompt = "Enter here> "
    while True:
        window.addstr(the_prompt)
        window.refresh()

        curses.echo()
        selection = window.getstr()
        curses.noecho()

        if selection == '':
            continue
        elif selection.upper() == 'Q':
            break
        else:
            window.addstr("Entered: {0}".format(selection))
            window.refresh()


if __name__ == '__main__':
    curses.wrapper(main)

Solution

  • I suspect it's not the writing to the subwindow that's causing the getstr() to return an empty string, but rather the alarm signal itself. (Writing to a window from within an signal handler may not be well defined either, but that's a separate issue.)

    The C library Curses, which Python's curses module is often built on top of, will return from most of its blocking input calls when any signal (other than a few it handles internally) comes in. In C there's a defined API for the situation (the function returns -1 and sets errno to EINTER).

    The Python module says it will raise an exception if a curses function returns with an error. I'm not sure why it is not doing so in this case.

    Edit: A possible solution is to use a more programmer-friendly console UI library than curses. Urwid appears (in my brief browsing of the manual) to support event-driven UI updates (including updates on a timer) while simultaneously handling keyboard input. It may be easier to learn that than to deal with the sketchy and poorly documented interactions between signals and curses.

    Edit: from the man page for the getch():

    The behavior of getch and friends in the presence of handled signals is unspecified in the SVr4 and XSI Curses documentation. Under historical curses implementations, it varied depending on whether the operating system's implementation of handled signal receipt interrupts a read(2) call in progress or not, and also (in some implementations) depending on whether an input timeout or non-blocking mode has been set.

    Programmers concerned about portability should be prepared for either of two cases: (a) signal receipt does not interrupt getch; (b) signal receipt interrupts getch and causes it to return ERR with errno set to EINTR. Under the ncurses implementation, handled signals never interrupt getch.

    I have tried using getch instead of getstr and it does return -1 on a signal. That (negative return value) would solve this problem if it was implemented by getstr. So now the option are (1) writing your own getstr with error handling or (2) using Urwid. Could this be a bug for Python library?