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