Add graceful shutdown, reconnect handling and config file integration
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]>
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user