• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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