blob: 52063a14a2fdbae1788edaad0ea760cf1f8f95a0 [file] [log] [blame]
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -08001
2"""
3lockfile.py - Platform-independent advisory file locks.
4
Ilja H. Friedel397eea22018-01-22 22:13:32 -08005Forked from python2.7/dist-packages/lockfile version 0.8.
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -08006
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
Ilja H. Friedel397eea22018-01-22 22:13:32 -080052import logging
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -080053import socket
54import os
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -080055import threading
56import time
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -080057import 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',
Ilja H. Friedel397eea22018-01-22 22:13:32 -080066 'LockFailed', 'UnlockError', 'LinkFileLock']
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -080067
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
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800131class LockBase(object):
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800132 """Base class for platform-specific lock classes."""
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800133 def __init__(self, path):
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800134 """
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800135 Unlike the original implementation we always assume the threaded case.
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800136 """
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. Friedel397eea22018-01-22 22:13:32 -0800141 name = threading.current_thread().get_name()
142 tname = "%s-" % urllib.quote(name, safe="")
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800143 dirname = os.path.dirname(self.lock_file)
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800144 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. Friedel711fcdb2018-01-22 22:08:00 -0800153
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. Friedel397eea22018-01-22 22:13:32 -0800168 raise NotImplementedError("implement in subclass")
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800169
170 def release(self):
171 """
172 Release the lock.
173
174 If the file is not locked, raise NotLocked.
175 """
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800176 raise NotImplementedError("implement in subclass")
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800177
178 def is_locked(self):
179 """
180 Tell whether or not the file is locked.
181 """
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800182 raise NotImplementedError("implement in subclass")
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800183
184 def i_am_locking(self):
185 """
186 Return True if this object is locking the file.
187 """
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800188 raise NotImplementedError("implement in subclass")
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800189
190 def break_lock(self):
191 """
192 Remove a lock. Useful if a locking thread failed to unlock.
193 """
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800194 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. Friedel711fcdb2018-01-22 22:08:00 -0800201
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. Friedel397eea22018-01-22 22:13:32 -0800215
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800216class 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. Friedel397eea22018-01-22 22:13:32 -0800248 # IHF: The original code used integer division/10.
249 time.sleep(timeout is not None and timeout / 10.0 or 0.1)
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800250 else:
251 # Link creation succeeded. We're good to go.
252 return
253
254 def release(self):
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800255 # 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. Friedel711fcdb2018-01-22 22:08:00 -0800268
269 def is_locked(self):
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800270 """Check if anybody is holding the lock."""
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800271 return os.path.exists(self.lock_file)
272
273 def i_am_locking(self):
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800274 """Check if we are holding the lock."""
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800275 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. Friedel397eea22018-01-22 22:13:32 -0800280 """Break (another processes) lock."""
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800281 if os.path.exists(self.lock_file):
282 os.unlink(self.lock_file)
283
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800284 def age_of_lock(self):
285 """Returns the time since creation of lock in seconds."""
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800286 try:
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800287 # 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. Friedel711fcdb2018-01-22 22:08:00 -0800292
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800293
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800294FileLock = LinkFileLock