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