python urwid timeout on idle

105 Views Asked by At

Is there a way to have a urwid app to do a sys.exit() after a configurable timeout if no input has been received from the user in more than 30 seconds?

We are facing network outages and that leads to the SSH Session being dropped but the client program keeps running and holds a Database lock and manually killing is the only option for now and hence this requirement.

2

There are 2 best solutions below

0
StephenK On BEST ANSWER

You can set an alarm in the main loop that will call whatever code you want when it times out. Here I call a function that uses the ExitMainLoop exception, but sys.exit() would also work.

This code cancels the previous alarm (if any) when keyboard input happens, then sets a new alarm. As long as keys are coming in, the alarm should never go off.

Internally, as of late 2020, for alarms urwid seems to use Python's time.time(), which is not guaranteed to only go forward one-second-per-second. The alarm might go off early, exiting the program, if the system clock gets adjusted forward (by NTP?).

import urwid

timeout_time=30

def urwid_exit(loop, user_data):
    raise urwid.ExitMainLoop
    
def handle_input(input):
    global txt
    global loop
    
    #don't reset the alarm on a mouse click,  
    #  and don't try to display it (the .set_text would error if given a tuple)
    if not type(input) is tuple:
        if hasattr(handle_input, "last_alarm") and handle_input.last_alarm:
            loop.remove_alarm(handle_input.last_alarm)
        handle_input.last_alarm = loop.set_alarm_in(timeout_time, urwid_exit)
        txt.set_text("key pressed: %s" % input)

txt = urwid.Text("Hello world")
fill = urwid.Filler(txt, 'top')
loop = urwid.MainLoop(fill, unhandled_input=handle_input)
#call handle input once without any input to get the alarm started
handle_input(None)
loop.run()
0
exquo On

A slight variation on StephenK's answer is to use loop.event_loop.enter_idle(callback) instead of unhandled_input. The callback function will be run whenever urwid enters an idle state, including after processing a keypress event. This is somewhat more general: the timer starts after all activity has finished. (Say, the last keypress starts an action that takes many seconds to finish)

The relevant documentation is at https://urwid.readthedocs.io/en/latest/reference/main_loop.html

import urwid

timeout = 10
txt = urwid.Text(
        'This program will exit after '
        f'_{timeout}_ seconds of inactivity '
        '(no keypresses, etc)\n',
        align='center'
        )
fill = urwid.Filler(txt, 'top')
loop = urwid.MainLoop(fill)

alarm_handle = None

def alarm_callback(_loop, _user_data):
    raise urwid.ExitMainLoop

def idle_callback():
    global alarm_handle
    loop.remove_alarm(alarm_handle)  # remove old alarm
    alarm_handle = loop.set_alarm_in(timeout, alarm_callback)
    text = txt.get_text()[0] + f"\nAlarm has been reset _{alarm_handle[1]}_ times"
    txt.set_text(text)

loop.event_loop.enter_idle(idle_callback)
loop.run()