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