• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Lint as: python2, python3
2# Copyright 2021 The Chromium OS 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"""Simple observer base class."""
6
7from __future__ import absolute_import
8from __future__ import division
9from __future__ import print_function
10
11import functools
12from gi.repository import GLib
13import logging
14import threading
15
16# All GLIB method calls should wait this many seconds by default
17GLIB_METHOD_CALL_TIMEOUT = 2
18
19# GLib thread name that will run the mainloop.
20GLIB_THREAD_NAME = 'glib'
21
22
23class GlibDeadlockException(Exception):
24    """Detected a situation that will cause a deadlock in GLib.
25
26    This exception should be emitted when we detect that a deadlock is likely to
27    occur. For example, a method call running in the mainloop context is making
28    a function call that is wrapped with @glib_call.
29    """
30    pass
31
32
33def glib_call(default_result=None,
34              timeout=GLIB_METHOD_CALL_TIMEOUT,
35              thread_name=GLIB_THREAD_NAME):
36    """Threads method call to glib thread and waits for result.
37
38    The dbus-python package does not support multi-threaded access. As a result,
39    we pipe all dbus function to the mainloop using GLib.idle_add which runs the
40    method as part of the mainloop.
41
42    @param default_result: The default return value from the function call if it
43                           fails or times out.
44    @param timeout: How long to wait for the method call to complete.
45    @param thread_name: Name of the thread that should be running GLib.Mainloop.
46    """
47
48    def decorator(method):
49        """Internal wrapper."""
50
51        def call_and_signal(data):
52            """Calls a function and signals completion.
53
54            This method is called by GLib and added via GLib.idle_add. It will
55            be run in the same thread as the GLib mainloop.
56
57            @param data: Dict containing data to be passed. Must have keys:
58                         event, method, args, kwargs and result. The value for
59                         result should be the default value and will be set
60                         before return.
61
62            @return False so that glib doesn't reschedule this to run again.
63            """
64            (event, method, args, kwargs) = (data['event'], data['method'],
65                                             data['args'], data['kwargs'])
66            logging.info('%s: Running %s',
67                         threading.current_thread().name, str(method))
68            err = None
69            try:
70                data['result'] = method(*args, **kwargs)
71            except Exception as e:
72                logging.error('Exception during %s: %s', str(method), str(e))
73                err = e
74
75            event.set()
76
77            # If method callback is set, this will call that method with results
78            # of this method call and any error that may have resulted.
79            if 'method_callback' in data:
80                data['method_callback'](err, data['result'])
81
82            return False
83
84        @functools.wraps(method)
85        def wrapper(*args, **kwargs):
86            """Sends method call to GLib and waits for its completion.
87
88            @param args: Positional arguments to method.
89            @param kwargs: Keyword arguments to method. Some special keywords:
90                |method_callback|: Returns result via callback without blocking.
91            """
92            method_callback = None
93            # If a method callback is given, we will not block on the completion
94            # of the call but expect the response in the callback instead. The
95            # callback has the signature: def callback(err, result)
96            if 'method_callback' in kwargs:
97                method_callback = kwargs['method_callback']
98                del kwargs['method_callback']
99
100            # Make sure we're not scheduling in the GLib thread since that'll
101            # cause a deadlock. An exception is if we have a method callback
102            # which is async.
103            current_thread_name = threading.current_thread().name
104            if current_thread_name is thread_name and not method_callback:
105                raise GlibDeadlockException(
106                        '{} called in GLib thread'.format(method))
107
108            done_event = threading.Event()
109            data = {
110                    'event': done_event,
111                    'method': method,
112                    'args': args,
113                    'kwargs': kwargs,
114                    'result': default_result,
115            }
116            if method_callback:
117                data['method_callback'] = method_callback
118
119            logging.info('%s: Adding %s to GLib.idle_add',
120                         threading.current_thread().name, str(method))
121            GLib.idle_add(call_and_signal, data)
122
123            if not method_callback:
124                # Wait for the result from the GLib call
125                if not done_event.wait(timeout=timeout):
126                    logging.warn('%s timed out after %d s', str(method),
127                                 timeout)
128
129            return data['result']
130
131        return wrapper
132
133    return decorator
134
135
136def glib_callback(thread_name=GLIB_THREAD_NAME):
137    """Marks callbacks that are called by GLib and checks for errors.
138    """
139
140    def _decorator(method):
141        @functools.wraps(method)
142        def _wrapper(*args, **kwargs):
143            current_thread_name = threading.current_thread().name
144            if current_thread_name is not thread_name:
145                raise GlibDeadlockException(
146                        '{} should be called by GLib'.format(method))
147
148            return method(*args, **kwargs)
149
150        return _wrapper
151
152    return _decorator
153
154
155class PropertySet:
156    """Helper class with getters and setters for properties. """
157
158    class MissingProperty(Exception):
159        """Raised when property is missing in PropertySet."""
160        pass
161
162    class PropertyGetterMissing(Exception):
163        """Raised when get is called on a property that doesn't support it."""
164        pass
165
166    class PropertySetterMissing(Exception):
167        """Raised when set is called on a property that doesn't support it."""
168        pass
169
170    def __init__(self, property_set):
171        """Constructor.
172
173        @param property_set: Dictionary with proxy methods for get/set of named
174                             properties. These are NOT normal DBus properties
175                             that are implemented via
176                             org.freedesktop.DBus.Properties.
177        """
178        self.pset = property_set
179
180    def get(self, prop_name, *args):
181        """Calls the getter function for a property if it exists.
182
183        @param prop_name: The property name to call the getter function on.
184        @param args: Any positional arguments to pass to getter function.
185
186        @return Result from calling the getter function with given args.
187        """
188        if prop_name not in self.pset:
189            raise self.MissingProperty('{} is unknown.'.format(prop_name))
190
191        (getter, _) = self.pset[prop_name]
192
193        if not getter:
194            raise self.PropertyGetterMissing(
195                    '{} has no getter.'.format(prop_name))
196
197        return getter(*args)
198
199    def set(self, prop_name, *args):
200        """Calls the setter function for a property if it exists.
201
202        @param prop_name: The property name to call the setter function on.
203        @param args: Any positional arguments to pass to the setter function.
204
205        @return Result from calling the setter function with given args.
206        """
207        if prop_name not in self.pset:
208            raise self.MissingProperty('{} is unknown.'.format(prop_name))
209
210        (_, setter) = self.pset[prop_name]
211
212        if not setter:
213            raise self.PropertySetterMissing(
214                    '{} has no getter.'.format(prop_name))
215
216        return setter(*args)
217