问题
So, I have a small console app I'm working on that runs a web-scraping process, and I want to be able to give it console commands mid-execution to control it. To do this I'll need some form of non-blocking keyboard input, as the program may self-terminate due to unexpected errors, and I don't want some thread hanging about and waiting for input when termination has occurred.
I have the following already hashed out:
import threading
import time
import queue
input_queue = queue.Queue()
command_input_event = threading.Event()
def kbdListener():
global input_queue, command_input_event
kbdInput = ''
while kbdInput.lower() not in ['quit', 'exit', 'stop']:
kbdInput = input("> ")
input_queue.put(kbdInput)
command_input_event.set()
input_queue.join()
listener = threading.Thread(target=kbdListener)
listener.start()
stop = False
while not stop:
if command_input_event.is_set():
while not input_queue.empty():
command = input_queue.get()
if command.lower() in ['quit', 'exit', 'stop']:
print('Stopping')
while not input_queue.empty():
input_queue.get()
input_queue.task_done()
input_queue.task_done()
stop = True
break
else:
print('Command "{}" received and processed'.format(command))
input_queue.task_done()
My problem is that on the line while not stop:
there will be another condition being checked in my program, that determines if the main loop has terminated. If this eventuality was to occur then the main thread would stop, but the background listener
thread would still be waiting for input; the situation I'm trying to avoid.
I'm not tied in to this approach, so if there is some alternative method for getting a non-blocking input, then I would be open to that advice as well.
回答1:
So after some work on this I came up with the following, which allows for a background thread input, which is then processed by the foreground thread, but is entirely non-blocking and updates as the user types in real-time.
import threading
import queue
import sys
from msvcrt import getch, kbhit
"""
Optional extra that I found necessary to get ANSI commands working on windows:
import colorama
colorama.init()
"""
class ClassWithNonBlockingInput:
def __init__(self):
self.command_queue = queue.Queue()
self.command_available_event = threading.Event()
self.stop_event = threading.Event()
def run(self):
main_loop = threading.Thread(target=self.main_loop)
main_loop.start()
listener = threading.Thread(target=self.keyboard_listener)
listener.start()
stop = False
while main_loop.is_alive() and not stop:
if self.command_available_event.is_set():
while not self.command_queue.empty():
command = self.command_queue.get()
#Process command here, long jobs should be on a seperate thread.
self.command_queue.task_done()
self.command_available_event.clear()
def main_loop(self):
#Main processing loop, may set self.stop_event at some point to terminate early.
pass
def keyboard_listener(self):
line = []
line_changed = False
while not self.stop_event.is_set():
while kbhit():
c = getch()
if c == b'\x00' or c == b'\xe0':
#This is a special function key such as F1 or the up arrow.
#There is a second identifier character to clear/identify the key.
id = getch()
#Process the control key by sending commands as necessary.
elif c == b'\x08':
#Backspace character, remove last character.
if len(line) > 0:
line = line[:-1]
line_changed = True
elif c == b'\x7f':
#ctrl-backspace, remove characters until last space.
while len(line) > 0 and line[-1] != b' ':
line = line[:-1]
line_changed = True
elif c == b'\x1b':
#Escacpe key, process as necessary.
pass
elif c == b'\r':
#Enter key, send command to main thread.
print()
command = b''.join(line).decode('utf-8')
self.command_queue.put(command)
self.command_available_event.set()
self.command_queue.join()
line = []
line_changed = True
else:
#Append all other characters to the current line.
#There may be other special keys which need to be considered,
# this is left as an exercise for the reader :P
line.append(c)
line_changed = True
if line_changed:
#Clear the current output line
print('\033[2K', end='\r')
print(b''.join(line).decode('utf-8'), end='')
sys.stdout.flush()
line_changed = False
This should give anyone who encounters this problem in the future, and stumbles upon this question a good head start.
来源:https://stackoverflow.com/questions/45039513/how-can-i-make-a-background-non-blocking-input-loop-in-python