| |
| """ |
| lockfile.py - Platform-independent advisory file locks. |
| |
| Forked from python2.7/dist-packages/lockfile version 0.8. |
| |
| Usage: |
| |
| >>> lock = FileLock('somefile') |
| >>> try: |
| ... lock.acquire() |
| ... except AlreadyLocked: |
| ... print 'somefile', 'is locked already.' |
| ... except LockFailed: |
| ... print 'somefile', 'can\\'t be locked.' |
| ... else: |
| ... print 'got lock' |
| got lock |
| >>> print lock.is_locked() |
| True |
| >>> lock.release() |
| |
| >>> lock = FileLock('somefile') |
| >>> print lock.is_locked() |
| False |
| >>> with lock: |
| ... print lock.is_locked() |
| True |
| >>> print lock.is_locked() |
| False |
| >>> # It is okay to lock twice from the same thread... |
| >>> with lock: |
| ... lock.acquire() |
| ... |
| >>> # Though no counter is kept, so you can't unlock multiple times... |
| >>> print lock.is_locked() |
| False |
| |
| Exceptions: |
| |
| Error - base class for other exceptions |
| LockError - base class for all locking exceptions |
| AlreadyLocked - Another thread or process already holds the lock |
| LockFailed - Lock failed for some other reason |
| UnlockError - base class for all unlocking exceptions |
| AlreadyUnlocked - File was not locked. |
| NotMyLock - File was locked but not by the current thread/process |
| """ |
| |
| from __future__ import division |
| |
| import logging |
| import socket |
| import os |
| import threading |
| import time |
| import urllib |
| |
| # Work with PEP8 and non-PEP8 versions of threading module. |
| if not hasattr(threading, "current_thread"): |
| threading.current_thread = threading.currentThread |
| if not hasattr(threading.Thread, "get_name"): |
| threading.Thread.get_name = threading.Thread.getName |
| |
| __all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked', |
| 'LockFailed', 'UnlockError', 'LinkFileLock'] |
| |
| class Error(Exception): |
| """ |
| Base class for other exceptions. |
| |
| >>> try: |
| ... raise Error |
| ... except Exception: |
| ... pass |
| """ |
| pass |
| |
| class LockError(Error): |
| """ |
| Base class for error arising from attempts to acquire the lock. |
| |
| >>> try: |
| ... raise LockError |
| ... except Error: |
| ... pass |
| """ |
| pass |
| |
| class LockTimeout(LockError): |
| """Raised when lock creation fails within a user-defined period of time. |
| |
| >>> try: |
| ... raise LockTimeout |
| ... except LockError: |
| ... pass |
| """ |
| pass |
| |
| class AlreadyLocked(LockError): |
| """Some other thread/process is locking the file. |
| |
| >>> try: |
| ... raise AlreadyLocked |
| ... except LockError: |
| ... pass |
| """ |
| pass |
| |
| class LockFailed(LockError): |
| """Lock file creation failed for some other reason. |
| |
| >>> try: |
| ... raise LockFailed |
| ... except LockError: |
| ... pass |
| """ |
| pass |
| |
| class UnlockError(Error): |
| """ |
| Base class for errors arising from attempts to release the lock. |
| |
| >>> try: |
| ... raise UnlockError |
| ... except Error: |
| ... pass |
| """ |
| pass |
| |
| class LockBase(object): |
| """Base class for platform-specific lock classes.""" |
| def __init__(self, path): |
| """ |
| Unlike the original implementation we always assume the threaded case. |
| """ |
| self.path = path |
| self.lock_file = os.path.abspath(path) + ".lock" |
| self.hostname = socket.gethostname() |
| self.pid = os.getpid() |
| name = threading.current_thread().get_name() |
| tname = "%s-" % urllib.quote(name, safe="") |
| dirname = os.path.dirname(self.lock_file) |
| self.unique_name = os.path.join(dirname, "%s.%s%s" % (self.hostname, |
| tname, self.pid)) |
| |
| def __del__(self): |
| """Paranoia: We are trying hard to not leave any file behind. This |
| might possibly happen in very unusual acquire exception cases.""" |
| if os.path.exists(self.unique_name): |
| logging.warning("Removing unexpected file %s", self.unique_name) |
| os.unlink(self.unique_name) |
| |
| def acquire(self, timeout=None): |
| """ |
| Acquire the lock. |
| |
| * If timeout is omitted (or None), wait forever trying to lock the |
| file. |
| |
| * If timeout > 0, try to acquire the lock for that many seconds. If |
| the lock period expires and the file is still locked, raise |
| LockTimeout. |
| |
| * If timeout <= 0, raise AlreadyLocked immediately if the file is |
| already locked. |
| """ |
| raise NotImplementedError("implement in subclass") |
| |
| def release(self): |
| """ |
| Release the lock. |
| |
| If the file is not locked, raise NotLocked. |
| """ |
| raise NotImplementedError("implement in subclass") |
| |
| def is_locked(self): |
| """ |
| Tell whether or not the file is locked. |
| """ |
| raise NotImplementedError("implement in subclass") |
| |
| def i_am_locking(self): |
| """ |
| Return True if this object is locking the file. |
| """ |
| raise NotImplementedError("implement in subclass") |
| |
| def break_lock(self): |
| """ |
| Remove a lock. Useful if a locking thread failed to unlock. |
| """ |
| raise NotImplementedError("implement in subclass") |
| |
| def age_of_lock(self): |
| """ |
| Return the time since creation of lock in seconds. |
| """ |
| raise NotImplementedError("implement in subclass") |
| |
| def __enter__(self): |
| """ |
| Context manager support. |
| """ |
| self.acquire() |
| return self |
| |
| def __exit__(self, *_exc): |
| """ |
| Context manager support. |
| """ |
| self.release() |
| |
| |
| class LinkFileLock(LockBase): |
| """Lock access to a file using atomic property of link(2).""" |
| |
| def acquire(self, timeout=None): |
| try: |
| open(self.unique_name, "wb").close() |
| except IOError: |
| raise LockFailed("failed to create %s" % self.unique_name) |
| |
| end_time = time.time() |
| if timeout is not None and timeout > 0: |
| end_time += timeout |
| |
| while True: |
| # Try and create a hard link to it. |
| try: |
| os.link(self.unique_name, self.lock_file) |
| except OSError: |
| # Link creation failed. Maybe we've double-locked? |
| nlinks = os.stat(self.unique_name).st_nlink |
| if nlinks == 2: |
| # The original link plus the one I created == 2. We're |
| # good to go. |
| return |
| else: |
| # Otherwise the lock creation failed. |
| if timeout is not None and time.time() > end_time: |
| os.unlink(self.unique_name) |
| if timeout > 0: |
| raise LockTimeout |
| else: |
| raise AlreadyLocked |
| # IHF: The original code used integer division/10. |
| time.sleep(timeout is not None and timeout / 10.0 or 0.1) |
| else: |
| # Link creation succeeded. We're good to go. |
| return |
| |
| def release(self): |
| # IHF: I think original cleanup was not correct when somebody else broke |
| # our lock and took it. Then we released the new process' lock causing |
| # a cascade of wrong lock releases. Notice the SQLiteFileLock::release() |
| # doesn't seem to run into this problem as it uses i_am_locking(). |
| if self.i_am_locking(): |
| # We own the lock and clean up both files. |
| os.unlink(self.unique_name) |
| os.unlink(self.lock_file) |
| return |
| if os.path.exists(self.unique_name): |
| # We don't own lock_file but clean up after ourselves. |
| os.unlink(self.unique_name) |
| raise UnlockError |
| |
| def is_locked(self): |
| """Check if anybody is holding the lock.""" |
| return os.path.exists(self.lock_file) |
| |
| def i_am_locking(self): |
| """Check if we are holding the lock.""" |
| return (self.is_locked() and |
| os.path.exists(self.unique_name) and |
| os.stat(self.unique_name).st_nlink == 2) |
| |
| def break_lock(self): |
| """Break (another processes) lock.""" |
| if os.path.exists(self.lock_file): |
| os.unlink(self.lock_file) |
| |
| def age_of_lock(self): |
| """Returns the time since creation of lock in seconds.""" |
| try: |
| # Creating the hard link for the lock updates the change time. |
| age = time.time() - os.stat(self.lock_file).st_ctime |
| except OSError: |
| age = -1.0 |
| return age |
| |
| |
| FileLock = LinkFileLock |