1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright 2021 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li"""Helper class to manage bluetooth logs""" 6*9c5db199SXin Li 7*9c5db199SXin Lifrom datetime import datetime 8*9c5db199SXin Liimport logging 9*9c5db199SXin Liimport os 10*9c5db199SXin Liimport re 11*9c5db199SXin Liimport subprocess 12*9c5db199SXin Liimport time 13*9c5db199SXin Li 14*9c5db199SXin Li 15*9c5db199SXin LiSYSLOG_PATH = '/var/log/messages' 16*9c5db199SXin Li 17*9c5db199SXin Li 18*9c5db199SXin Liclass LogManager(object): 19*9c5db199SXin Li """The LogManager class helps to collect logs without a listening thread""" 20*9c5db199SXin Li 21*9c5db199SXin Li DEFAULT_ENCODING = 'utf-8' 22*9c5db199SXin Li 23*9c5db199SXin Li class LoggingException(Exception): 24*9c5db199SXin Li """A stub exception class for LogManager class.""" 25*9c5db199SXin Li pass 26*9c5db199SXin Li 27*9c5db199SXin Li def __init__(self, log_path=SYSLOG_PATH, raise_missing=False): 28*9c5db199SXin Li """Initialize log manager object 29*9c5db199SXin Li 30*9c5db199SXin Li @param log_path: string path to log file to manage 31*9c5db199SXin Li @param raise_missing: raise an exception if the log file is missing 32*9c5db199SXin Li 33*9c5db199SXin Li @raises: LogManager.LoggingException on non-existent log file 34*9c5db199SXin Li """ 35*9c5db199SXin Li if not os.path.isfile(log_path): 36*9c5db199SXin Li msg = 'Requested log file {} does not exist'.format(log_path) 37*9c5db199SXin Li if raise_missing: 38*9c5db199SXin Li raise LogManager.LoggingException(msg) 39*9c5db199SXin Li else: 40*9c5db199SXin Li self._LogErrorToSyslog(msg) 41*9c5db199SXin Li 42*9c5db199SXin Li self.log_path = log_path 43*9c5db199SXin Li 44*9c5db199SXin Li self.ResetLogMarker() 45*9c5db199SXin Li self._bin_log_contents = [] 46*9c5db199SXin Li 47*9c5db199SXin Li def _LogErrorToSyslog(self, message): 48*9c5db199SXin Li """Create a new syslog file and add a message to syslog.""" 49*9c5db199SXin Li subprocess.call(['reload', 'syslog']) 50*9c5db199SXin Li subprocess.call(['logger', message]) 51*9c5db199SXin Li 52*9c5db199SXin Li def _GetSize(self): 53*9c5db199SXin Li """Get the size of the log""" 54*9c5db199SXin Li try: 55*9c5db199SXin Li return os.path.getsize(self.log_path) 56*9c5db199SXin Li except Exception as e: 57*9c5db199SXin Li logging.error('Failed to get log size: {}'.format(e)) 58*9c5db199SXin Li return 0 59*9c5db199SXin Li 60*9c5db199SXin Li def ResetLogMarker(self, now_size=None): 61*9c5db199SXin Li """Reset the start-of-log marker for later comparison""" 62*9c5db199SXin Li if now_size is None: 63*9c5db199SXin Li now_size = self._GetSize() 64*9c5db199SXin Li self.initial_log_size = now_size 65*9c5db199SXin Li 66*9c5db199SXin Li def StartRecording(self): 67*9c5db199SXin Li """Mark initial log size for later comparison""" 68*9c5db199SXin Li 69*9c5db199SXin Li self._bin_log_contents = [] 70*9c5db199SXin Li 71*9c5db199SXin Li def StopRecording(self): 72*9c5db199SXin Li """Gather the logs since StartRecording was called 73*9c5db199SXin Li 74*9c5db199SXin Li @raises: LogManager.LoggingException if: 75*9c5db199SXin Li - Log file disappeared since StartRecording was called 76*9c5db199SXin Li - Log file is smaller than when logging began 77*9c5db199SXin Li - StartRecording was never called 78*9c5db199SXin Li """ 79*9c5db199SXin Li initial_size = self.initial_log_size 80*9c5db199SXin Li now_size = self._GetSize() 81*9c5db199SXin Li 82*9c5db199SXin Li if not os.path.isfile(self.log_path): 83*9c5db199SXin Li msg = 'File {} disappeared unexpectedly'.format(self.log_path) 84*9c5db199SXin Li raise LogManager.LoggingException(msg) 85*9c5db199SXin Li 86*9c5db199SXin Li if now_size < initial_size: 87*9c5db199SXin Li msg = 'Log became smaller unexpectedly' 88*9c5db199SXin Li raise LogManager.LoggingException(msg) 89*9c5db199SXin Li 90*9c5db199SXin Li with open(self.log_path, 'rb') as mf: 91*9c5db199SXin Li # Skip to the point where we started recording 92*9c5db199SXin Li mf.seek(self.initial_log_size) 93*9c5db199SXin Li 94*9c5db199SXin Li readsize = now_size - self.initial_log_size 95*9c5db199SXin Li self._bin_log_contents = mf.read(readsize).split(b'\n') 96*9c5db199SXin Li 97*9c5db199SXin Li # Re-set start of log marker 98*9c5db199SXin Li self.ResetLogMarker(now_size) 99*9c5db199SXin Li 100*9c5db199SXin Li def LogContains(self, search_str): 101*9c5db199SXin Li """Performs simple string checking on each line from the collected log 102*9c5db199SXin Li 103*9c5db199SXin Li @param search_str: string to be located within log contents. This arg 104*9c5db199SXin Li is expected to not span between lines in the logs 105*9c5db199SXin Li 106*9c5db199SXin Li @returns: True if search_str was located in the collected log contents, 107*9c5db199SXin Li False otherwise 108*9c5db199SXin Li """ 109*9c5db199SXin Li pattern = re.compile(search_str.encode(self.DEFAULT_ENCODING)) 110*9c5db199SXin Li for line in self._bin_log_contents: 111*9c5db199SXin Li if pattern.search(line): 112*9c5db199SXin Li return True 113*9c5db199SXin Li 114*9c5db199SXin Li return False 115*9c5db199SXin Li 116*9c5db199SXin Li def FilterOut(self, rm_reg_exp): 117*9c5db199SXin Li """Remove lines with specified pattern from the log file 118*9c5db199SXin Li 119*9c5db199SXin Li @param rm_reg_exp: regular expression of the lines to be removed 120*9c5db199SXin Li """ 121*9c5db199SXin Li # If log_path doesn't exist, there's nothing to do 122*9c5db199SXin Li if not os.path.isfile(self.log_path): 123*9c5db199SXin Li return 124*9c5db199SXin Li 125*9c5db199SXin Li rm_line_cnt = 0 126*9c5db199SXin Li initial_size = self._GetSize() 127*9c5db199SXin Li rm_pattern = re.compile(rm_reg_exp.encode(self.DEFAULT_ENCODING)) 128*9c5db199SXin Li 129*9c5db199SXin Li with open(self.log_path, 'rb+') as mf: 130*9c5db199SXin Li lines = mf.readlines() 131*9c5db199SXin Li mf.seek(0) 132*9c5db199SXin Li for line in lines: 133*9c5db199SXin Li if rm_pattern.search(line): 134*9c5db199SXin Li rm_line_cnt += 1 135*9c5db199SXin Li else: 136*9c5db199SXin Li mf.write(line) 137*9c5db199SXin Li mf.truncate() 138*9c5db199SXin Li 139*9c5db199SXin Li # Some tracebacks point out here causing /var/log/messages missing but 140*9c5db199SXin Li # we don't have many clues. Adding a check and logs here. 141*9c5db199SXin Li if not os.path.isfile(self.log_path): 142*9c5db199SXin Li msg = '{} does not exist after FilterOut'.format(self.log_path) 143*9c5db199SXin Li logging.warning(msg) 144*9c5db199SXin Li self._LogErrorToSyslog(msg) 145*9c5db199SXin Li 146*9c5db199SXin Li new_size = self._GetSize() 147*9c5db199SXin Li rm_byte = initial_size - new_size 148*9c5db199SXin Li logging.info('Removed number of line: %d, Reduced log size: %d byte', 149*9c5db199SXin Li rm_line_cnt, rm_byte) 150*9c5db199SXin Li 151*9c5db199SXin Li # Note the new size of the log 152*9c5db199SXin Li self.ResetLogMarker(new_size) 153*9c5db199SXin Li 154*9c5db199SXin Li 155*9c5db199SXin Liclass InterleaveLogger(LogManager): 156*9c5db199SXin Li """LogManager class that focus on interleave scan""" 157*9c5db199SXin Li 158*9c5db199SXin Li # Example bluetooth kernel log: 159*9c5db199SXin Li # "2020-11-23T07:52:31.395941Z DEBUG kernel: [ 6469.811135] Bluetooth: " 160*9c5db199SXin Li # "cancel_interleave_scan() hci0: cancelling interleave scan" 161*9c5db199SXin Li KERNEL_LOG_PATTERN = ('([^ ]+) DEBUG kernel: \[.*\] Bluetooth: ' 162*9c5db199SXin Li '{FUNCTION}\(\) hci0: {LOG_STR}') 163*9c5db199SXin Li STATE_PATTERN = KERNEL_LOG_PATTERN.format( 164*9c5db199SXin Li FUNCTION='hci_req_add_le_interleaved_scan', 165*9c5db199SXin Li LOG_STR='next state: (.+)') 166*9c5db199SXin Li CANCEL_PATTERN = KERNEL_LOG_PATTERN.format( 167*9c5db199SXin Li FUNCTION='cancel_interleave_scan', 168*9c5db199SXin Li LOG_STR='cancelling interleave scan') 169*9c5db199SXin Li SYSTIME_LENGTH = len('2020-12-18T00:11:22.345678') 170*9c5db199SXin Li 171*9c5db199SXin Li def __init__(self): 172*9c5db199SXin Li """ Initialize object 173*9c5db199SXin Li """ 174*9c5db199SXin Li self.reset() 175*9c5db199SXin Li self.state_pattern = re.compile( 176*9c5db199SXin Li self.STATE_PATTERN.encode(self.DEFAULT_ENCODING)) 177*9c5db199SXin Li self.cancel_pattern = re.compile( 178*9c5db199SXin Li self.CANCEL_PATTERN.encode(self.DEFAULT_ENCODING)) 179*9c5db199SXin Li super(InterleaveLogger, self).__init__() 180*9c5db199SXin Li 181*9c5db199SXin Li def reset(self): 182*9c5db199SXin Li """ Clear data between each log collection attempt 183*9c5db199SXin Li """ 184*9c5db199SXin Li self.records = [] 185*9c5db199SXin Li self.cancel_events = [] 186*9c5db199SXin Li 187*9c5db199SXin Li def StartRecording(self): 188*9c5db199SXin Li """ Reset the previous data and start recording. 189*9c5db199SXin Li """ 190*9c5db199SXin Li self.reset() 191*9c5db199SXin Li super(InterleaveLogger, self).ResetLogMarker() 192*9c5db199SXin Li super(InterleaveLogger, self).StartRecording() 193*9c5db199SXin Li 194*9c5db199SXin Li def StopRecording(self): 195*9c5db199SXin Li """ Stop recording and parse logs 196*9c5db199SXin Li The following data will be set after this call 197*9c5db199SXin Li 198*9c5db199SXin Li - self.records: a dictionary where each item is a record of 199*9c5db199SXin Li interleave |state| and the |time| the state starts. 200*9c5db199SXin Li |state| could be {'no filter', 'allowlist'} 201*9c5db199SXin Li |time| is system time in sec 202*9c5db199SXin Li 203*9c5db199SXin Li - self.cancel_events: a list of |time| when a interleave cancel 204*9c5db199SXin Li event log was found 205*9c5db199SXin Li |time| is system time in sec 206*9c5db199SXin Li 207*9c5db199SXin Li @returns: True if StopRecording success, False otherwise 208*9c5db199SXin Li 209*9c5db199SXin Li """ 210*9c5db199SXin Li try: 211*9c5db199SXin Li super(InterleaveLogger, self).StopRecording() 212*9c5db199SXin Li except Exception as e: 213*9c5db199SXin Li logging.error(e) 214*9c5db199SXin Li return False 215*9c5db199SXin Li 216*9c5db199SXin Li success = True 217*9c5db199SXin Li 218*9c5db199SXin Li def sys_time_to_timestamp(time_str): 219*9c5db199SXin Li """ Return timestamp of time_str """ 220*9c5db199SXin Li 221*9c5db199SXin Li # This is to remove the suffix of time string, in some cases the 222*9c5db199SXin Li # time string ends with an extra 'Z', in other cases, the string 223*9c5db199SXin Li # ends with time zone (ex. '+08:00') 224*9c5db199SXin Li time_str = time_str[:self.SYSTIME_LENGTH] 225*9c5db199SXin Li 226*9c5db199SXin Li try: 227*9c5db199SXin Li dt = datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S.%f") 228*9c5db199SXin Li except Exception as e: 229*9c5db199SXin Li logging.error(e) 230*9c5db199SXin Li success = False 231*9c5db199SXin Li return 0 232*9c5db199SXin Li 233*9c5db199SXin Li return time.mktime(dt.timetuple()) + dt.microsecond * (10**-6) 234*9c5db199SXin Li 235*9c5db199SXin Li for line in self._bin_log_contents: 236*9c5db199SXin Li line = line.strip().replace(b'\\r\\n', b'') 237*9c5db199SXin Li state_pattern = self.state_pattern.search(line) 238*9c5db199SXin Li cancel_pattern = self.cancel_pattern.search(line) 239*9c5db199SXin Li 240*9c5db199SXin Li if cancel_pattern: 241*9c5db199SXin Li time_str = cancel_pattern.groups()[0].decode( 242*9c5db199SXin Li self.DEFAULT_ENCODING) 243*9c5db199SXin Li time_sec = sys_time_to_timestamp(time_str) 244*9c5db199SXin Li self.cancel_events.append(time_sec) 245*9c5db199SXin Li 246*9c5db199SXin Li if state_pattern: 247*9c5db199SXin Li time_str, state = [ 248*9c5db199SXin Li x.decode(self.DEFAULT_ENCODING) 249*9c5db199SXin Li for x in state_pattern.groups() 250*9c5db199SXin Li ] 251*9c5db199SXin Li time_sec = sys_time_to_timestamp(time_str) 252*9c5db199SXin Li self.records.append({'time': time_sec, 'state': state}) 253*9c5db199SXin Li 254*9c5db199SXin Li return success 255