0

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.

  • 2
    I think you should do the scraping in the background thread and the input loop in main instead. – moooeeeep Jul 11 '17 at 16:10
  • I'm already doing the scraping on a background thread, but it could terminate in a way that I'm able to detect on the main thread. I'm still left with the issue that the main thread would be waiting for input after the background thread is complete. This is due to the blocking nature of the `input` method in python, so I'm fairly certain that that needs to change, but I'm not sure what to. – Jake Conkerton-Darby Jul 11 '17 at 16:18
  • Do you use Linux? https://stackoverflow.com/q/3762881/1025391 – moooeeeep Jul 12 '17 at 08:06
  • Afraid not, while I prefer Linux myself, this is on a work PC that runs Windows 10. – Jake Conkerton-Darby Jul 12 '17 at 08:08
  • I should mention as well that I have looked at the `msvcrt` module and it doesn't appear to provide the functionality I want. It appears to only offer catching one character at a time, rather than a line of input, and while I know I could use this to put together an input line, that may well be more hassle than everything is worth. – Jake Conkerton-Darby Jul 12 '17 at 08:35

1 Answers1

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.