diff --git a/main.py b/main.py index 0b73c99..36bdb34 100644 --- a/main.py +++ b/main.py @@ -49,22 +49,32 @@ unknown_json_logger.setLevel(logging.DEBUG) quit_confirm_time = None quit_confirm_lock = threading.Lock() +# Configurable timeouts (set from config in __main__) +_QUIT_CONFIRM_TIMEOUT = QUIT_CONFIRM_TIMEOUT +_RESPAWN_DELAY = RESPAWN_DELAY + +# Global shutdown flag (set by signal_handler, checked by main_loop) +shutdown_requested = False + def signal_handler(sig, frame): """Handle Ctrl+C with confirmation""" - global quit_confirm_time + global quit_confirm_time, shutdown_requested current_time = time.time() with quit_confirm_lock: - if quit_confirm_time is None or (current_time - quit_confirm_time) > QUIT_CONFIRM_TIMEOUT: + if shutdown_requested: + # Third Ctrl-C: last-resort forced exit + curses.endwin() + os._exit(0) + elif quit_confirm_time is None or (current_time - quit_confirm_time) > _QUIT_CONFIRM_TIMEOUT: # First Ctrl-C or timeout expired - logger.warning(f"^1^8Press Ctrl-C again within {QUIT_CONFIRM_TIMEOUT:.0f} seconds to quit^0") + logger.warning(f"^1^8Press Ctrl-C again within {_QUIT_CONFIRM_TIMEOUT:.0f} seconds to quit^0") quit_confirm_time = current_time else: - # Second Ctrl-C within timeout + # Second Ctrl-C within timeout: request graceful shutdown logger.warning("^1^8Quittin'^0") - curses.endwin() # Restore terminal before force exit - os._exit(0) + shutdown_requested = True def parse_cvar_response(message, game_state, ui): """ @@ -91,7 +101,7 @@ def parse_cvar_response(message, game_state, ui): return False -def handle_stats_connection(message, rcon, ui, game_state): +def handle_stats_connection(message): """ Handle stats connection info extraction Returns (stats_port, stats_password) or (None, None) @@ -163,7 +173,7 @@ def handle_player_respawns(game_state, ui): if game_state.server_info.gametype == 'Clan Arena': # CA: revive all players after round end if game_state.server_info.round_end_time: - if time.time() - game_state.server_info.round_end_time >= RESPAWN_DELAY: + if time.time() - game_state.server_info.round_end_time >= _RESPAWN_DELAY: game_state.server_info.dead_players.clear() game_state.server_info.round_end_time = None ui.update_server_info(game_state) @@ -172,7 +182,7 @@ def handle_player_respawns(game_state, ui): current_time = time.time() players_to_revive = [ name for name, death_time in game_state.server_info.dead_players.items() - if current_time - death_time >= RESPAWN_DELAY + if current_time - death_time >= _RESPAWN_DELAY ] if players_to_revive: for name in players_to_revive: @@ -307,7 +317,7 @@ def main_loop(screen, args): # Set logging level if args.verbose == 0: - logger.setLevel(logging.WARNING) + logger.setLevel(args.config_log_level if args.config_log_level is not None else logging.WARNING) elif args.verbose == 1: logger.setLevel(logging.INFO) else: @@ -321,15 +331,22 @@ def main_loop(screen, args): unknown_json_logger.propagate = False # Initialize components - ui = UIManager(screen, args.host) + ui = UIManager(screen, args.host, args.max_history) game_state = GameState() # Setup logging to output window log_handler = ui.setup_logging() logger.addHandler(log_handler) + # Attach UI log handler to lib module loggers + for lib_name in ('network', 'parser', 'state'): + lib_logger = logging.getLogger(lib_name) + if log_handler not in lib_logger.handlers: + lib_logger.addHandler(log_handler) + lib_logger.setLevel(logger.level) + # Setup input queue - input_queue = ui.setup_input_queue() + input_queue = ui.setup_input_queue(player_names_provider=lambda: game_state.player_tracker.get_player_names()) # Display startup messages ui.print_message(f"*** QL pyCon Version {VERSION} starting ***\n") @@ -345,9 +362,6 @@ def main_loop(screen, args): stats_password = None stats_check_counter = 0 - # Shutdown flag - shutdown = False - # Timer refresh tracking (update UI once per second) last_ui_update = 0 @@ -366,7 +380,7 @@ def main_loop(screen, args): # Main event loop with resource cleanup try: - while not shutdown: + while not shutdown_requested: # Poll RCON socket event = rcon.poll(POLL_TIMEOUT) @@ -380,6 +394,14 @@ def main_loop(screen, args): ui.print_message("Requesting connection info...\n") rcon.send_command(b'zmq_stats_password') rcon.send_command(b'net_port') + elif monitor_event and monitor_event[0] == zmq.EVENT_DISCONNECTED: + timestamp = time.strftime('%H:%M:%S') + ui.print_message(f"^3[^7{timestamp}^3] ^8^1Disconnected from server - waiting for reconnect...^0^7\n") + if stats_conn is not None: + stats_conn.close() + stats_conn = None + stats_port = None + stats_password = None # Handle user input handle_user_input(input_queue, rcon, ui) @@ -414,7 +436,7 @@ def main_loop(screen, args): if parse_player_events(message, game_state, ui): continue - if '------- Game Initialization -------' in message or 'Game Initialization' in message: + if 'Game Initialization' in message: logger.info('Game initialization detected - refreshing server info') timestamp = time.strftime('%H:%M:%S') @@ -432,6 +454,8 @@ def main_loop(screen, args): # Clear player dict since map changed game_state.server_info.players = {} game_state.player_tracker.player_teams = {} + game_state.server_info.reset_round_scores() + game_state.server_info.dead_players.clear() ui.update_server_info(game_state) # Try to parse as cvar response @@ -440,7 +464,7 @@ def main_loop(screen, args): continue # Check for stats connection info - port, password = handle_stats_connection(message, rcon, ui, game_state) + port, password = handle_stats_connection(message) if port: stats_port = port if password: @@ -531,6 +555,10 @@ if __name__ == '__main__': config = ConfigLoader() config.load() + # Read settings from config ([logging]/[ui]/[behavior]) + _QUIT_CONFIRM_TIMEOUT = config.get_quit_timeout(QUIT_CONFIRM_TIMEOUT) + _RESPAWN_DELAY = config.get_respawn_delay(RESPAWN_DELAY) + # Parse arguments parser = argparse.ArgumentParser(description='Quake Live Python Console') parser.add_argument('server', nargs='?', default=None, @@ -551,6 +579,10 @@ if __name__ == '__main__': help='File to log all JSON events') args = parser.parse_args() + # Apply config settings to args + args.config_log_level = config.get_log_level() + args.max_history = config.get_max_history(MAX_COMMAND_HISTORY) + # Handle --list if args.list: servers = config.get_servers()