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