1 2""" 3lockfile.py - Platform-independent advisory file locks. 4 5Forked from python2.7/dist-packages/lockfile version 0.8. 6 7Usage: 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' 18got lock 19>>> print lock.is_locked() 20True 21>>> lock.release() 22 23>>> lock = FileLock('somefile') 24>>> print lock.is_locked() 25False 26>>> with lock: 27... print lock.is_locked() 28True 29>>> print lock.is_locked() 30False 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() 37False 38 39Exceptions: 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 50from __future__ import division 51 52import logging 53import socket 54import os 55import threading 56import time 57import urllib 58 59# Work with PEP8 and non-PEP8 versions of threading module. 60if not hasattr(threading, "current_thread"): 61 threading.current_thread = threading.currentThread 62if 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 68class Error(Exception): 69 """ 70 Base class for other exceptions. 71 72 >>> try: 73 ... raise Error 74 ... except Exception: 75 ... pass 76 """ 77 pass 78 79class 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 90class 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 100class 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 110class 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 120class 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 131class 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 216class 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 294FileLock = LinkFileLock 295