blob: 2279a504598eeca5e6b036b2a8b79c90e184d2ef [file] [log] [blame]
# Lint as: python2, python3
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This module provides cras DBus audio utilities."""
import logging
import multiprocessing
import pprint
from import cras_utils
def _set_default_main_loop():
"""Sets the gobject main loop to be the event loop for DBus.
@raises: ImportError if dbus.mainloop.glib can not be imported.
import dbus.mainloop.glib
except ImportError as e:
'Can not import dbus.mainloop.glib: %s. '
'This method should only be called on Cros device.', e)
def _get_gobject():
"""Tries to import gobject.
@returns: The imported gobject module.
@raises: ImportError if gobject can not be imported.
import gobject
except ImportError as e:
'Can not import gobject: %s. This method should only be '
'called on Cros device.', e)
return gobject
class CrasDBusMonitorError(Exception):
"""Error in CrasDBusMonitor."""
class CrasDBusMonitor(object):
"""Monitor for DBus signal from Cras."""
def __init__(self):
# Acquires a new Cras interface through a new dbus.SystemBus instance
# which has default main loop.
self._iface = cras_utils.get_cras_control_interface(private=True)
self._loop = _get_gobject().MainLoop()
self._count = 0
class CrasDBusSignalListener(CrasDBusMonitor):
"""Listener for certain signal."""
def __init__(self):
super(CrasDBusSignalListener, self).__init__()
self._target_signal_count = 0
def wait_for_nodes_changed(self, target_signal_count, timeout_secs):
"""Waits for NodesChanged signal.
@param target_signal_count: The expected number of signal.
@param timeout_secs: The timeout in seconds.
@raises: CrasDBusMonitorError if there is no enough signals before
self._target_signal_count = target_signal_count
signal_match = self._iface.connect_to_signal(
'NodesChanged', self._nodes_changed_handler)
timeout_secs * 1000, self._timeout_quit_main_loop)
# Blocks here until _nodes_changed_handler or _timeout_quit_main_loop
# quits the loop.
if self._count < self._target_signal_count:
raise CrasDBusMonitorError('Timeout')
def _nodes_changed_handler(self):
"""Handler for NodesChanged signal."""
if self._loop.is_running():
logging.debug('Got NodesChanged signal when loop is running.')
self._count = self._count + 1
logging.debug('count = %d', self._count)
if self._count >= self._target_signal_count:
logging.debug('Quit main loop')
logging.debug('Got NodesChanged signal when loop is not running.'
' Ignore it')
def _timeout_quit_main_loop(self):
"""Handler for timeout in main loop.
@returns: False so this callback will not be called again.
if self._loop.is_running():
logging.error('Quit main loop because of timeout')
'Got _quit_main_loop after main loop quits. Ignore it')
return False
class CrasDBusBackgroundSignalCounter(object):
"""Controls signal counter which runs in background."""
def __init__(self):
self._proc = None
self._signal_name = None
self._counter = None
self._parent_conn = None
self._child_conn = None
def start(self, signal_name):
"""Starts the signal counter in a subprocess.
@param signal_name: The name of the signal to count.
self._signal_name = signal_name
self._parent_conn, self._child_conn = multiprocessing.Pipe()
self._proc = multiprocessing.Process(
target=self._run, args=(self._child_conn,))
self._proc.daemon = True
def _run(self, child_conn):
"""Runs CrasDBusCounter.
This should be called in a subprocess.
This blocks until parent_conn send stop command to the pipe.
self._counter = CrasDBusCounter(self._signal_name, child_conn)
def stop(self):
"""Stops the CrasDBusCounter by sending stop command to parent_conn.
The result of CrasDBusCounter in its subproces can be obtained by
reading from parent_conn.
@returns: The count of the signal of interest.
return self._parent_conn.recv()
class CrasDBusCounter(CrasDBusMonitor):
"""Counter for DBus signal sent from Cras"""
STOP_CMD = 'stop'
def __init__(self, signal_name, child_conn, ignore_redundant=True):
"""Initializes a CrasDBusCounter.
@param signal_name: The name of the signal of interest.
@param child_conn: A multiprocessing.Pipe which is used to receive stop
signal and to send the counting result.
@param ignore_redundant: Ignores signal if GetNodes result stays the
same. This happens when there is change in unplugged nodes,
which does not affect Cras client.
super(CrasDBusCounter, self).__init__()
self._signal_name = signal_name
self._count = None
self._child_conn = child_conn
self._ignore_redundant = ignore_redundant
self._nodes = None
def run(self):
"""Runs the gobject main loop and listens for the signal."""
self._count = 0
self._nodes = cras_utils.get_cras_nodes()
logging.debug('Before starting the counter')
logging.debug('nodes = %s', pprint.pformat(self._nodes))
signal_match = self._iface.connect_to_signal(
self._signal_name, self._signal_handler)
int(self._CHECK_QUIT_PERIOD_SECS * 1000),
logging.debug('Start counting for signal %s', self._signal_name)
# Blocks here until _check_quit_main_loop quits the loop.
logging.debug('Count result: %s', self._count)
def _signal_handler(self):
"""Handler for signal."""
if self._loop.is_running():
logging.debug('Got %s signal when loop is running.',
logging.debug('Getting nodes.')
nodes = cras_utils.get_cras_nodes()
logging.debug('nodes = %s', pprint.pformat(nodes))
if self._ignore_redundant and self._nodes == nodes:
logging.debug('Nodes did not change. Ignore redundant signal')
self._count = self._count + 1
logging.debug('count = %d', self._count)
logging.debug('Got %s signal when loop is not running.'
' Ignore it', self._signal_name)
def _should_stop(self):
"""Checks if user wants to stop main loop."""
if self._child_conn.poll():
if self._child_conn.recv() == self.STOP_CMD:
logging.debug('Should stop')
return True
return False
def _check_quit_main_loop(self):
"""Handler for timeout in main loop.
@returns: True so this callback will not be called again.
False if user quits main loop.
if self._loop.is_running():
logging.debug('main loop is running in _check_quit_main_loop')
if self._should_stop():
logging.debug('Quit main loop because of stop command')
return False
logging.debug('No stop command, keep running')
return True
'Got _quit_main_loop after main loop quits. Ignore it')
return False
class CrasDBusMonitorUnexpectedNodesChanged(Exception):
"""Error for unexpected nodes changed."""
def wait_for_unexpected_nodes_changed(timeout_secs):
"""Waits for unexpected nodes changed signal in this blocking call.
@param timeout_secs: Timeout in seconds for waiting.
@raises CrasDBusMonitorUnexpectedNodesChanged if there is NodesChanged
CrasDBusSignalListener().wait_for_nodes_changed(1, timeout_secs)
except CrasDBusMonitorError:
logging.debug('There is no NodesChanged signal, as expected')
raise CrasDBusMonitorUnexpectedNodesChanged()