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_time = None
|
||||||
quit_confirm_lock = threading.Lock()
|
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):
|
def signal_handler(sig, frame):
|
||||||
"""Handle Ctrl+C with confirmation"""
|
"""Handle Ctrl+C with confirmation"""
|
||||||
global quit_confirm_time
|
global quit_confirm_time, shutdown_requested
|
||||||
|
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
with quit_confirm_lock:
|
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
|
# 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
|
quit_confirm_time = current_time
|
||||||
else:
|
else:
|
||||||
# Second Ctrl-C within timeout
|
# Second Ctrl-C within timeout: request graceful shutdown
|
||||||
logger.warning("^1^8Quittin'^0")
|
logger.warning("^1^8Quittin'^0")
|
||||||
curses.endwin() # Restore terminal before force exit
|
shutdown_requested = True
|
||||||
os._exit(0)
|
|
||||||
|
|
||||||
def parse_cvar_response(message, game_state, ui):
|
def parse_cvar_response(message, game_state, ui):
|
||||||
"""
|
"""
|
||||||
@@ -91,7 +101,7 @@ def parse_cvar_response(message, game_state, ui):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def handle_stats_connection(message, rcon, ui, game_state):
|
def handle_stats_connection(message):
|
||||||
"""
|
"""
|
||||||
Handle stats connection info extraction
|
Handle stats connection info extraction
|
||||||
Returns (stats_port, stats_password) or (None, None)
|
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':
|
if game_state.server_info.gametype == 'Clan Arena':
|
||||||
# CA: revive all players after round end
|
# CA: revive all players after round end
|
||||||
if game_state.server_info.round_end_time:
|
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.dead_players.clear()
|
||||||
game_state.server_info.round_end_time = None
|
game_state.server_info.round_end_time = None
|
||||||
ui.update_server_info(game_state)
|
ui.update_server_info(game_state)
|
||||||
@@ -172,7 +182,7 @@ def handle_player_respawns(game_state, ui):
|
|||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
players_to_revive = [
|
players_to_revive = [
|
||||||
name for name, death_time in game_state.server_info.dead_players.items()
|
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:
|
if players_to_revive:
|
||||||
for name in players_to_revive:
|
for name in players_to_revive:
|
||||||
@@ -307,7 +317,7 @@ def main_loop(screen, args):
|
|||||||
|
|
||||||
# Set logging level
|
# Set logging level
|
||||||
if args.verbose == 0:
|
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:
|
elif args.verbose == 1:
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
else:
|
else:
|
||||||
@@ -321,15 +331,22 @@ def main_loop(screen, args):
|
|||||||
unknown_json_logger.propagate = False
|
unknown_json_logger.propagate = False
|
||||||
|
|
||||||
# Initialize components
|
# Initialize components
|
||||||
ui = UIManager(screen, args.host)
|
ui = UIManager(screen, args.host, args.max_history)
|
||||||
game_state = GameState()
|
game_state = GameState()
|
||||||
|
|
||||||
# Setup logging to output window
|
# Setup logging to output window
|
||||||
log_handler = ui.setup_logging()
|
log_handler = ui.setup_logging()
|
||||||
logger.addHandler(log_handler)
|
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
|
# 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
|
# Display startup messages
|
||||||
ui.print_message(f"*** QL pyCon Version {VERSION} starting ***\n")
|
ui.print_message(f"*** QL pyCon Version {VERSION} starting ***\n")
|
||||||
@@ -345,9 +362,6 @@ def main_loop(screen, args):
|
|||||||
stats_password = None
|
stats_password = None
|
||||||
stats_check_counter = 0
|
stats_check_counter = 0
|
||||||
|
|
||||||
# Shutdown flag
|
|
||||||
shutdown = False
|
|
||||||
|
|
||||||
# Timer refresh tracking (update UI once per second)
|
# Timer refresh tracking (update UI once per second)
|
||||||
last_ui_update = 0
|
last_ui_update = 0
|
||||||
|
|
||||||
@@ -366,7 +380,7 @@ def main_loop(screen, args):
|
|||||||
|
|
||||||
# Main event loop with resource cleanup
|
# Main event loop with resource cleanup
|
||||||
try:
|
try:
|
||||||
while not shutdown:
|
while not shutdown_requested:
|
||||||
# Poll RCON socket
|
# Poll RCON socket
|
||||||
event = rcon.poll(POLL_TIMEOUT)
|
event = rcon.poll(POLL_TIMEOUT)
|
||||||
|
|
||||||
@@ -380,6 +394,14 @@ def main_loop(screen, args):
|
|||||||
ui.print_message("Requesting connection info...\n")
|
ui.print_message("Requesting connection info...\n")
|
||||||
rcon.send_command(b'zmq_stats_password')
|
rcon.send_command(b'zmq_stats_password')
|
||||||
rcon.send_command(b'net_port')
|
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
|
||||||
handle_user_input(input_queue, rcon, ui)
|
handle_user_input(input_queue, rcon, ui)
|
||||||
@@ -414,7 +436,7 @@ def main_loop(screen, args):
|
|||||||
if parse_player_events(message, game_state, ui):
|
if parse_player_events(message, game_state, ui):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if '------- Game Initialization -------' in message or 'Game Initialization' in message:
|
if 'Game Initialization' in message:
|
||||||
logger.info('Game initialization detected - refreshing server info')
|
logger.info('Game initialization detected - refreshing server info')
|
||||||
|
|
||||||
timestamp = time.strftime('%H:%M:%S')
|
timestamp = time.strftime('%H:%M:%S')
|
||||||
@@ -432,6 +454,8 @@ def main_loop(screen, args):
|
|||||||
# Clear player dict since map changed
|
# Clear player dict since map changed
|
||||||
game_state.server_info.players = {}
|
game_state.server_info.players = {}
|
||||||
game_state.player_tracker.player_teams = {}
|
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)
|
ui.update_server_info(game_state)
|
||||||
|
|
||||||
# Try to parse as cvar response
|
# Try to parse as cvar response
|
||||||
@@ -440,7 +464,7 @@ def main_loop(screen, args):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Check for stats connection info
|
# Check for stats connection info
|
||||||
port, password = handle_stats_connection(message, rcon, ui, game_state)
|
port, password = handle_stats_connection(message)
|
||||||
if port:
|
if port:
|
||||||
stats_port = port
|
stats_port = port
|
||||||
if password:
|
if password:
|
||||||
@@ -531,6 +555,10 @@ if __name__ == '__main__':
|
|||||||
config = ConfigLoader()
|
config = ConfigLoader()
|
||||||
config.load()
|
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
|
# Parse arguments
|
||||||
parser = argparse.ArgumentParser(description='Quake Live Python Console')
|
parser = argparse.ArgumentParser(description='Quake Live Python Console')
|
||||||
parser.add_argument('server', nargs='?', default=None,
|
parser.add_argument('server', nargs='?', default=None,
|
||||||
@@ -551,6 +579,10 @@ if __name__ == '__main__':
|
|||||||
help='File to log all JSON events')
|
help='File to log all JSON events')
|
||||||
args = parser.parse_args()
|
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
|
# Handle --list
|
||||||
if args.list:
|
if args.list:
|
||||||
servers = config.get_servers()
|
servers = config.get_servers()
|
||||||
|
|||||||
Reference in New Issue
Block a user