Home | History | Annotate | Download | only in cros
      1 
      2 """
      3 lockfile.py - Platform-independent advisory file locks.
      4 
      5 Forked from python2.7/dist-packages/lockfile version 0.8.
      6 
      7 Usage:
      8 
      9 >>> lock = FileLock('somefile')
     10 >>> try:
     11 ...     lock.acquire()
     12 ... except AlreadyLocked:
     13 ...     print 'somefile', 'is locked already.'
     14 ... except LockFailed:
     15 ...     print 'somefile', 'can\\'t be locked.'
     16 ... else:
     17 ...     print 'got lock'
     18 got lock
     19 >>> print lock.is_locked()
     20 True
     21 >>> lock.release()
     22 
     23 >>> lock = FileLock('somefile')
     24 >>> print lock.is_locked()
     25 False
     26 >>> with lock:
     27 ...    print lock.is_locked()
     28 True
     29 >>> print lock.is_locked()
     30 False
     31 >>> # It is okay to lock twice from the same thread...
     32 >>> with lock:
     33 ...     lock.acquire()
     34 ...
     35 >>> # Though no counter is kept, so you can't unlock multiple times...
     36 >>> print lock.is_locked()
     37 False
     38 
     39 Exceptions:
     40 
     41     Error - base class for other exceptions
     42         LockError - base class for all locking exceptions
     43             AlreadyLocked - Another thread or process already holds the lock
     44             LockFailed - Lock failed for some other reason
     45         UnlockError - base class for all unlocking exceptions
     46             AlreadyUnlocked - File was not locked.
     47             NotMyLock - File was locked but not by the current thread/process
     48 """
     49 
     50 from __future__ import division
     51 
     52 import logging
     53 import socket
     54 import os
     55 import threading
     56 import time
     57 import urllib
     58 
     59 # Work with PEP8 and non-PEP8 versions of threading module.
     60 if not hasattr(threading, "current_thread"):
     61     threading.current_thread = threading.currentThread
     62 if not hasattr(threading.Thread, "get_name"):
     63     threading.Thread.get_name = threading.Thread.getName
     64 
     65 __all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked',
     66            'LockFailed', 'UnlockError', 'LinkFileLock']
     67 
     68 class Error(Exception):
     69     """
     70     Base class for other exceptions.
     71 
     72     >>> try:
     73     ...   raise Error
     74     ... except Exception:
     75     ...   pass
     76     """
     77     pass
     78 
     79 class LockError(Error):
     80     """
     81     Base class for error arising from attempts to acquire the lock.
     82 
     83     >>> try:
     84     ...   raise LockError
     85     ... except Error:
     86     ...   pass
     87     """
     88     pass
     89 
     90 class LockTimeout(LockError):
     91     """Raised when lock creation fails within a user-defined period of time.
     92 
     93     >>> try:
     94     ...   raise LockTimeout
     95     ... except LockError:
     96     ...   pass
     97     """
     98     pass
     99 
    100 class AlreadyLocked(LockError):
    101     """Some other thread/process is locking the file.
    102 
    103     >>> try:
    104     ...   raise AlreadyLocked
    105     ... except LockError:
    106     ...   pass
    107     """
    108     pass
    109 
    110 class LockFailed(LockError):
    111     """Lock file creation failed for some other reason.
    112 
    113     >>> try:
    114     ...   raise LockFailed
    115     ... except LockError:
    116     ...   pass
    117     """
    118     pass
    119 
    120 class UnlockError(Error):
    121     """
    122     Base class for errors arising from attempts to release the lock.
    123 
    124     >>> try:
    125     ...   raise UnlockError
    126     ... except Error:
    127     ...   pass
    128     """
    129     pass
    130 
    131 class LockBase(object):
    132     """Base class for platform-specific lock classes."""
    133     def __init__(self, path):
    134         """
    135         Unlike the original implementation we always assume the threaded case.
    136         """
    137         self.path = path
    138         self.lock_file = os.path.abspath(path) + ".lock"
    139         self.hostname = socket.gethostname()
    140         self.pid = os.getpid()
    141         name = threading.current_thread().get_name()
    142         tname = "%s-" % urllib.quote(name, safe="")
    143         dirname = os.path.dirname(self.lock_file)
    144         self.unique_name = os.path.join(dirname, "%s.%s%s" % (self.hostname,
    145                                                               tname, self.pid))
    146 
    147     def __del__(self):
    148         """Paranoia: We are trying hard to not leave any file behind. This
    149         might possibly happen in very unusual acquire exception cases."""
    150         if os.path.exists(self.unique_name):
    151             logging.warning("Removing unexpected file %s", self.unique_name)
    152             os.unlink(self.unique_name)
    153 
    154     def acquire(self, timeout=None):
    155         """
    156         Acquire the lock.
    157 
    158         * If timeout is omitted (or None), wait forever trying to lock the
    159           file.
    160 
    161         * If timeout > 0, try to acquire the lock for that many seconds.  If
    162           the lock period expires and the file is still locked, raise
    163           LockTimeout.
    164 
    165         * If timeout <= 0, raise AlreadyLocked immediately if the file is
    166           already locked.
    167         """
    168         raise NotImplementedError("implement in subclass")
    169 
    170     def release(self):
    171         """
    172         Release the lock.
    173 
    174         If the file is not locked, raise NotLocked.
    175         """
    176         raise NotImplementedError("implement in subclass")
    177 
    178     def is_locked(self):
    179         """
    180         Tell whether or not the file is locked.
    181         """
    182         raise NotImplementedError("implement in subclass")
    183 
    184     def i_am_locking(self):
    185         """
    186         Return True if this object is locking the file.
    187         """
    188         raise NotImplementedError("implement in subclass")
    189 
    190     def break_lock(self):
    191         """
    192         Remove a lock.  Useful if a locking thread failed to unlock.
    193         """
    194         raise NotImplementedError("implement in subclass")
    195 
    196     def age_of_lock(self):
    197         """
    198         Return the time since creation of lock in seconds.
    199         """
    200         raise NotImplementedError("implement in subclass")
    201 
    202     def __enter__(self):
    203         """
    204         Context manager support.
    205         """
    206         self.acquire()
    207         return self
    208 
    209     def __exit__(self, *_exc):
    210         """
    211         Context manager support.
    212         """
    213         self.release()
    214 
    215 
    216 class LinkFileLock(LockBase):
    217     """Lock access to a file using atomic property of link(2)."""
    218 
    219     def acquire(self, timeout=None):
    220         try:
    221             open(self.unique_name, "wb").close()
    222         except IOError:
    223             raise LockFailed("failed to create %s" % self.unique_name)
    224 
    225         end_time = time.time()
    226         if timeout is not None and timeout > 0:
    227             end_time += timeout
    228 
    229         while True:
    230             # Try and create a hard link to it.
    231             try:
    232                 os.link(self.unique_name, self.lock_file)
    233             except OSError:
    234                 # Link creation failed.  Maybe we've double-locked?
    235                 nlinks = os.stat(self.unique_name).st_nlink
    236                 if nlinks == 2:
    237                     # The original link plus the one I created == 2.  We're
    238                     # good to go.
    239                     return
    240                 else:
    241                     # Otherwise the lock creation failed.
    242                     if timeout is not None and time.time() > end_time:
    243                         os.unlink(self.unique_name)
    244                         if timeout > 0:
    245                             raise LockTimeout
    246                         else:
    247                             raise AlreadyLocked
    248                     # IHF: The original code used integer division/10.
    249                     time.sleep(timeout is not None and timeout / 10.0 or 0.1)
    250             else:
    251                 # Link creation succeeded.  We're good to go.
    252                 return
    253 
    254     def release(self):
    255         # IHF: I think original cleanup was not correct when somebody else broke
    256         # our lock and took it. Then we released the new process' lock causing
    257         # a cascade of wrong lock releases. Notice the SQLiteFileLock::release()
    258         # doesn't seem to run into this problem as it uses i_am_locking().
    259         if self.i_am_locking():
    260             # We own the lock and clean up both files.
    261             os.unlink(self.unique_name)
    262             os.unlink(self.lock_file)
    263             return
    264         if os.path.exists(self.unique_name):
    265             # We don't own lock_file but clean up after ourselves.
    266             os.unlink(self.unique_name)
    267         raise UnlockError
    268 
    269     def is_locked(self):
    270         """Check if anybody is holding the lock."""
    271         return os.path.exists(self.lock_file)
    272 
    273     def i_am_locking(self):
    274         """Check if we are holding the lock."""
    275         return (self.is_locked() and
    276                 os.path.exists(self.unique_name) and
    277                 os.stat(self.unique_name).st_nlink == 2)
    278 
    279     def break_lock(self):
    280         """Break (another processes) lock."""
    281         if os.path.exists(self.lock_file):
    282             os.unlink(self.lock_file)
    283 
    284     def age_of_lock(self):
    285         """Returns the time since creation of lock in seconds."""
    286         try:
    287             # Creating the hard link for the lock updates the change time.
    288             age = time.time() - os.stat(self.lock_file).st_ctime
    289         except OSError:
    290             age = -1.0
    291         return age
    292 
    293 
    294 FileLock = LinkFileLock
    295