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