unplugged-system/external/autotest/client/cros/bluetooth/floss/utils.py

217 lines
7.7 KiB
Python

# Lint as: python2, python3
# Copyright 2021 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Simple observer base class."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import functools
from gi.repository import GLib
import logging
import threading
# All GLIB method calls should wait this many seconds by default
GLIB_METHOD_CALL_TIMEOUT = 2
# GLib thread name that will run the mainloop.
GLIB_THREAD_NAME = 'glib'
class GlibDeadlockException(Exception):
"""Detected a situation that will cause a deadlock in GLib.
This exception should be emitted when we detect that a deadlock is likely to
occur. For example, a method call running in the mainloop context is making
a function call that is wrapped with @glib_call.
"""
pass
def glib_call(default_result=None,
timeout=GLIB_METHOD_CALL_TIMEOUT,
thread_name=GLIB_THREAD_NAME):
"""Threads method call to glib thread and waits for result.
The dbus-python package does not support multi-threaded access. As a result,
we pipe all dbus function to the mainloop using GLib.idle_add which runs the
method as part of the mainloop.
@param default_result: The default return value from the function call if it
fails or times out.
@param timeout: How long to wait for the method call to complete.
@param thread_name: Name of the thread that should be running GLib.Mainloop.
"""
def decorator(method):
"""Internal wrapper."""
def call_and_signal(data):
"""Calls a function and signals completion.
This method is called by GLib and added via GLib.idle_add. It will
be run in the same thread as the GLib mainloop.
@param data: Dict containing data to be passed. Must have keys:
event, method, args, kwargs and result. The value for
result should be the default value and will be set
before return.
@return False so that glib doesn't reschedule this to run again.
"""
(event, method, args, kwargs) = (data['event'], data['method'],
data['args'], data['kwargs'])
logging.info('%s: Running %s',
threading.current_thread().name, str(method))
err = None
try:
data['result'] = method(*args, **kwargs)
except Exception as e:
logging.error('Exception during %s: %s', str(method), str(e))
err = e
event.set()
# If method callback is set, this will call that method with results
# of this method call and any error that may have resulted.
if 'method_callback' in data:
data['method_callback'](err, data['result'])
return False
@functools.wraps(method)
def wrapper(*args, **kwargs):
"""Sends method call to GLib and waits for its completion.
@param args: Positional arguments to method.
@param kwargs: Keyword arguments to method. Some special keywords:
|method_callback|: Returns result via callback without blocking.
"""
method_callback = None
# If a method callback is given, we will not block on the completion
# of the call but expect the response in the callback instead. The
# callback has the signature: def callback(err, result)
if 'method_callback' in kwargs:
method_callback = kwargs['method_callback']
del kwargs['method_callback']
# Make sure we're not scheduling in the GLib thread since that'll
# cause a deadlock. An exception is if we have a method callback
# which is async.
current_thread_name = threading.current_thread().name
if current_thread_name is thread_name and not method_callback:
raise GlibDeadlockException(
'{} called in GLib thread'.format(method))
done_event = threading.Event()
data = {
'event': done_event,
'method': method,
'args': args,
'kwargs': kwargs,
'result': default_result,
}
if method_callback:
data['method_callback'] = method_callback
logging.info('%s: Adding %s to GLib.idle_add',
threading.current_thread().name, str(method))
GLib.idle_add(call_and_signal, data)
if not method_callback:
# Wait for the result from the GLib call
if not done_event.wait(timeout=timeout):
logging.warn('%s timed out after %d s', str(method),
timeout)
return data['result']
return wrapper
return decorator
def glib_callback(thread_name=GLIB_THREAD_NAME):
"""Marks callbacks that are called by GLib and checks for errors.
"""
def _decorator(method):
@functools.wraps(method)
def _wrapper(*args, **kwargs):
current_thread_name = threading.current_thread().name
if current_thread_name is not thread_name:
raise GlibDeadlockException(
'{} should be called by GLib'.format(method))
return method(*args, **kwargs)
return _wrapper
return _decorator
class PropertySet:
"""Helper class with getters and setters for properties. """
class MissingProperty(Exception):
"""Raised when property is missing in PropertySet."""
pass
class PropertyGetterMissing(Exception):
"""Raised when get is called on a property that doesn't support it."""
pass
class PropertySetterMissing(Exception):
"""Raised when set is called on a property that doesn't support it."""
pass
def __init__(self, property_set):
"""Constructor.
@param property_set: Dictionary with proxy methods for get/set of named
properties. These are NOT normal DBus properties
that are implemented via
org.freedesktop.DBus.Properties.
"""
self.pset = property_set
def get(self, prop_name, *args):
"""Calls the getter function for a property if it exists.
@param prop_name: The property name to call the getter function on.
@param args: Any positional arguments to pass to getter function.
@return Result from calling the getter function with given args.
"""
if prop_name not in self.pset:
raise self.MissingProperty('{} is unknown.'.format(prop_name))
(getter, _) = self.pset[prop_name]
if not getter:
raise self.PropertyGetterMissing(
'{} has no getter.'.format(prop_name))
return getter(*args)
def set(self, prop_name, *args):
"""Calls the setter function for a property if it exists.
@param prop_name: The property name to call the setter function on.
@param args: Any positional arguments to pass to the setter function.
@return Result from calling the setter function with given args.
"""
if prop_name not in self.pset:
raise self.MissingProperty('{} is unknown.'.format(prop_name))
(_, setter) = self.pset[prop_name]
if not setter:
raise self.PropertySetterMissing(
'{} has no getter.'.format(prop_name))
return setter(*args)