Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 1 | |
| 2 | """ |
| 3 | lockfile.py - Platform-independent advisory file locks. |
| 4 | |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 5 | Forked from python2.7/dist-packages/lockfile version 0.8. |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 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 | |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 52 | import logging |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 53 | import socket |
| 54 | import os |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 55 | import threading |
| 56 | import time |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 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', |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 66 | 'LockFailed', 'UnlockError', 'LinkFileLock'] |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 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 | |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 131 | class LockBase(object): |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 132 | """Base class for platform-specific lock classes.""" |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 133 | def __init__(self, path): |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 134 | """ |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 135 | Unlike the original implementation we always assume the threaded case. |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 136 | """ |
| 137 | self.path = path |
| 138 | self.lock_file = os.path.abspath(path) + ".lock" |
| 139 | self.hostname = socket.gethostname() |
| 140 | self.pid = os.getpid() |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 141 | name = threading.current_thread().get_name() |
| 142 | tname = "%s-" % urllib.quote(name, safe="") |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 143 | dirname = os.path.dirname(self.lock_file) |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 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) |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 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 | """ |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 168 | raise NotImplementedError("implement in subclass") |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 169 | |
| 170 | def release(self): |
| 171 | """ |
| 172 | Release the lock. |
| 173 | |
| 174 | If the file is not locked, raise NotLocked. |
| 175 | """ |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 176 | raise NotImplementedError("implement in subclass") |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 177 | |
| 178 | def is_locked(self): |
| 179 | """ |
| 180 | Tell whether or not the file is locked. |
| 181 | """ |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 182 | raise NotImplementedError("implement in subclass") |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 183 | |
| 184 | def i_am_locking(self): |
| 185 | """ |
| 186 | Return True if this object is locking the file. |
| 187 | """ |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 188 | raise NotImplementedError("implement in subclass") |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 189 | |
| 190 | def break_lock(self): |
| 191 | """ |
| 192 | Remove a lock. Useful if a locking thread failed to unlock. |
| 193 | """ |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 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") |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 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 | |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 215 | |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 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 |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 248 | # IHF: The original code used integer division/10. |
| 249 | time.sleep(timeout is not None and timeout / 10.0 or 0.1) |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 250 | else: |
| 251 | # Link creation succeeded. We're good to go. |
| 252 | return |
| 253 | |
| 254 | def release(self): |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 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 |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 268 | |
| 269 | def is_locked(self): |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 270 | """Check if anybody is holding the lock.""" |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 271 | return os.path.exists(self.lock_file) |
| 272 | |
| 273 | def i_am_locking(self): |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 274 | """Check if we are holding the lock.""" |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 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): |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 280 | """Break (another processes) lock.""" |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 281 | if os.path.exists(self.lock_file): |
| 282 | os.unlink(self.lock_file) |
| 283 | |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 284 | def age_of_lock(self): |
| 285 | """Returns the time since creation of lock in seconds.""" |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 286 | try: |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 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 |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 292 | |
Ilja H. Friedel | 711fcdb | 2018-01-22 22:08:00 -0800 | [diff] [blame] | 293 | |
Ilja H. Friedel | 397eea2 | 2018-01-22 22:13:32 -0800 | [diff] [blame^] | 294 | FileLock = LinkFileLock |