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