Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get keyboard event in console with python

Tags:

python

I want to deal with keyboard event in console with python. The running script has some persistent output stream, when admin trigger a keypress event, the script will change its output content.

I have done it with code as follows(press 'q' will trigger the output-change), but there are two issues

  1. there is an increased space in my output. After debug, i find the code "tty.setraw(fd)" lead to that, But i don't know how to solve it
  2. ctrl+c couldn't work anymore (if # "tty.setraw(fd)", ctrl+c will work)

If it is too complex, any else module could do what I want ? I tried curse module, it seems that will freeze the window-output and couldn't coordinate in mutlithread

#!/usr/bin/python
import sys
import select
import tty, termios
import threading
import time

def loop():

    while loop_bool:
        if switch:   
            output = 'aaaa'
        else:
            output = 'bbbb'
        print output
        time.sleep(0.2)


def change():
    global switch
    global loop_bool    
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)

    try: 
        while loop_bool:
            tty.setraw(fd)
            i,o,e = select.select([sys.stdin],[],[],1)
            if len(i)!=0:
                if i[0] == sys.stdin:
                    input = sys.stdin.read(1)

                    if input =='q':
                        if switch:
                            switch = False                                         
                        else: 
                            switch =  True


        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    except KeyboardInterrupt:

        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        loop_bool = False


try:   
    switch = True
    loop_bool = True    
    t1=threading.Thread(target=loop)
    t2=threading.Thread(target=change)        

    t1.start()
    t2.start()

    t1.join(1)
    t2.join(1)
except KeyboardInterrupt:

    loop_bool = False
like image 216
user1675167 Avatar asked Jan 03 '13 06:01

user1675167


1 Answers

This probably depends on what platform you're on, and maybe even what terminal emulator you're using, and I'm not sure whether it will solve your problem or not, but…

You should be able to get character-by-character input without calling tty.setraw, just by setting "canonical mode" off, which you do by masking out the ICANON bit in the lflag attribute of tcgetattr(). You may also need to set VMIN or VTIME attributes, but the defaults should already be correct.

For details, see the section "Canonical and noncanonical mode" in the linux man page, "Noncanonical Mode Input Processing" in the OS X man page, or the equivalent if you're on a different platform.

It's probably cleaner to write this as a context manager than to do explicit cleanup. Especially since your existing code does setraw each time through the loop, and only restores at the end; they should ideally be in matched pairs, and using a with statement guarantees that. (Also, this way you don't need to repeat yourself in the except clause and the normal flow.) So:

@contextlib.contextmanager
def decanonize(fd):
    old_settings = termios.tcgetattr(fd)
    new_settings = old_settings[:]
    new_settings[3] &= ~termios.ICANON
    termios.tcsetattr(fd, termios.TCSAFLUSH, new_settings)
    yield
    termios.tcsetattr(fd, termios.TCSAFLUSH, old_settings)

Now:

def change():
    global switch
    global loop_bool

    with decanonize(sys.stdin.fileno()):
        try:
            while loop_bool:
                i,o,e = select.select([sys.stdin],[],[],1)
                if i and i[0] == sys.stdin:
                    input = sys.stdin.read(1)                    
                    if input =='q':
                        switch = not switch
        except KeyboardInterrupt:
            loop_bool = False

Or maybe you want the with block at a lower level (inside the while, or at least the try).

(PS, I transformed a few lines of your code into equivalent but simpler forms to remove a few levels of nesting.)

YMMV, but here's a test on my Mac:

Retina:test abarnert$ python termtest.py 
aaaa
aaaa
aaaa
qbbbb
bbbb
bbbb
qaaaa
aaaa
aaaa
^CRetina:test abarnert$

This makes me think you might want to turn off input echo (which you do by new_settings[3] &= ~termios.ECHO), which implies that you probably want to replace the decanonize function with something more general, for temporarily setting or clearing arbitrary termios flags. (Also, it would be nice if tcgetattr returned a namedtuple instead of a list, so you could do new_settings.lflag instead of new_settings[3], or at least provided symbolic constants for the attribute indices.)

Meanwhile, from your comments, it sounds like ^C only works during the first second or two, and it has something to do with the timeout in the joins. This makes sense—the main thread just kicks off the two threads, does two join(1) calls, and then finishes. So, 2.something seconds after startup, it's finished all of its work—and left the try: block—so there's no longer any way for a KeyboardInterrupt to trigger the loop_bool = False and signal the worker threads to quit.

I'm not sure why you have timeouts on the joins in the first place, and what's supposed to happen when they time out, but there are three possibilities:

  1. You don't want to quit until ^C, and the timeouts aren't there for any good reason. So take them out. Then the main thread will wait forever for the other two threads to finish, and it's still inside the try block, so ^C should be able to set loop_bool = False.

  2. The app is supposed to exit normally after 2 seconds. (I'm guessing you would have preferred a single join-any or join-all on the pair of threads, with a 2-second timeout, but because Python doesn't have any easy way to do that, you joined the threads sequentially.) In this case, you want to set loop_bool = False as soon as the timeouts end. So just change the except to a finally.

  3. The timeouts are always supposed to be generous enough (presumably this is just a simplified version of your real app), and if you get past the timeout, that's an exceptional condition. The previous option may or may not still work. If it doesn't, set daemon = True on the two threads, and they'll be killed (not nicely asked to shut down) when the main thread finishes. (Note that the way this works is a bit different on Windows vs. Unix—although you presumably don't care much about Windows for this app. More importantly, all the docs say is "The entire Python program exits when no alive non-daemon threads are left," so you shouldn't count on any daemon threads being able to do any cleanup, but also shouldn't count on them not doing any cleanup. Don't do anything in a daemon thread that could leave temporary files around, crucial log messages unwritten, etc.)

like image 118
abarnert Avatar answered Oct 27 '22 03:10

abarnert