• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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