Home | History | Annotate | Download | only in audio
      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 
      7 import logging
      8 import multiprocessing
      9 import pprint
     10 
     11 from autotest_lib.client.cros.audio import cras_utils
     12 
     13 
     14 def _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 
     30 def _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 
     48 class CrasDBusMonitorError(Exception):
     49     """Error in CrasDBusMonitor."""
     50     pass
     51 
     52 
     53 class 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 
     64 class 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 
    126 class 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 
    174 class 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 
    276 class CrasDBusMonitorUnexpectedNodesChanged(Exception):
    277     """Error for unexpected nodes changed."""
    278     pass
    279 
    280 
    281 def 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