blob: 44476fd00c7de9ac4671c98574006a2789ee0211 [file] [log] [blame]
Derek Beckett5fb683c2020-08-19 15:24:13 -07001# Lint as: python2, python3
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -08002
3"""
4lockfile.py - Platform-independent advisory file locks.
5
Ilja H. Friedel397eea22018-01-22 22:13:32 -08006Forked from python2.7/dist-packages/lockfile version 0.8.
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -08007
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
Derek Beckett5fb683c2020-08-19 15:24:13 -070051from __future__ import absolute_import
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -080052from __future__ import division
Derek Beckett5fb683c2020-08-19 15:24:13 -070053from __future__ import print_function
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -080054
Ilja H. Friedel397eea22018-01-22 22:13:32 -080055import logging
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -080056import socket
57import os
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -080058import threading
59import time
Derek Beckett5fb683c2020-08-19 15:24:13 -070060import six
61from six.moves import urllib
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -080062
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',
Ilja H. Friedel397eea22018-01-22 22:13:32 -080070 'LockFailed', 'UnlockError', 'LinkFileLock']
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -080071
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
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800135class LockBase(object):
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800136 """Base class for platform-specific lock classes."""
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800137 def __init__(self, path):
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800138 """
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800139 Unlike the original implementation we always assume the threaded case.
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800140 """
141 self.path = path
142 self.lock_file = os.path.abspath(path) + ".lock"
143 self.hostname = socket.gethostname()
144 self.pid = os.getpid()
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800145 name = threading.current_thread().get_name()
Derek Beckett5fb683c2020-08-19 15:24:13 -0700146 tname = "%s-" % urllib.parse.quote(name, safe="")
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800147 dirname = os.path.dirname(self.lock_file)
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800148 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)
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800157
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 """
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800172 raise NotImplementedError("implement in subclass")
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800173
174 def release(self):
175 """
176 Release the lock.
177
178 If the file is not locked, raise NotLocked.
179 """
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800180 raise NotImplementedError("implement in subclass")
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800181
182 def is_locked(self):
183 """
184 Tell whether or not the file is locked.
185 """
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800186 raise NotImplementedError("implement in subclass")
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800187
188 def i_am_locking(self):
189 """
190 Return True if this object is locking the file.
191 """
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800192 raise NotImplementedError("implement in subclass")
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800193
194 def break_lock(self):
195 """
196 Remove a lock. Useful if a locking thread failed to unlock.
197 """
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800198 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")
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800205
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
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800219
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800220class 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
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800252 # IHF: The original code used integer division/10.
253 time.sleep(timeout is not None and timeout / 10.0 or 0.1)
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800254 else:
255 # Link creation succeeded. We're good to go.
256 return
257
258 def release(self):
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800259 # 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
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800272
273 def is_locked(self):
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800274 """Check if anybody is holding the lock."""
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800275 return os.path.exists(self.lock_file)
276
277 def i_am_locking(self):
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800278 """Check if we are holding the lock."""
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800279 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):
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800284 """Break (another processes) lock."""
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800285 if os.path.exists(self.lock_file):
286 os.unlink(self.lock_file)
287
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800288 def age_of_lock(self):
289 """Returns the time since creation of lock in seconds."""
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800290 try:
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800291 # 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
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800296
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -0800297
Ilja H. Friedel397eea22018-01-22 22:13:32 -0800298FileLock = LinkFileLock