You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
297 lines
9.2 KiB
297 lines
9.2 KiB
# 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 autotest_lib.client.cros.audio 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.
|
|
|
|
"""
|
|
try:
|
|
import dbus.mainloop.glib
|
|
except ImportError as e:
|
|
logging.exception(
|
|
'Can not import dbus.mainloop.glib: %s. '
|
|
'This method should only be called on Cros device.', e)
|
|
raise
|
|
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
|
|
|
|
|
def _get_gobject():
|
|
"""Tries to import gobject.
|
|
|
|
@returns: The imported gobject module.
|
|
|
|
@raises: ImportError if gobject can not be imported.
|
|
|
|
"""
|
|
try:
|
|
import gobject
|
|
except ImportError as e:
|
|
logging.exception(
|
|
'Can not import gobject: %s. This method should only be '
|
|
'called on Cros device.', e)
|
|
raise
|
|
return gobject
|
|
|
|
|
|
class CrasDBusMonitorError(Exception):
|
|
"""Error in CrasDBusMonitor."""
|
|
pass
|
|
|
|
|
|
class CrasDBusMonitor(object):
|
|
"""Monitor for DBus signal from Cras."""
|
|
def __init__(self):
|
|
_set_default_main_loop()
|
|
# 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
|
|
timeout.
|
|
|
|
"""
|
|
self._target_signal_count = target_signal_count
|
|
signal_match = self._iface.connect_to_signal(
|
|
'NodesChanged', self._nodes_changed_handler)
|
|
_get_gobject().timeout_add(
|
|
timeout_secs * 1000, self._timeout_quit_main_loop)
|
|
|
|
# Blocks here until _nodes_changed_handler or _timeout_quit_main_loop
|
|
# quits the loop.
|
|
self._loop.run()
|
|
|
|
signal_match.remove()
|
|
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')
|
|
self._loop.quit()
|
|
else:
|
|
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')
|
|
self._loop.quit()
|
|
else:
|
|
logging.debug(
|
|
'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
|
|
self._proc.start()
|
|
|
|
|
|
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)
|
|
self._counter.run()
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
self._parent_conn.send(CrasDBusCounter.STOP_CMD)
|
|
return self._parent_conn.recv()
|
|
|
|
|
|
class CrasDBusCounter(CrasDBusMonitor):
|
|
"""Counter for DBus signal sent from Cras"""
|
|
|
|
_CHECK_QUIT_PERIOD_SECS = 0.1
|
|
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)
|
|
_get_gobject().timeout_add(
|
|
int(self._CHECK_QUIT_PERIOD_SECS * 1000),
|
|
self._check_quit_main_loop)
|
|
|
|
logging.debug('Start counting for signal %s', self._signal_name)
|
|
|
|
# Blocks here until _check_quit_main_loop quits the loop.
|
|
self._loop.run()
|
|
|
|
signal_match.remove()
|
|
|
|
logging.debug('Count result: %s', self._count)
|
|
self._child_conn.send(self._count)
|
|
|
|
|
|
def _signal_handler(self):
|
|
"""Handler for signal."""
|
|
if self._loop.is_running():
|
|
logging.debug('Got %s signal when loop is running.',
|
|
self._signal_name)
|
|
|
|
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')
|
|
return
|
|
|
|
self._count = self._count + 1
|
|
logging.debug('count = %d', self._count)
|
|
else:
|
|
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')
|
|
self._loop.quit()
|
|
return False
|
|
else:
|
|
logging.debug('No stop command, keep running')
|
|
return True
|
|
else:
|
|
logging.debug(
|
|
'Got _quit_main_loop after main loop quits. Ignore it')
|
|
|
|
return False
|
|
|
|
|
|
class CrasDBusMonitorUnexpectedNodesChanged(Exception):
|
|
"""Error for unexpected nodes changed."""
|
|
pass
|
|
|
|
|
|
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
|
|
signal
|
|
|
|
"""
|
|
try:
|
|
CrasDBusSignalListener().wait_for_nodes_changed(1, timeout_secs)
|
|
except CrasDBusMonitorError:
|
|
logging.debug('There is no NodesChanged signal, as expected')
|
|
return
|
|
raise CrasDBusMonitorUnexpectedNodesChanged()
|