Edit: Clarifying that this is for Python 3. Some other posts on SO were for Python 2 and don't work in Python 3.
I'm writing a bot in Python. The bot will spin up some background threads to do things like poll a service, perform API calls, etc. The main thread of this bot is a CLI that implements a sort of "REPL" loop (enter a command and parameters, something happens, wait for another command, rinse and repeat.)
There are two situations in which the bot needs to terminate:
- If the user types a quit command. In this instance, the main loop will set flags telling the processing threads to terminate and join those threads; once all threads terminate the application exits properly.
- If one of the processing threads decides that the bot needs to terminate. For example, if a request for the bot to disconnect is given by an admin via the external interface - and not via the CLI.
Here is some very rough pseudocode:
import threading
class Bot:
KeepRunning = True
def api_thread(self):
# we can assume this loop will execute at least once per second or so
while self.KeepRunning:
shouldQuit = pollTheApiAndDoThings()
if shouldQuit:
self.KeepRunning = False
return
def service_thread(self):
# we can assume this loop will execute at least once per second or so
while self.KeepRunning:
shouldQuit = connectToAServiceAndDoThings()
if shouldQuit:
self.KeepRunning = False
return
def stop(self):
self.KeepRunning = False
self._api_thread.join()
self._service_thread.join()
def run(self):
self._api_thread = threading.Thread(target=self.api_thread)
self._service_thread = threading.Thread(target=self.service_thread)
self._api_thread.start()
self._service_thread.start()
if __name__ == "__main__":
b = Bot()
b.run()
while b.KeepRunning:
cmd = input("> ")
if cmd.lower().rstrip()=="quit":
b.stop() # wait for bot to stop
exit(0)
elif ... # process all other user commands
# if keepRunning got set to false while we were in the input loop, don't repeat the loop.
# however, we want some way to end the input() call immediately if keepRunning gets set false.
print("Remote shutdown requested. Exiting now.")
exit(0)
This code works properly if the user types quit. Each thread will exit on its next iteration, and the bot will exit.
However, if one of the bot threads needs to tell the bot to shutdown, the program will stay running until the user presses Enter at least once. Also, since the bot has already basically terminated, any command would need to be ignored.
If there is no CLI, then this application will work properly (e.g. replacing the input loop with just a time.sleep(1) so that the main thread stays alive until a bot thread requests exit). So, what I need logically, is a way for the input() function to work similarly to a loop that has a sleep-then-check-flag idea going on.
I've seen other posts about doing timeouts on input(), but the difference here is that there is no specific timeout. As long as the bot is running, we can block on input() for as long as needed. But as soon as one of the threads signals an exit, the input() call needs to be canceled. Even if the user has already input something, it is OK to ignore it since the bot has already shut down anyway.
This needs to be able to work both on Windows and on Unix-based OSes with the same code. Checking the OS with platform or similar is of course acceptable.
I've considered using an approach like this one and writing my own version of input() but this seems awfully cumbersome and error-prone, especially cross-platform. It also would need to include a read timeout, so that it won't just become the same as input(), and also I would need to manually code things like Ctrl+C handling - not ideal. And it also would preclude passing strings via stdin, e.g. using subprocess - because as far as I know that won't work if you're doing raw terminal interaction(?)