Home | History | Annotate | Download | only in bluetooth
      1 # Copyright 2016 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """This module provides an object to record the output of command-line program.
      6 """
      7 
      8 import fcntl
      9 import logging
     10 import os
     11 import pty
     12 import re
     13 import subprocess
     14 import threading
     15 import time
     16 
     17 
     18 class OutputRecorderError(Exception):
     19     """An exception class for output_recorder module."""
     20     pass
     21 
     22 
     23 class OutputRecorder(object):
     24     """A class used to record the output of command line program.
     25 
     26     A thread is dedicated to performing non-blocking reading of the
     27     command outpt in this class. Other possible approaches include
     28     1. using gobject.io_add_watch() to register a callback and
     29        reading the output when available, or
     30     2. using select.select() with a short timeout, and reading
     31        the output if available.
     32     However, the above two approaches are not very reliable. Hence,
     33     this approach using non-blocking reading is adopted.
     34 
     35     To prevent the block buffering of the command output, a pseudo
     36     terminal is created through pty.openpty(). This forces the
     37     line output.
     38 
     39     This class saves the output in self.contents so that it is
     40     easy to perform regular expression search(). The output is
     41     also saved in a file.
     42 
     43     """
     44 
     45     DEFAULT_OPEN_MODE = 'a'
     46     START_DELAY_SECS = 1        # Delay after starting recording.
     47     STOP_DELAY_SECS = 1         # Delay before stopping recording.
     48     POLLING_DELAY_SECS = 0.1    # Delay before next polling.
     49     TMP_FILE = '/tmp/output_recorder.dat'
     50 
     51     def __init__(self, cmd, open_mode=DEFAULT_OPEN_MODE,
     52                  start_delay_secs=START_DELAY_SECS,
     53                  stop_delay_secs=STOP_DELAY_SECS, save_file=TMP_FILE):
     54         """Construction of output recorder.
     55 
     56         @param cmd: the command of which the output is to record.
     57         @param open_mode: the open mode for writing output to save_file.
     58                 Could be either 'w' or 'a'.
     59         @param stop_delay_secs: the delay time before stopping the cmd.
     60         @param save_file: the file to save the output.
     61 
     62         """
     63         self.cmd = cmd
     64         self.open_mode = open_mode
     65         self.start_delay_secs = start_delay_secs
     66         self.stop_delay_secs = stop_delay_secs
     67         self.save_file = save_file
     68         self.contents = []
     69 
     70         # Create a thread dedicated to record the output.
     71         self._recording_thread = None
     72         self._stop_recording_thread_event = threading.Event()
     73 
     74         # Use pseudo terminal to prevent buffering of the program output.
     75         self._master, self._slave = pty.openpty()
     76         self._output = os.fdopen(self._master)
     77 
     78         # Set non-blocking flag.
     79         fcntl.fcntl(self._output, fcntl.F_SETFL, os.O_NONBLOCK)
     80 
     81 
     82     def record(self):
     83         """Record the output of the cmd."""
     84         logging.info('Recording output of "%s".', self.cmd)
     85         try:
     86             self._recorder = subprocess.Popen(
     87                     self.cmd, stdout=self._slave, stderr=self._slave)
     88         except:
     89             raise OutputRecorderError('Failed to run "%s"' % self.cmd)
     90 
     91         with open(self.save_file, self.open_mode) as output_f:
     92             output_f.write(os.linesep + '*' * 80 + os.linesep)
     93             while True:
     94                 try:
     95                     # Perform non-blocking read.
     96                     line = self._output.readline()
     97                 except:
     98                     # Set empty string if nothing to read.
     99                     line = ''
    100 
    101                 if line:
    102                     output_f.write(line)
    103                     output_f.flush()
    104                     # The output, e.g. the output of btmon, may contain some
    105                     # special unicode such that we would like to escape.
    106                     # In this way, regular expression search could be conducted
    107                     # properly.
    108                     self.contents.append(line.encode('unicode-escape'))
    109                 elif self._stop_recording_thread_event.is_set():
    110                     self._stop_recording_thread_event.clear()
    111                     break
    112                 else:
    113                     # Sleep a while if nothing to read yet.
    114                     time.sleep(self.POLLING_DELAY_SECS)
    115 
    116 
    117     def start(self):
    118         """Start the recording thread."""
    119         logging.info('Start recording thread.')
    120         self.clear_contents()
    121         self._recording_thread = threading.Thread(target=self.record)
    122         self._recording_thread.start()
    123         time.sleep(self.start_delay_secs)
    124 
    125 
    126     def stop(self):
    127         """Stop the recording thread."""
    128         logging.info('Stop recording thread.')
    129         time.sleep(self.stop_delay_secs)
    130         self._stop_recording_thread_event.set()
    131         self._recording_thread.join()
    132 
    133         # Kill the process.
    134         self._recorder.terminate()
    135         self._recorder.kill()
    136 
    137 
    138     def clear_contents(self):
    139         """Clear the contents."""
    140         self.contents = []
    141 
    142 
    143     def get_contents(self, search_str='', start_str=''):
    144         """Get the (filtered) contents.
    145 
    146         @param search_str: only lines with search_str would be kept.
    147         @param start_str: all lines before the occurrence of start_str would be
    148                           filtered.
    149 
    150         @returns: the (filtered) contents.
    151 
    152         """
    153         search_pattern = re.compile(search_str) if search_str else None
    154         start_pattern = re.compile(start_str) if start_str else None
    155 
    156         # Just returns the original contents if no filtered conditions are
    157         # specified.
    158         if not search_pattern and not start_pattern:
    159             return self.contents
    160 
    161         contents = []
    162         start_flag = not bool(start_pattern)
    163         for line in self.contents:
    164             if start_flag:
    165                 if search_pattern.search(line):
    166                     contents.append(line.strip())
    167             elif start_pattern.search(line):
    168                 start_flag = True
    169                 contents.append(line.strip())
    170 
    171         return contents
    172 
    173 
    174     def find(self, pattern_str, flags=re.I):
    175         """Find a pattern string in the contents.
    176 
    177         Note that the pattern_str is considered as an arbitrary literal string
    178         that might contain re meta-characters, e.g., '(' or ')'. Hence,
    179         re.escape() is applied before using re.compile.
    180 
    181         @param pattern_str: the pattern string to search.
    182         @param flags: the flags of the pattern expression behavior.
    183 
    184         @returns: True if found. False otherwise.
    185 
    186         """
    187         pattern = re.compile(re.escape(pattern_str), flags)
    188         for line in self.contents:
    189             result = pattern.search(line)
    190             if result:
    191                 return True
    192         return False
    193 
    194 
    195 if __name__ == '__main__':
    196     # A demo using btmon tool to monitor bluetoohd activity.
    197     cmd = 'btmon'
    198     recorder = OutputRecorder(cmd)
    199 
    200     if True:
    201         recorder.start()
    202         # Perform some bluetooth activities here in another terminal.
    203         time.sleep(recorder.stop_delay_secs)
    204         recorder.stop()
    205 
    206     for line in recorder.get_contents():
    207         print line
    208