1# Copyright 2015 The Chromium 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 cras DBus audio utilities.""" 6 7import logging 8import multiprocessing 9import pprint 10 11from autotest_lib.client.cros.audio import cras_utils 12 13 14def _set_default_main_loop(): 15 """Sets the gobject main loop to be the event loop for DBus. 16 17 @raises: ImportError if dbus.mainloop.glib can not be imported. 18 19 """ 20 try: 21 import dbus.mainloop.glib 22 except ImportError, e: 23 logging.exception( 24 'Can not import dbus.mainloop.glib: %s. ' 25 'This method should only be called on Cros device.', e) 26 raise 27 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 28 29 30def _get_gobject(): 31 """Tries to import gobject. 32 33 @returns: The imported gobject module. 34 35 @raises: ImportError if gobject can not be imported. 36 37 """ 38 try: 39 import gobject 40 except ImportError, e: 41 logging.exception( 42 'Can not import gobject: %s. This method should only be ' 43 'called on Cros device.', e) 44 raise 45 return gobject 46 47 48class CrasDBusMonitorError(Exception): 49 """Error in CrasDBusMonitor.""" 50 pass 51 52 53class CrasDBusMonitor(object): 54 """Monitor for DBus signal from Cras.""" 55 def __init__(self): 56 _set_default_main_loop() 57 # Acquires a new Cras interface through a new dbus.SystemBus instance 58 # which has default main loop. 59 self._iface = cras_utils.get_cras_control_interface(private=True) 60 self._loop = _get_gobject().MainLoop() 61 self._count = 0 62 63 64class CrasDBusSignalListener(CrasDBusMonitor): 65 """Listener for certain signal.""" 66 def __init__(self): 67 super(CrasDBusSignalListener, self).__init__() 68 self._target_signal_count = 0 69 70 71 def wait_for_nodes_changed(self, target_signal_count, timeout_secs): 72 """Waits for NodesChanged signal. 73 74 @param target_signal_count: The expected number of signal. 75 @param timeout_secs: The timeout in seconds. 76 77 @raises: CrasDBusMonitorError if there is no enough signals before 78 timeout. 79 80 """ 81 self._target_signal_count = target_signal_count 82 signal_match = self._iface.connect_to_signal( 83 'NodesChanged', self._nodes_changed_handler) 84 _get_gobject().timeout_add( 85 timeout_secs * 1000, self._timeout_quit_main_loop) 86 87 # Blocks here until _nodes_changed_handler or _timeout_quit_main_loop 88 # quits the loop. 89 self._loop.run() 90 91 signal_match.remove() 92 if self._count < self._target_signal_count: 93 raise CrasDBusMonitorError('Timeout') 94 95 96 def _nodes_changed_handler(self): 97 """Handler for NodesChanged signal.""" 98 if self._loop.is_running(): 99 logging.debug('Got NodesChanged signal when loop is running.') 100 self._count = self._count + 1 101 logging.debug('count = %d', self._count) 102 if self._count >= self._target_signal_count: 103 logging.debug('Quit main loop') 104 self._loop.quit() 105 else: 106 logging.debug('Got NodesChanged signal when loop is not running.' 107 ' Ignore it') 108 109 110 def _timeout_quit_main_loop(self): 111 """Handler for timeout in main loop. 112 113 @returns: False so this callback will not be called again. 114 115 """ 116 if self._loop.is_running(): 117 logging.error('Quit main loop because of timeout') 118 self._loop.quit() 119 else: 120 logging.debug( 121 'Got _quit_main_loop after main loop quits. Ignore it') 122 123 return False 124 125 126class CrasDBusBackgroundSignalCounter(object): 127 """Controls signal counter which runs in background.""" 128 def __init__(self): 129 self._proc = None 130 self._signal_name = None 131 self._counter = None 132 self._parent_conn = None 133 self._child_conn = None 134 135 136 def start(self, signal_name): 137 """Starts the signal counter in a subprocess. 138 139 @param signal_name: The name of the signal to count. 140 141 """ 142 self._signal_name = signal_name 143 self._parent_conn, self._child_conn = multiprocessing.Pipe() 144 self._proc = multiprocessing.Process( 145 target=self._run, args=(self._child_conn,)) 146 self._proc.daemon = True 147 self._proc.start() 148 149 150 def _run(self, child_conn): 151 """Runs CrasDBusCounter. 152 153 This should be called in a subprocess. 154 This blocks until parent_conn send stop command to the pipe. 155 156 """ 157 self._counter = CrasDBusCounter(self._signal_name, child_conn) 158 self._counter.run() 159 160 161 def stop(self): 162 """Stops the CrasDBusCounter by sending stop command to parent_conn. 163 164 The result of CrasDBusCounter in its subproces can be obtained by 165 reading from parent_conn. 166 167 @returns: The count of the signal of interest. 168 169 """ 170 self._parent_conn.send(CrasDBusCounter.STOP_CMD) 171 return self._parent_conn.recv() 172 173 174class CrasDBusCounter(CrasDBusMonitor): 175 """Counter for DBus signal sent from Cras""" 176 177 _CHECK_QUIT_PERIOD_SECS = 0.1 178 STOP_CMD = 'stop' 179 180 def __init__(self, signal_name, child_conn, ignore_redundant=True): 181 """Initializes a CrasDBusCounter. 182 183 @param signal_name: The name of the signal of interest. 184 @param child_conn: A multiprocessing.Pipe which is used to receive stop 185 signal and to send the counting result. 186 @param ignore_redundant: Ignores signal if GetNodes result stays the 187 same. This happens when there is change in unplugged nodes, 188 which does not affect Cras client. 189 190 """ 191 super(CrasDBusCounter, self).__init__() 192 self._signal_name = signal_name 193 self._count = None 194 self._child_conn = child_conn 195 self._ignore_redundant = ignore_redundant 196 self._nodes = None 197 198 199 def run(self): 200 """Runs the gobject main loop and listens for the signal.""" 201 self._count = 0 202 203 self._nodes = cras_utils.get_cras_nodes() 204 logging.debug('Before starting the counter') 205 logging.debug('nodes = %s', pprint.pformat(self._nodes)) 206 207 signal_match = self._iface.connect_to_signal( 208 self._signal_name, self._signal_handler) 209 _get_gobject().timeout_add( 210 int(self._CHECK_QUIT_PERIOD_SECS * 1000), 211 self._check_quit_main_loop) 212 213 logging.debug('Start counting for signal %s', self._signal_name) 214 215 # Blocks here until _check_quit_main_loop quits the loop. 216 self._loop.run() 217 218 signal_match.remove() 219 220 logging.debug('Count result: %s', self._count) 221 self._child_conn.send(self._count) 222 223 224 def _signal_handler(self): 225 """Handler for signal.""" 226 if self._loop.is_running(): 227 logging.debug('Got %s signal when loop is running.', 228 self._signal_name) 229 230 logging.debug('Getting nodes.') 231 nodes = cras_utils.get_cras_nodes() 232 logging.debug('nodes = %s', pprint.pformat(nodes)) 233 if self._ignore_redundant and self._nodes == nodes: 234 logging.debug('Nodes did not change. Ignore redundant signal') 235 return 236 237 self._count = self._count + 1 238 logging.debug('count = %d', self._count) 239 else: 240 logging.debug('Got %s signal when loop is not running.' 241 ' Ignore it', self._signal_name) 242 243 244 def _should_stop(self): 245 """Checks if user wants to stop main loop.""" 246 if self._child_conn.poll(): 247 if self._child_conn.recv() == self.STOP_CMD: 248 logging.debug('Should stop') 249 return True 250 return False 251 252 253 def _check_quit_main_loop(self): 254 """Handler for timeout in main loop. 255 256 @returns: True so this callback will not be called again. 257 False if user quits main loop. 258 259 """ 260 if self._loop.is_running(): 261 logging.debug('main loop is running in _check_quit_main_loop') 262 if self._should_stop(): 263 logging.debug('Quit main loop because of stop command') 264 self._loop.quit() 265 return False 266 else: 267 logging.debug('No stop command, keep running') 268 return True 269 else: 270 logging.debug( 271 'Got _quit_main_loop after main loop quits. Ignore it') 272 273 return False 274 275 276class CrasDBusMonitorUnexpectedNodesChanged(Exception): 277 """Error for unexpected nodes changed.""" 278 pass 279 280 281def wait_for_unexpected_nodes_changed(timeout_secs): 282 """Waits for unexpected nodes changed signal in this blocking call. 283 284 @param timeout_secs: Timeout in seconds for waiting. 285 286 @raises CrasDBusMonitorUnexpectedNodesChanged if there is NodesChanged 287 signal 288 289 """ 290 try: 291 CrasDBusSignalListener().wait_for_nodes_changed(1, timeout_secs) 292 except CrasDBusMonitorError: 293 logging.debug('There is no NodesChanged signal, as expected') 294 return 295 raise CrasDBusMonitorUnexpectedNodesChanged() 296