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
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
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 join
s. 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 join
s in the first place, and what's supposed to happen when they time out, but there are three possibilities:
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
.
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
.
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.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With