bluetooth: Add support of collecting interleave scan log

This adds supports in bluetooth_device_xmlrpc_server of collecting logs
related to interleave scan.

BUG=b:163702298
TEST=None

Change-Id: Ib324911066a8dde0e02596d710410ff7cab21bbe
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/2490080
Tested-by: Yun-Hao Chung <howardchung@google.com>
Commit-Queue: Yun-Hao Chung <howardchung@google.com>
Reviewed-by: Manish Mandlik <mmandlik@chromium.org>
diff --git a/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py b/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py
index c0f1ab5..058954e 100755
--- a/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py
+++ b/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py
@@ -23,6 +23,7 @@
 import subprocess
 import functools
 import time
+import re
 
 import common
 from autotest_lib.client.bin import utils
@@ -108,7 +109,7 @@
     return decorator
 
 
-class LogRecorder:
+class LogRecorder(object):
     """The LogRecorder class helps to collect logs without a listening thread"""
 
     class LoggingException(Exception):
@@ -181,6 +182,78 @@
         return False
 
 
+class InterleaveLogger(LogRecorder):
+    """LogRecorder class that focus on interleave scan"""
+
+    SYSLOG_PATH = '/var/log/messages'
+    KERNEL_LOG_PATTERN = ('[^ ]+ DEBUG kernel: \[(.*)\] Bluetooth: '
+                          '{FUNCTION}\(\) hci0: {LOG_STR}')
+    STATE_PATTERN = KERNEL_LOG_PATTERN.format(
+            FUNCTION='add_le_interleave_adv_monitor_scan',
+            LOG_STR='next state: (.+)')
+    CANCEL_PATTERN = KERNEL_LOG_PATTERN.format(
+            FUNCTION='cancel_interleave_scan',
+            LOG_STR='hci0 cancelling interleave scan')
+
+    def __init__(self):
+        """ Initialize object
+        """
+        self.reset()
+        self.state_pattern = re.compile(self.STATE_PATTERN)
+        self.cancel_pattern = re.compile(self.CANCEL_PATTERN)
+        super(InterleaveLogger, self).__init__(self.SYSLOG_PATH)
+
+    def reset(self):
+        """ Clear data between each log collection attempt
+        """
+        self.records = []
+        self.cancel_events = []
+
+    def StartRecording(self):
+        """ Reset the previous data and start recording.
+        """
+        self.reset()
+        super(InterleaveLogger, self).StartRecording()
+
+    def StopRecording(self):
+        """ Stop recording and parse logs
+            The following data will be set after this call
+
+            - self.records: a dictionary where each item is a record of
+                            interleave |state| and the |time| the state starts.
+                            |state| could be {'no filter', 'allowlist'}
+                            |time| is kernel time in sec
+
+            - self.cancel_events: a list of |time| when a interleave cancel
+                                  event log was found
+                                  |time| is kernel time in sec
+
+            @returns: True if StopRecording success, False otherwise
+
+        """
+        try:
+            super(InterleaveLogger, self).StopRecording()
+        except Exception as e:
+            logging.error(e)
+            return False
+
+        last_ktime = None
+        for line in self.log_contents:
+            line = line.strip().replace('\\r\\n', '')
+            state_pattern = self.state_pattern.search(line)
+            cancel_pattern = self.cancel_pattern.search(line)
+
+            if cancel_pattern:
+                ktime = float(cancel_pattern.groups()[0])
+                self.cancel_events.append(ktime)
+
+            if state_pattern:
+                ktime, state = state_pattern.groups()
+                ktime = float(ktime)
+                self.records.append({'time': ktime, 'state': state})
+        return True
+
+
 class PairingAgent(dbus.service.Object):
     """The agent handling the authentication process of bluetooth pairing.
 
@@ -329,6 +402,7 @@
         self._cras_test_client = cras_utils.CrasTestClient()
 
         self.advertisements = []
+        self.advmon_interleave_logger = InterleaveLogger()
         self._chrc_property = None
         self._timeout_id = 0
         self._signal_watch = None
@@ -2260,6 +2334,41 @@
         return self.advmon_appmgr.reset_event_count(app_id, monitor_id, event)
 
 
+    def advmon_interleave_scan_logger_start(self):
+        """ Start interleave logger recording
+        """
+        self.advmon_interleave_logger.StartRecording()
+
+    def advmon_interleave_scan_logger_stop(self):
+        """ Stop interleave logger recording
+
+        @returns: True if logs were successfully collected,
+                  False otherwise.
+
+        """
+        return self.advmon_interleave_logger.StopRecording()
+
+    def advmon_interleave_scan_logger_get_records(self):
+        """ Get records in previous log collections
+
+        @returns: a list of records, where each item is a record of
+                  interleave |state| and the |time| the state starts.
+                  |state| could be {'no filter', 'allowlist'}
+                  |time| is kernel time in sec
+
+        """
+        return self.advmon_interleave_logger.records
+
+    def advmon_interleave_scan_logger_get_cancel_events(self):
+        """ Get cancel events in previous log collections
+
+        @returns: a list of cancel |time| when a interleave cancel event log
+                  was found.
+                  |time| is kernel time in sec
+
+        """
+        return self.advmon_interleave_logger.cancel_events
+
     def register_advertisement(self, advertisement_data):
         """Register an advertisement.
 
diff --git a/server/cros/bluetooth/bluetooth_device.py b/server/cros/bluetooth/bluetooth_device.py
index 6d225a0..2f1deb6 100644
--- a/server/cros/bluetooth/bluetooth_device.py
+++ b/server/cros/bluetooth/bluetooth_device.py
@@ -984,6 +984,45 @@
         return self._proxy.advmon_reset_event_count(app_id, monitor_id, event)
 
     @proxy_thread_safe
+    def advmon_interleave_scan_logger_start(self):
+        """ Start interleave logger recording
+        """
+        self._proxy.advmon_interleave_scan_logger_start()
+
+    @proxy_thread_safe
+    def advmon_interleave_scan_logger_stop(self):
+        """ Stop interleave logger recording
+
+        @returns: True if logs were successfully collected,
+                  False otherwise.
+
+        """
+        return self._proxy.advmon_interleave_scan_logger_stop()
+
+    @proxy_thread_safe
+    def advmon_interleave_scan_logger_get_records(self):
+        """ Get records in previous log collections
+
+        @returns: a list of records, where each item is a record of
+                  interleave |state| and the |time| the state starts.
+                  |state| could be {'no filter', 'allowlist'}
+                  |time| is kernel time in sec
+
+        """
+        return self._proxy.advmon_interleave_scan_logger_get_records()
+
+    @proxy_thread_safe
+    def advmon_interleave_scan_logger_get_cancel_events(self):
+        """ Get cancel events in previous log collections
+
+        @returns: a list of cancel |time| when a interleave cancel event log
+                  was found.
+                  |time| is kernel time in sec
+
+        """
+        return self._proxy.advmon_interleave_scan_logger_get_cancel_events()
+
+    @proxy_thread_safe
     def messages_start(self):
         """Start messages monitoring."""
         self._proxy.messages_start()