Compare commits
3
Commits
3d1dc19d9b
...
b8864ca8dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8864ca8dd | ||
|
|
b641eb4e45 | ||
|
|
71bd740d78 |
+17
-4
@@ -22,6 +22,11 @@ logger = logging.getLogger('namelist')
|
|||||||
# from the request: a large cvarlist (5000+ messages) takes seconds to process.
|
# from the request: a large cvarlist (5000+ messages) takes seconds to process.
|
||||||
CAPTURE_TIMEOUT = 5.0
|
CAPTURE_TIMEOUT = 5.0
|
||||||
|
|
||||||
|
# Absolute cap on a capture, measured from the request. The timeout above is
|
||||||
|
# refreshed by every incoming message, so regular server output can keep a
|
||||||
|
# stuck capture (count line never matches) alive indefinitely without this bound.
|
||||||
|
CAPTURE_MAX_DURATION = 30.0
|
||||||
|
|
||||||
# Cvar line: the name is the word directly before the quoted value.
|
# Cvar line: the name is the word directly before the quoted value.
|
||||||
# Example: 'S A sv_hostname "My Server"' -> sv_hostname
|
# Example: 'S A sv_hostname "My Server"' -> sv_hostname
|
||||||
CVARLIST_LINE = re.compile(r'([A-Za-z_]\w*)\s+"')
|
CVARLIST_LINE = re.compile(r'([A-Za-z_]\w*)\s+"')
|
||||||
@@ -31,8 +36,8 @@ CVARLIST_LINE = re.compile(r'([A-Za-z_]\w*)\s+"')
|
|||||||
CMDLIST_LINE = re.compile(r'^\s*([A-Za-z_]\w*)\s*$')
|
CMDLIST_LINE = re.compile(r'^\s*([A-Za-z_]\w*)\s*$')
|
||||||
|
|
||||||
# Count line that ends a listing.
|
# Count line that ends a listing.
|
||||||
# Examples: '1234 total cvars', '321 commands'
|
# Examples: '1234 total cvars', '321 commands', '321 total commands'
|
||||||
END_LINE = re.compile(r'^\s*\d+\s+(total cvars|commands)\s*$')
|
END_LINE = re.compile(r'^\s*\d+\s+(total cvars|total commands|commands)\s*$')
|
||||||
|
|
||||||
|
|
||||||
def unwrap(message):
|
def unwrap(message):
|
||||||
@@ -55,12 +60,14 @@ class NameCapture:
|
|||||||
self.names = set()
|
self.names = set()
|
||||||
self.listings_pending = 0 # count lines still expected; 0 = not capturing
|
self.listings_pending = 0 # count lines still expected; 0 = not capturing
|
||||||
self.deadline = 0
|
self.deadline = 0
|
||||||
|
self.deadline_max = 0
|
||||||
self.partial_line = ''
|
self.partial_line = ''
|
||||||
|
|
||||||
def request(self, rcon):
|
def request(self, rcon):
|
||||||
"""Send cvarlist and cmdlist; their output is captured until both count lines arrived"""
|
"""Send cvarlist and cmdlist; their output is captured until both count lines arrived"""
|
||||||
self.listings_pending = 2
|
self.listings_pending = 2
|
||||||
self.deadline = time.time() + CAPTURE_TIMEOUT
|
self.deadline = time.time() + CAPTURE_TIMEOUT
|
||||||
|
self.deadline_max = time.time() + CAPTURE_MAX_DURATION
|
||||||
self.partial_line = ''
|
self.partial_line = ''
|
||||||
rcon.send_command(b'cvarlist')
|
rcon.send_command(b'cvarlist')
|
||||||
rcon.send_command(b'cmdlist')
|
rcon.send_command(b'cmdlist')
|
||||||
@@ -73,12 +80,18 @@ class NameCapture:
|
|||||||
"""
|
"""
|
||||||
if self.listings_pending == 0:
|
if self.listings_pending == 0:
|
||||||
return False
|
return False
|
||||||
if time.time() > self.deadline:
|
now = time.time()
|
||||||
|
if now > self.deadline_max:
|
||||||
|
logger.warning(f'Capture exceeded {CAPTURE_MAX_DURATION:.0f}s, '
|
||||||
|
f'stopping capture with {len(self.names)} names')
|
||||||
|
self.listings_pending = 0
|
||||||
|
return False
|
||||||
|
if now > self.deadline:
|
||||||
logger.warning(f'No listing output for {CAPTURE_TIMEOUT:.0f}s, '
|
logger.warning(f'No listing output for {CAPTURE_TIMEOUT:.0f}s, '
|
||||||
f'stopping capture with {len(self.names)} names')
|
f'stopping capture with {len(self.names)} names')
|
||||||
self.listings_pending = 0
|
self.listings_pending = 0
|
||||||
return False
|
return False
|
||||||
self.deadline = time.time() + CAPTURE_TIMEOUT
|
self.deadline = now + CAPTURE_TIMEOUT
|
||||||
|
|
||||||
# A line can arrive in several messages (the server sends each flag column of
|
# A line can arrive in several messages (the server sends each flag column of
|
||||||
# a cvarlist row as its own 1-byte message), so keep the unfinished tail
|
# a cvarlist row as its own 1-byte message), so keep the unfinished tail
|
||||||
|
|||||||
@@ -586,6 +586,9 @@ class UIManager:
|
|||||||
key_seen = True
|
key_seen = True
|
||||||
if key == curses.KEY_RESIZE:
|
if key == curses.KEY_RESIZE:
|
||||||
self.handle_resize()
|
self.handle_resize()
|
||||||
|
elif self.too_small:
|
||||||
|
# Warning screen: ignore typing and scrolling, only resizes act
|
||||||
|
continue
|
||||||
elif key == curses.KEY_PPAGE:
|
elif key == curses.KEY_PPAGE:
|
||||||
self.scroll_output(self._page_rows())
|
self.scroll_output(self._page_rows())
|
||||||
elif key == curses.KEY_NPAGE:
|
elif key == curses.KEY_NPAGE:
|
||||||
@@ -625,6 +628,7 @@ class UIManager:
|
|||||||
return
|
return
|
||||||
self._draw_output()
|
self._draw_output()
|
||||||
self._draw_divider()
|
self._draw_divider()
|
||||||
|
self._flush()
|
||||||
|
|
||||||
# --- output ---
|
# --- output ---
|
||||||
|
|
||||||
@@ -646,9 +650,18 @@ class UIManager:
|
|||||||
pieces.pop()
|
pieces.pop()
|
||||||
|
|
||||||
new_lines = []
|
new_lines = []
|
||||||
|
continued_delta = 0
|
||||||
for piece in pieces:
|
for piece in pieces:
|
||||||
if self.line_open:
|
if self.line_open:
|
||||||
self.output_lines[-1] += piece
|
if not self.too_small and self.scroll_offset > 0:
|
||||||
|
# The continued line can gain wrapped rows; count them so a
|
||||||
|
# scrolled-back view stays still
|
||||||
|
_, width = self.output_window.getmaxyx()
|
||||||
|
rows_before = len(wrap_colored(self.output_lines[-1], width))
|
||||||
|
self.output_lines[-1] += piece
|
||||||
|
continued_delta += len(wrap_colored(self.output_lines[-1], width)) - rows_before
|
||||||
|
else:
|
||||||
|
self.output_lines[-1] += piece
|
||||||
self.line_open = False
|
self.line_open = False
|
||||||
else:
|
else:
|
||||||
self.output_lines.append(piece)
|
self.output_lines.append(piece)
|
||||||
@@ -663,6 +676,7 @@ class UIManager:
|
|||||||
_, width = self.output_window.getmaxyx()
|
_, width = self.output_window.getmaxyx()
|
||||||
for line in new_lines:
|
for line in new_lines:
|
||||||
self.scroll_offset += len(wrap_colored(line, width))
|
self.scroll_offset += len(wrap_colored(line, width))
|
||||||
|
self.scroll_offset += continued_delta
|
||||||
|
|
||||||
self._draw_output()
|
self._draw_output()
|
||||||
self._draw_divider()
|
self._draw_divider()
|
||||||
|
|||||||
@@ -162,12 +162,10 @@ def handle_stats_stream(stats_conn, stats_check_counter, event_parser, ui, game_
|
|||||||
if stats_msg:
|
if stats_msg:
|
||||||
logger.info(f'Stats event received ({len(stats_msg)} bytes)')
|
logger.info(f'Stats event received ({len(stats_msg)} bytes)')
|
||||||
|
|
||||||
# Parse game event
|
# Parse game event (parse_event returns a display-ready string)
|
||||||
parsed = event_parser.parse_event(stats_msg)
|
parsed = event_parser.parse_event(stats_msg)
|
||||||
if parsed:
|
if parsed:
|
||||||
# Format with timestamp before displaying
|
ui.print_message(parsed)
|
||||||
formatted_msg, attributes = format_message(parsed)
|
|
||||||
ui.print_message(formatted_msg)
|
|
||||||
ui.update_server_info(game_state)
|
ui.update_server_info(game_state)
|
||||||
|
|
||||||
return stats_check_counter
|
return stats_check_counter
|
||||||
@@ -557,7 +555,7 @@ def main_loop(screen, args):
|
|||||||
message = format_chat_message(message, game_state.player_tracker)
|
message = format_chat_message(message, game_state.player_tracker)
|
||||||
|
|
||||||
# Format and display message
|
# Format and display message
|
||||||
formatted_msg, attributes = format_message(message)
|
formatted_msg, _ = format_message(message)
|
||||||
ui.print_message(formatted_msg)
|
ui.print_message(formatted_msg)
|
||||||
|
|
||||||
# Update server info panel every second (for live timer display)
|
# Update server info panel every second (for live timer display)
|
||||||
|
|||||||
Reference in New Issue
Block a user