mirror of
https://github.com/ARM-software/devlib.git
synced 2025-01-31 02:00:45 +00:00
target: Create new connection for reentrant calls
When Target.conn property is required while the current connection is already in use, provide a fresh connection to avoid deadlocks. This is enabled by the @call_conn decorator that is used on all Target methods that use self.conn directly.
This commit is contained in:
parent
796536d67d
commit
79783fa09a
@ -76,6 +76,48 @@ GOOGLE_DNS_SERVER_ADDRESS = '8.8.8.8'
|
|||||||
|
|
||||||
installed_package_info = namedtuple('installed_package_info', 'apk_path package')
|
installed_package_info = namedtuple('installed_package_info', 'apk_path package')
|
||||||
|
|
||||||
|
|
||||||
|
def call_conn(f):
|
||||||
|
"""
|
||||||
|
Decorator to be used on all :class:`devlib.target.Target` methods that
|
||||||
|
directly use a method of ``self.conn``.
|
||||||
|
|
||||||
|
This ensures that if a call to any of the decorated method occurs while
|
||||||
|
executing, a new connection will be created in order to avoid possible
|
||||||
|
deadlocks. This can happen if e.g. a target's method is called from
|
||||||
|
``__del__``, which could be executed by the garbage collector, interrupting
|
||||||
|
another call to a method of the connection instance.
|
||||||
|
|
||||||
|
.. note:: This decorator could be applied directly to all methods with a
|
||||||
|
metaclass or ``__init_subclass__`` but it could create issues when
|
||||||
|
passing target methods as callbacks to connections' methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(f)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
reentered = self.conn.is_in_use
|
||||||
|
disconnect = False
|
||||||
|
try:
|
||||||
|
# If the connection was already in use we need to use a different
|
||||||
|
# instance to avoid reentrancy deadlocks. This can happen even in
|
||||||
|
# single threaded code via __del__ implementations that can be
|
||||||
|
# called at any point.
|
||||||
|
if reentered:
|
||||||
|
# Shallow copy so we can use another connection instance
|
||||||
|
_self = copy.copy(self)
|
||||||
|
_self.conn = _self.get_connection()
|
||||||
|
assert self.conn is not _self.conn
|
||||||
|
disconnect = True
|
||||||
|
else:
|
||||||
|
_self = self
|
||||||
|
return f(_self, *args, **kwargs)
|
||||||
|
finally:
|
||||||
|
if disconnect:
|
||||||
|
_self.disconnect()
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
class Target(object):
|
class Target(object):
|
||||||
|
|
||||||
path = None
|
path = None
|
||||||
@ -294,6 +336,14 @@ class Target(object):
|
|||||||
if connect:
|
if connect:
|
||||||
self.connect()
|
self.connect()
|
||||||
|
|
||||||
|
def __copy__(self):
|
||||||
|
new = self.__class__.__new__(self.__class__)
|
||||||
|
new.__dict__ = self.__dict__.copy()
|
||||||
|
# Avoid sharing the connection instance with the original target, so
|
||||||
|
# that each target can live its own independent life
|
||||||
|
del new.__dict__['_conn']
|
||||||
|
return new
|
||||||
|
|
||||||
# connection and initialization
|
# connection and initialization
|
||||||
|
|
||||||
def connect(self, timeout=None, check_boot_completed=True):
|
def connect(self, timeout=None, check_boot_completed=True):
|
||||||
@ -433,6 +483,7 @@ class Target(object):
|
|||||||
dst_mkdir(dest)
|
dst_mkdir(dest)
|
||||||
|
|
||||||
|
|
||||||
|
@call_conn
|
||||||
def push(self, source, dest, as_root=False, timeout=None, globbing=False): # pylint: disable=arguments-differ
|
def push(self, source, dest, as_root=False, timeout=None, globbing=False): # pylint: disable=arguments-differ
|
||||||
sources = glob.glob(source) if globbing else [source]
|
sources = glob.glob(source) if globbing else [source]
|
||||||
self._prepare_xfer('push', sources, dest)
|
self._prepare_xfer('push', sources, dest)
|
||||||
@ -488,6 +539,7 @@ class Target(object):
|
|||||||
|
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
@call_conn
|
||||||
def pull(self, source, dest, as_root=False, timeout=None, globbing=False): # pylint: disable=arguments-differ
|
def pull(self, source, dest, as_root=False, timeout=None, globbing=False): # pylint: disable=arguments-differ
|
||||||
if globbing:
|
if globbing:
|
||||||
sources = self._expand_glob(source, as_root=as_root)
|
sources = self._expand_glob(source, as_root=as_root)
|
||||||
@ -557,6 +609,7 @@ class Target(object):
|
|||||||
|
|
||||||
return command
|
return command
|
||||||
|
|
||||||
|
@call_conn
|
||||||
def execute(self, command, timeout=None, check_exit_code=True,
|
def execute(self, command, timeout=None, check_exit_code=True,
|
||||||
as_root=False, strip_colors=True, will_succeed=False,
|
as_root=False, strip_colors=True, will_succeed=False,
|
||||||
force_locale='C'):
|
force_locale='C'):
|
||||||
@ -566,6 +619,7 @@ class Target(object):
|
|||||||
check_exit_code=check_exit_code, as_root=as_root,
|
check_exit_code=check_exit_code, as_root=as_root,
|
||||||
strip_colors=strip_colors, will_succeed=will_succeed)
|
strip_colors=strip_colors, will_succeed=will_succeed)
|
||||||
|
|
||||||
|
@call_conn
|
||||||
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False,
|
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False,
|
||||||
force_locale='C', timeout=None):
|
force_locale='C', timeout=None):
|
||||||
command = self._prepare_cmd(command, force_locale)
|
command = self._prepare_cmd(command, force_locale)
|
||||||
@ -691,6 +745,7 @@ class Target(object):
|
|||||||
pass
|
pass
|
||||||
self.conn.connected_as_root = None
|
self.conn.connected_as_root = None
|
||||||
|
|
||||||
|
@call_conn
|
||||||
def check_responsive(self, explode=True):
|
def check_responsive(self, explode=True):
|
||||||
try:
|
try:
|
||||||
self.conn.execute('ls /', timeout=5)
|
self.conn.execute('ls /', timeout=5)
|
||||||
@ -1005,6 +1060,7 @@ class Target(object):
|
|||||||
os.remove(shutils_ofile)
|
os.remove(shutils_ofile)
|
||||||
os.rmdir(tmp_dir)
|
os.rmdir(tmp_dir)
|
||||||
|
|
||||||
|
@call_conn
|
||||||
def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
|
def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
|
||||||
command = '{} {}'.format(self.shutils, command)
|
command = '{} {}'.format(self.shutils, command)
|
||||||
return self.conn.execute(command, timeout, check_exit_code, as_root)
|
return self.conn.execute(command, timeout, check_exit_code, as_root)
|
||||||
@ -1169,6 +1225,7 @@ class LinuxTarget(Target):
|
|||||||
def wait_boot_complete(self, timeout=10):
|
def wait_boot_complete(self, timeout=10):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@call_conn
|
||||||
def kick_off(self, command, as_root=False):
|
def kick_off(self, command, as_root=False):
|
||||||
command = 'sh -c {} 1>/dev/null 2>/dev/null &'.format(quote(command))
|
command = 'sh -c {} 1>/dev/null 2>/dev/null &'.format(quote(command))
|
||||||
return self.conn.execute(command, as_root=as_root)
|
return self.conn.execute(command, as_root=as_root)
|
||||||
@ -1711,9 +1768,11 @@ class AndroidTarget(Target):
|
|||||||
def get_logcat_monitor(self, regexps=None):
|
def get_logcat_monitor(self, regexps=None):
|
||||||
return LogcatMonitor(self, regexps)
|
return LogcatMonitor(self, regexps)
|
||||||
|
|
||||||
|
@call_conn
|
||||||
def wait_for_device(self, timeout=30):
|
def wait_for_device(self, timeout=30):
|
||||||
self.conn.wait_for_device()
|
self.conn.wait_for_device()
|
||||||
|
|
||||||
|
@call_conn
|
||||||
def reboot_bootloader(self, timeout=30):
|
def reboot_bootloader(self, timeout=30):
|
||||||
self.conn.reboot_bootloader()
|
self.conn.reboot_bootloader()
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ import string
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import types
|
||||||
import wrapt
|
import wrapt
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
@ -883,10 +884,14 @@ class _BoundTLSProperty:
|
|||||||
|
|
||||||
class InitCheckpointMeta(type):
|
class InitCheckpointMeta(type):
|
||||||
"""
|
"""
|
||||||
Metaclass providing an ``initialized`` boolean attributes on instances.
|
Metaclass providing an ``initialized`` and ``is_in_use`` boolean attributes
|
||||||
|
on instances.
|
||||||
|
|
||||||
``initialized`` is set to ``True`` once the ``__init__`` constructor has
|
``initialized`` is set to ``True`` once the ``__init__`` constructor has
|
||||||
returned. It will deal cleanly with nested calls to ``super().__init__``.
|
returned. It will deal cleanly with nested calls to ``super().__init__``.
|
||||||
|
|
||||||
|
``is_in_use`` is set to ``True`` when an instance method is being called.
|
||||||
|
This allows to detect reentrance.
|
||||||
"""
|
"""
|
||||||
def __new__(metacls, name, bases, dct, **kwargs):
|
def __new__(metacls, name, bases, dct, **kwargs):
|
||||||
cls = super().__new__(metacls, name, bases, dct, **kwargs)
|
cls = super().__new__(metacls, name, bases, dct, **kwargs)
|
||||||
@ -895,6 +900,7 @@ class InitCheckpointMeta(type):
|
|||||||
@wraps(init_f)
|
@wraps(init_f)
|
||||||
def init_wrapper(self, *args, **kwargs):
|
def init_wrapper(self, *args, **kwargs):
|
||||||
self.initialized = False
|
self.initialized = False
|
||||||
|
self.is_in_use = False
|
||||||
|
|
||||||
# Track the nesting of super()__init__ to set initialized=True only
|
# Track the nesting of super()__init__ to set initialized=True only
|
||||||
# when the outer level is finished
|
# when the outer level is finished
|
||||||
@ -918,6 +924,45 @@ class InitCheckpointMeta(type):
|
|||||||
|
|
||||||
cls.__init__ = init_wrapper
|
cls.__init__ = init_wrapper
|
||||||
|
|
||||||
|
# Set the is_in_use attribute to allow external code to detect if the
|
||||||
|
# methods are about to be re-entered.
|
||||||
|
def make_wrapper(f):
|
||||||
|
if f is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
f_ = f.__get__(self, self.__class__)
|
||||||
|
initial_state = self.is_in_use
|
||||||
|
try:
|
||||||
|
self.is_in_use = True
|
||||||
|
return f_(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
self.is_in_use = initial_state
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
# This will not decorate methods defined in base classes, but we cannot
|
||||||
|
# use inspect.getmembers() as it uses __get__ to bind the attributes to
|
||||||
|
# the class, making staticmethod indistinguishible from instance
|
||||||
|
# methods.
|
||||||
|
for name, attr in cls.__dict__.items():
|
||||||
|
# Only wrap the methods (exposed as functions), not things like
|
||||||
|
# classmethod or staticmethod
|
||||||
|
if (
|
||||||
|
name not in ('__init__', '__new__') and
|
||||||
|
isinstance(attr, types.FunctionType)
|
||||||
|
):
|
||||||
|
setattr(cls, name, make_wrapper(attr))
|
||||||
|
elif isinstance(attr, property):
|
||||||
|
prop = property(
|
||||||
|
fget=make_wrapper(attr.fget),
|
||||||
|
fset=make_wrapper(attr.fset),
|
||||||
|
fdel=make_wrapper(attr.fdel),
|
||||||
|
doc=attr.__doc__,
|
||||||
|
)
|
||||||
|
setattr(cls, name, prop)
|
||||||
|
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user