import collections import errno import logging import os import struct import sys import time import library.python.windows logger = logging.getLogger(__name__) # python2 compat os_O_CLOEXEC = getattr(os, 'O_CLOEXEC', 1 << 19) class AbstractFileLock(object): def __init__(self, path): self.path = path def acquire(self, blocking=True): raise NotImplementedError def release(self): raise NotImplementedError def __enter__(self): self.acquire() return self def __exit__(self, type, value, traceback): self.release() class _NixFileLock(AbstractFileLock): def __init__(self, path): super(_NixFileLock, self).__init__(path) from fcntl import flock, LOCK_EX, LOCK_UN, LOCK_NB self._locker = lambda lock, blocking: flock(lock, LOCK_EX if blocking else LOCK_EX | LOCK_NB) self._unlocker = lambda lock: flock(lock, LOCK_UN) # nonbuffered random access rw mode self._lock = os.fdopen(os.open(self.path, os.O_RDWR | os.O_CREAT | os_O_CLOEXEC), 'r+b', 0) def acquire(self, blocking=True): import errno try: self._locker(self._lock, blocking) except IOError as e: if e.errno in (errno.EAGAIN, errno.EACCES) and not blocking: return False raise return True def release(self): self._unlocker(self._lock) def __del__(self): if hasattr(self, "_lock"): self._lock.close() class _WinFileLock(AbstractFileLock): """ Based on LockFile / UnlockFile from win32 API https://msdn.microsoft.com/en-us/library/windows/desktop/aa365202(v=vs.85).aspx """ _LOCKED_BYTES_NUM = 1 def __init__(self, path): super(_WinFileLock, self).__init__(path) # nonbuffered random access rw mode self._lock = os.fdopen(os.open(self.path, os.O_RDWR | os.O_CREAT | os.O_BINARY | os.O_NOINHERIT), 'r+b', 0) try: self._lock.write(b' ' * self._LOCKED_BYTES_NUM) except IOError as e: if e.errno != errno.EACCES or not os.path.isfile(path): raise def acquire(self, blocking=True): locked = False while not locked: locked = library.python.windows.lock_file(self._lock, 0, self._LOCKED_BYTES_NUM, raises=False) if locked: return True if blocking: time.sleep(0.5) else: return False def release(self): if self._lock: library.python.windows.unlock_file(self._lock, 0, self._LOCKED_BYTES_NUM, raises=False) def __del__(self): if getattr(self, '_lock', False): self._lock.close() class FileLock(AbstractFileLock): def __init__(self, path): super(FileLock, self).__init__(path) if sys.platform.startswith('win'): self._lock = _WinFileLock(path) else: self._lock = _NixFileLock(path) def acquire(self, blocking=True): logger.debug('Acquiring %s (blocking=%s): %s', type(self).__name__, blocking, self.path) return self._lock.acquire(blocking) def release(self): logger.debug('Ensuring %s released: %s', type(self).__name__, self.path) return self._lock.release() _LockInfo = collections.namedtuple('LockInfo', ['pid', 'time']) class _PidLockMixin(object): _LockedBytes = 0 _InfoFormat = 'QQ' _InfoFmtSize = struct.calcsize(_InfoFormat) def _register_lock(self): self._lock.seek(self._LockedBytes, os.SEEK_SET) self._lock.write(struct.pack(self._InfoFormat, os.getpid(), int(time.time()))) @property def info(self): self._lock.seek(self._LockedBytes, os.SEEK_SET) try: data = struct.unpack(self._InfoFormat, self._lock.read(self._InfoFmtSize)) except struct.error: data = 0, 0 return _LockInfo(*data) class _NixPidFileLock(_NixFileLock, _PidLockMixin): def acquire(self, blocking=True): if super(_NixPidFileLock, self).acquire(blocking): self._register_lock() return True return False class _WinPidFileLock(_WinFileLock, _PidLockMixin): _LockedBytes = _WinFileLock._LOCKED_BYTES_NUM def acquire(self, blocking=True): if super(_WinPidFileLock, self).acquire(blocking): self._register_lock() return True return False class PidFileLock(FileLock): def __init__(self, path): AbstractFileLock.__init__(self, path) if sys.platform.startswith('win'): self._lock = _WinPidFileLock(path) else: self._lock = _NixPidFileLock(path) @property def info(self): return self._lock.info