• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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