From 8d9771a715cc70ac753d84860fb18a7a3f3611ed Mon Sep 17 00:00:00 2001 From: Carlos Sanchez Date: Mon, 1 May 2023 18:44:14 -0400 Subject: [PATCH] Recent attempts at actual locking --- main.py | 110 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/main.py b/main.py index 0ca9ba2..3ed62fb 100644 --- a/main.py +++ b/main.py @@ -18,10 +18,9 @@ from colorama import Fore, Back, Style, init as colorama_init import contentapi import myutils -VERSION="0.1.0" # Arbitrary but whatever +VERSION="0.1.1" # Arbitrary but whatever CONFIGFILE="config.toml" MAXTITLE=25 -MAXMESSAGEWIDTH=80 # May instead check the console window MSGPREFIX=Back.GREEN + " " + Back.RESET + " " # The entire config object with all defaults @@ -34,6 +33,8 @@ config = { "expire_seconds" : 31536000, # 365 days in seconds, expiration for token "appear_in_global" : False, "print_status_after_insert" : True, + "output_buffer_timeout" : 0.05, # 50 milliseconds + "fixed_width" : 78, # Need room for the pre-message decoration "tokenfile" : ".qcstoken" } @@ -71,7 +72,7 @@ def main(): # Might as well reuse the websocket object for my websocket context data (oops, is that bad?) ws.context = context ws.user_info = context.user_me() - ws.pause_output = False # Whether all output from the websocket should be paused (including status updates) + ws.output_lock = threading.Lock() ws.output_buffer = [] # Individual print statements buffered from output. ws.main_config = config ws.current_room = 0 @@ -109,20 +110,20 @@ def ws_onopen(ws): # The infinite input loop! Or something! while True: - ws.pause_output = False # Allow arbitrary output again. Do this BEFORE other stuff for race conditions - - # We can be certain that at this point, there is no user input (because this loop is all there is) - # As such, just dump the buffer - for output in ws.output_buffer: - printr(output) + # Dump the buffer, if there is any, since we're outside of user input + with ws.output_lock: + for output in ws.output_buffer: + printr(output) - ws.output_buffer = [] + ws.output_buffer = [] - if printstatus: - print_statusline(ws) + if printstatus: + print_statusline(ws) printstatus = False # Assume we are not printing the status every time (it's kinda annoying) + # Listen for a key. The implementation of 'readkey()' on this version of python and windows makes the CPU spike + # to 100%, so we switch to getch() if we detect windows. if platform.system().startswith("Windows"): import msvcrt logging.debug("on windows, using msvcrt.getch") @@ -130,42 +131,43 @@ def ws_onopen(ws): else: key = readchar.readkey() - ws.pause_output = True # Disable output for the duration of input handling - - if key == "h": - print(" -- Help menu / Controls --") - for key, value in commands.items(): - print(" " + Style.BRIGHT + key + Style.NORMAL + " - " + value) - elif key == "s": - search(ws) - printstatus = True - elif key == "g": - ws.send(ws.context.gen_ws_request("userlist", id = "userlist_global")) - elif key == "u": - if not ws.current_room: - print("You're not in a room! Can't check userlist!") - else: - # Just send it out, we have to wait for the websocket handler to get the response - ws.send(ws.context.gen_ws_request("userlist", id = "userlist_room_%d" % ws.current_room)) - elif key == "i": - if not ws.current_room: - print("You're not in a room! Can't send messages!") - else: - message = input("Post (empty = exit): ") - if message: - ws.context.post_message(contentapi.comment_builder(message, - ws.current_room, ws.main_config["default_markup"], ws.user_info["avatar"])) - printstatus = ws.main_config["print_status_after_insert"] - elif key == "t": - print(" -- Ignored WS Data (normal) --") - for key,value in ws.ignored.items(): - printr(Style.BRIGHT + ("%16s" % key) + (" : %d" % value)) - elif key == "q": - print("Quitting (may take a bit for the websocket to close)") - ws.close() - break - elif key == " ": - printstatus = True + # We get exclusive access to output while we're handling user input. This allows us to "pause/buffer" the + # output that might come from websocket events + with ws.output_lock: + if key == "h": + print(" -- Help menu / Controls --") + for key, value in commands.items(): + print(" " + Style.BRIGHT + key + Style.NORMAL + " - " + value) + elif key == "s": + search(ws) + printstatus = True + elif key == "g": + ws.send(ws.context.gen_ws_request("userlist", id = "userlist_global")) + elif key == "u": + if not ws.current_room: + print("You're not in a room! Can't check userlist!") + else: + # Just send it out, we have to wait for the websocket handler to get the response + ws.send(ws.context.gen_ws_request("userlist", id = "userlist_room_%d" % ws.current_room)) + elif key == "i": + if not ws.current_room: + print("You're not in a room! Can't send messages!") + else: + message = input("Post (empty = exit): ") + if message: + ws.context.post_message(contentapi.comment_builder(message, + ws.current_room, ws.main_config["default_markup"], ws.user_info["avatar"])) + printstatus = ws.main_config["print_status_after_insert"] + elif key == "t": + print(" -- Ignored WS Data (normal) --") + for key,value in ws.ignored.items(): + printr(Style.BRIGHT + ("%16s" % key) + (" : %d" % value)) + elif key == "q": + print("Quitting (may take a bit for the websocket to close)") + ws.close() + break + elif key == " ": + printstatus = True # Set the main room; we want to wait until the websocket is open because this also sets your # status in the userlist @@ -211,7 +213,7 @@ def ws_onmessage(ws, message): user = contentapi.get_user_or_default(objects["user"], message["createUserId"]) ws_print(ws, MSGPREFIX + Fore.CYAN + Style.BRIGHT + user["username"] + " " + Style.DIM + "#%d" % user["id"] + Fore.MAGENTA + " " + message["createDate"] + " [%d]" % message["id"]) - for t in textwrap.wrap(message["text"], width = MAXMESSAGEWIDTH): + for t in textwrap.wrap(message["text"], width = ws.main_config["fixed_width"]): ws_print(ws, MSGPREFIX + t) # Track ignored data @@ -223,10 +225,14 @@ def ws_onmessage(ws, message): # Printing websocket event output is tricky because we don't want to interrupt user input (we don't have # curses). As such, we must buffer our output IF we are asked to pause def ws_print(ws, output): - if ws.pause_output: - ws.output_buffer.append(output) + # We can't block output for too long, so we need to only try to acquire the lock for a very short time + if ws.output_lock.acquire(timeout=ws.main_config["output_buffer_timeout"]): + try: + printr(output) + finally: + ws.output_lock.release() else: - printr(output) + ws.output_buffer.append(output) # Loads the config from file into the global config var. If the file