diff --git a/lib/namelist.py b/lib/namelist.py index 890b7b7..11adf12 100644 --- a/lib/namelist.py +++ b/lib/namelist.py @@ -22,6 +22,11 @@ logger = logging.getLogger('namelist') # from the request: a large cvarlist (5000+ messages) takes seconds to process. 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. # Example: 'S A sv_hostname "My Server"' -> sv_hostname 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*$') # Count line that ends a listing. -# Examples: '1234 total cvars', '321 commands' -END_LINE = re.compile(r'^\s*\d+\s+(total cvars|commands)\s*$') +# Examples: '1234 total cvars', '321 commands', '321 total commands' +END_LINE = re.compile(r'^\s*\d+\s+(total cvars|total commands|commands)\s*$') def unwrap(message): @@ -55,12 +60,14 @@ class NameCapture: self.names = set() self.listings_pending = 0 # count lines still expected; 0 = not capturing self.deadline = 0 + self.deadline_max = 0 self.partial_line = '' def request(self, rcon): """Send cvarlist and cmdlist; their output is captured until both count lines arrived""" self.listings_pending = 2 self.deadline = time.time() + CAPTURE_TIMEOUT + self.deadline_max = time.time() + CAPTURE_MAX_DURATION self.partial_line = '' rcon.send_command(b'cvarlist') rcon.send_command(b'cmdlist') @@ -73,12 +80,18 @@ class NameCapture: """ if self.listings_pending == 0: 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, ' f'stopping capture with {len(self.names)} names') self.listings_pending = 0 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 cvarlist row as its own 1-byte message), so keep the unfinished tail