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 8import fcntl 9import logging 10import os 11import pty 12import re 13import subprocess 14import threading 15import time 16 17 18class OutputRecorderError(Exception): 19 """An exception class for output_recorder module.""" 20 pass 21 22 23class 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 195if __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