466 lines
14 KiB
Python
466 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
|
|
@author: Fabio Erculiani <lxnay@sabayon.org>
|
|
@contact: lxnay@sabayon.org
|
|
@copyright: Fabio Erculiani
|
|
@license: GPL-2
|
|
|
|
B{Entropy resources lock functions module}.
|
|
|
|
"""
|
|
import contextlib
|
|
import errno
|
|
import fcntl
|
|
import threading
|
|
import os
|
|
|
|
from entropy.cache import EntropyCacher
|
|
from entropy.const import etpConst, const_setup_directory, const_setup_file
|
|
from entropy.core.settings.base import SystemSettings
|
|
from entropy.i18n import _
|
|
from entropy.misc import FlockFile
|
|
from entropy.output import TextInterface, blue, darkred, darkgreen, teal
|
|
|
|
|
|
class SimpleFileLock(object):
|
|
"""
|
|
Helper class that makes it easy to acquire and release file based locks.
|
|
"""
|
|
|
|
@classmethod
|
|
def acquire_lock(cls, lock_file, lock_map):
|
|
"""
|
|
Make possible to protect a code region using an EXCLUSIVE, non-blocking
|
|
file lock. A lock map (dict) is required in order to register the lock
|
|
data (usually lock file object) and then unlock it using release_lock().
|
|
|
|
@param lock_file: path to lock file used for locking
|
|
@type lock_file: string
|
|
@param lock_map: lock map (dict object) that can be used to
|
|
record the lock data in order to unlock it on release_lock().
|
|
@type lock_map: dict
|
|
@return: True, if lock has been acquired, False otherwise
|
|
@rtype: bool
|
|
"""
|
|
lock_f = open(lock_file, "a+")
|
|
try:
|
|
fcntl.flock(lock_f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
lock_f.truncate()
|
|
lock_f.write(str(os.getpid()))
|
|
lock_f.flush()
|
|
lock_map[lock_file] = lock_f
|
|
return True
|
|
except IOError as err:
|
|
lock_f.close()
|
|
if err.errno not in (errno.EACCES, errno.EAGAIN,):
|
|
# ouch, wtf?
|
|
raise
|
|
return False # lock already acquired
|
|
except Exception:
|
|
lock_f.close()
|
|
raise
|
|
|
|
@classmethod
|
|
def release_lock(cls, lock_file, lock_map):
|
|
"""
|
|
Release a previously acquired lock through acquire_lock().
|
|
|
|
@param lock_file: path to lock file used for locking
|
|
@type lock_file: string
|
|
@param lock_map: lock map (dict object) that can be used to
|
|
record the lock data in order to unlock it on release_lock().
|
|
@type lock_map: dict
|
|
"""
|
|
try:
|
|
lock_f = lock_map.pop(lock_file)
|
|
except KeyError:
|
|
lock_f = None
|
|
|
|
if lock_f is not None:
|
|
fcntl.flock(lock_f.fileno(), fcntl.LOCK_UN)
|
|
lock_f.close()
|
|
|
|
try:
|
|
os.remove(lock_file)
|
|
except OSError as err:
|
|
# cope with possible race conditions
|
|
if err.errno != errno.ENOENT:
|
|
raise
|
|
|
|
|
|
class ResourceLock(object):
|
|
"""
|
|
Generic Entropy Resource Lock abstract class.
|
|
"""
|
|
|
|
_TLS = threading.local()
|
|
|
|
def __init__(self, lock_map, lock_mutex, output=None):
|
|
"""
|
|
Object constructor.
|
|
|
|
@keyword output: a TextInterface interface
|
|
@type output: entropy.output.TextInterface or None
|
|
"""
|
|
self._lock_map = lock_map
|
|
self._lock_mutex = lock_mutex
|
|
|
|
if output is not None:
|
|
self._out = output
|
|
else:
|
|
self._out = TextInterface
|
|
|
|
def path(self):
|
|
"""
|
|
Return the path to the lock file.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def _file_lock_setup(self, file_path):
|
|
"""
|
|
Setup _FILE_LOCK_MAP for file_path, allocating locking information.
|
|
"""
|
|
mapped = self._lock_map.get(file_path)
|
|
if mapped is None:
|
|
mapped = {
|
|
'count': 0,
|
|
'ref': None,
|
|
'path': file_path,
|
|
'shared': None,
|
|
}
|
|
self._lock_map[file_path] = mapped
|
|
return mapped
|
|
|
|
def _lock_resource(self, blocking, shared):
|
|
"""
|
|
Internal function that does the locking given a lock
|
|
file path.
|
|
"""
|
|
lock_path = self.path()
|
|
|
|
with self._lock_mutex:
|
|
mapped = self._file_lock_setup(lock_path)
|
|
|
|
# I asked for an exclusive lock, but
|
|
# I am only holding a shared one, don't
|
|
# return True.
|
|
want_exclusive_when_shared = not shared and mapped['shared']
|
|
|
|
if mapped['ref'] is not None:
|
|
if not want_exclusive_when_shared:
|
|
# reentrant lock, already acquired
|
|
mapped['count'] += 1
|
|
return True
|
|
|
|
else:
|
|
mapped['shared'] = shared
|
|
|
|
# watch for deadlocks using TLS
|
|
recursed = getattr(self._TLS, "recursed", False)
|
|
if recursed and want_exclusive_when_shared:
|
|
# deadlock, raise exception
|
|
raise RuntimeError(
|
|
"want exclusive lock when shared acquired")
|
|
|
|
# not the same thread requested an exclusive lock when shared
|
|
self._TLS.recursed = True
|
|
# fall through, we won't deadlock
|
|
|
|
path = mapped['path']
|
|
|
|
acquired, flock_f = self._file_lock_create(
|
|
path, blocking=blocking, shared=shared)
|
|
|
|
if acquired:
|
|
with self._lock_mutex:
|
|
mapped['count'] += 1
|
|
if flock_f is not None:
|
|
mapped['ref'] = flock_f
|
|
|
|
return acquired
|
|
|
|
def _unlock_resource(self):
|
|
"""
|
|
Internal function that does the unlocking of a given
|
|
lock file.
|
|
"""
|
|
lock_path = self.path()
|
|
with self._lock_mutex:
|
|
|
|
mapped = self._file_lock_setup(lock_path)
|
|
|
|
if mapped['count'] == 0:
|
|
raise RuntimeError("releasing a non-acquired lock")
|
|
|
|
# decrement lock counter
|
|
if mapped['count'] > 0:
|
|
mapped['count'] -= 1
|
|
# if lock counter > 0, still locked
|
|
# waiting for other upper-level calls
|
|
if mapped['count'] > 0:
|
|
return
|
|
|
|
ref_obj = mapped['ref']
|
|
if ref_obj is not None:
|
|
# do not remove!
|
|
ref_obj.release()
|
|
ref_obj.close()
|
|
mapped['ref'] = None
|
|
|
|
# allow the same thread to acquire the lock again.
|
|
self._TLS.recursed = False
|
|
|
|
def _file_lock_create(self, lock_path, blocking=False, shared=False):
|
|
"""
|
|
Create and allocate the lock file pointed by lock_data structure.
|
|
"""
|
|
lock_dir = os.path.dirname(lock_path)
|
|
try:
|
|
os.makedirs(lock_dir, 0o775)
|
|
except OSError as err:
|
|
if err.errno != errno.EEXIST:
|
|
raise
|
|
const_setup_directory(lock_dir)
|
|
|
|
try:
|
|
fmode = 0o664
|
|
if shared:
|
|
fd = os.open(lock_path, os.O_CREAT | os.O_RDONLY, fmode)
|
|
else:
|
|
fd = os.open(lock_path, os.O_CREAT | os.O_APPEND, fmode)
|
|
except OSError as err:
|
|
if err.errno in (errno.ENOENT, errno.EACCES):
|
|
# cannot get lock or dir doesn't exist
|
|
return False, None
|
|
raise
|
|
|
|
# ensure that entropy group can write on that
|
|
try:
|
|
const_setup_file(lock_path, etpConst['entropygid'], 0o664)
|
|
except OSError:
|
|
pass
|
|
|
|
flock_f = FlockFile(lock_path, fd=fd)
|
|
if blocking:
|
|
if shared:
|
|
flock_f.acquire_shared()
|
|
else:
|
|
flock_f.acquire_exclusive()
|
|
else:
|
|
acquired = False
|
|
if shared:
|
|
acquired = flock_f.try_acquire_shared()
|
|
else:
|
|
acquired = flock_f.try_acquire_exclusive()
|
|
if not acquired:
|
|
return False, None
|
|
|
|
return True, flock_f
|
|
|
|
def _wait_resource(self, shared):
|
|
"""
|
|
Try to acquire the resource, on failure, print a warning
|
|
and block until acquired.
|
|
"""
|
|
if shared:
|
|
acquired = self.try_acquire_shared()
|
|
else:
|
|
acquired = self.try_acquire_exclusive()
|
|
|
|
if acquired:
|
|
return True
|
|
|
|
if shared:
|
|
msg = "%s %s ..." % (
|
|
blue(_("Acquiring shared lock on")),
|
|
darkgreen(self.path()),)
|
|
else:
|
|
msg = "%s %s ..." % (
|
|
blue(_("Acquiring exclusive lock on")),
|
|
darkgreen(self.path()),)
|
|
|
|
self._out.output(
|
|
msg,
|
|
importance=0,
|
|
back=True,
|
|
level="warning"
|
|
)
|
|
|
|
if shared:
|
|
self.acquire_shared()
|
|
else:
|
|
self.acquire_exclusive()
|
|
return True
|
|
|
|
@contextlib.contextmanager
|
|
def exclusive(self):
|
|
"""
|
|
Acquire an exclusive file lock for this repository (context manager).
|
|
"""
|
|
self.wait_exclusive()
|
|
try:
|
|
yield
|
|
finally:
|
|
self.release()
|
|
|
|
def acquire_exclusive(self):
|
|
"""
|
|
Acquire the resource in exclusive blocking mode.
|
|
"""
|
|
self._lock_resource(True, False)
|
|
|
|
def try_acquire_exclusive(self):
|
|
"""
|
|
Acquire the resource in exclusive non-blocking mode.
|
|
Return True if resource is acquired, False otherwise.
|
|
"""
|
|
return self._lock_resource(False, False)
|
|
|
|
def wait_exclusive(self):
|
|
"""
|
|
Try to acquire the resource in non-blocking mode, on
|
|
failure, print a warning and then acquire the resource
|
|
in blocking mode.
|
|
"""
|
|
return self._wait_resource(False)
|
|
|
|
@contextlib.contextmanager
|
|
def shared(self):
|
|
"""
|
|
Acquire a shared file lock for this repository (context manager).
|
|
"""
|
|
self.wait_shared()
|
|
try:
|
|
yield
|
|
finally:
|
|
self.release()
|
|
|
|
def acquire_shared(self):
|
|
"""
|
|
Acquire the resource in shared blocking mode.
|
|
"""
|
|
self._lock_resource(True, True)
|
|
|
|
def try_acquire_shared(self):
|
|
"""
|
|
Acquire the resource in shared non-blocking mode.
|
|
Return True if resource is acquired, False otherwise.
|
|
"""
|
|
return self._lock_resource(False, True)
|
|
|
|
def wait_shared(self):
|
|
"""
|
|
Try to acquire the resource in non-blocking mode, on
|
|
failure, print a warning and then acquire the resource
|
|
in blocking mode.
|
|
"""
|
|
return self._wait_resource(True)
|
|
|
|
def release(self):
|
|
"""
|
|
Release the previously acquired resource.
|
|
"""
|
|
self._unlock_resource()
|
|
|
|
|
|
class EntropyResourcesLock(ResourceLock):
|
|
"""
|
|
Entropy Resources Lock (or Big Entropy Lock, BEL) class.
|
|
|
|
This class wraps the interface to the Entropy Resources Lock,
|
|
for acquiring shared or exclusive access to the Entropy Package
|
|
Manager.
|
|
|
|
Typically, routines acquire this lock in shared mode and
|
|
only a handful of code paths require exclusive locking.
|
|
"""
|
|
|
|
_FILE_LOCK_MUTEX = threading.Lock()
|
|
_FILE_LOCK_MAP = {}
|
|
|
|
# List of callables that will be triggered upon lock acquisition.
|
|
# It can be used to execute cache cleanups.
|
|
_POST_ACQUIRE_HOOK_LOCK = threading.Lock()
|
|
_POST_ACQUIRE_HOOKS = {}
|
|
_POST_ACQUIRE_HOOK_COUNT = 0
|
|
|
|
@classmethod
|
|
def add_post_acquire_hook(cls, callab):
|
|
"""
|
|
Add a hook that will be executed once the lock is acquired.
|
|
This can be used to execute cache cleanups or other activities
|
|
of the same sort. The callable is called with the EntropyCacher
|
|
lock held.
|
|
|
|
This method returns a reference number that must be used to perform
|
|
the removal of the hook.
|
|
"""
|
|
with EntropyResourcesLock._POST_ACQUIRE_HOOK_LOCK:
|
|
EntropyResourcesLock._POST_ACQUIRE_HOOK_COUNT += 1
|
|
index = EntropyResourcesLock._POST_ACQUIRE_HOOK_COUNT
|
|
EntropyResourcesLock._POST_ACQUIRE_HOOKS[index] = callab
|
|
|
|
return index
|
|
|
|
@classmethod
|
|
def remove_post_acquire_hook(cls, index):
|
|
"""
|
|
Remove a previously added hook using its reference.
|
|
|
|
This method raises KeyError if the hook is not present.
|
|
"""
|
|
with EntropyResourcesLock._POST_ACQUIRE_HOOK_LOCK:
|
|
EntropyResourcesLock._POST_ACQUIRE_HOOKS.pop(index)
|
|
|
|
def __init__(self, output=None):
|
|
"""
|
|
Object constructor.
|
|
|
|
@keyword output: a TextInterface interface
|
|
@type output: entropy.output.TextInterface or None
|
|
"""
|
|
super(EntropyResourcesLock, self).__init__(
|
|
EntropyResourcesLock._FILE_LOCK_MAP,
|
|
EntropyResourcesLock._FILE_LOCK_MUTEX,
|
|
output=output)
|
|
|
|
def path(self):
|
|
"""
|
|
Return the path to the lock file.
|
|
"""
|
|
return os.path.join(etpConst['entropyworkdir'],
|
|
'.using_resources')
|
|
|
|
def _clear_resources_after_lock(self):
|
|
"""
|
|
Clear resources that could have become stale after
|
|
the Entropy Lock acquisition.
|
|
"""
|
|
cacher = EntropyCacher()
|
|
with cacher:
|
|
|
|
SystemSettings().clear()
|
|
cacher.discard()
|
|
|
|
with EntropyResourcesLock._POST_ACQUIRE_HOOK_LOCK:
|
|
callables = list(
|
|
EntropyResourcesLock._POST_ACQUIRE_HOOKS.values()
|
|
)
|
|
|
|
for callab in callables:
|
|
callab()
|
|
|
|
cacher.sync()
|
|
|
|
def _lock_resource(self, blocking, shared):
|
|
"""
|
|
Overridden from ResourceLock.
|
|
Add hooks support code.
|
|
"""
|
|
acquired = super(EntropyResourcesLock, self)._lock_resource(
|
|
blocking, shared)
|
|
if acquired:
|
|
self._clear_resources_after_lock()
|
|
return acquired
|