You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
8.5 KiB

# Lint as: python2, python3
"""
lockfile.py - Platform-independent advisory file locks.
Forked from python2.7/dist-packages/lockfile version 0.8.
Usage:
>>> lock = FileLock('somefile')
>>> try:
... lock.acquire()
... except AlreadyLocked:
... print 'somefile', 'is locked already.'
... except LockFailed:
... print 'somefile', 'can\\'t be locked.'
... else:
... print 'got lock'
got lock
>>> print lock.is_locked()
True
>>> lock.release()
>>> lock = FileLock('somefile')
>>> print lock.is_locked()
False
>>> with lock:
... print lock.is_locked()
True
>>> print lock.is_locked()
False
>>> # It is okay to lock twice from the same thread...
>>> with lock:
... lock.acquire()
...
>>> # Though no counter is kept, so you can't unlock multiple times...
>>> print lock.is_locked()
False
Exceptions:
Error - base class for other exceptions
LockError - base class for all locking exceptions
AlreadyLocked - Another thread or process already holds the lock
LockFailed - Lock failed for some other reason
UnlockError - base class for all unlocking exceptions
AlreadyUnlocked - File was not locked.
NotMyLock - File was locked but not by the current thread/process
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import logging
import socket
import os
import threading
import time
import six
from six.moves import urllib
# Work with PEP8 and non-PEP8 versions of threading module.
if not hasattr(threading, "current_thread"):
threading.current_thread = threading.currentThread
if not hasattr(threading.Thread, "get_name"):
threading.Thread.get_name = threading.Thread.getName
__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked',
'LockFailed', 'UnlockError', 'LinkFileLock']
class Error(Exception):
"""
Base class for other exceptions.
>>> try:
... raise Error
... except Exception:
... pass
"""
pass
class LockError(Error):
"""
Base class for error arising from attempts to acquire the lock.
>>> try:
... raise LockError
... except Error:
... pass
"""
pass
class LockTimeout(LockError):
"""Raised when lock creation fails within a user-defined period of time.
>>> try:
... raise LockTimeout
... except LockError:
... pass
"""
pass
class AlreadyLocked(LockError):
"""Some other thread/process is locking the file.
>>> try:
... raise AlreadyLocked
... except LockError:
... pass
"""
pass
class LockFailed(LockError):
"""Lock file creation failed for some other reason.
>>> try:
... raise LockFailed
... except LockError:
... pass
"""
pass
class UnlockError(Error):
"""
Base class for errors arising from attempts to release the lock.
>>> try:
... raise UnlockError
... except Error:
... pass
"""
pass
class LockBase(object):
"""Base class for platform-specific lock classes."""
def __init__(self, path):
"""
Unlike the original implementation we always assume the threaded case.
"""
self.path = path
self.lock_file = os.path.abspath(path) + ".lock"
self.hostname = socket.gethostname()
self.pid = os.getpid()
name = threading.current_thread().get_name()
tname = "%s-" % urllib.parse.quote(name, safe="")
dirname = os.path.dirname(self.lock_file)
self.unique_name = os.path.join(dirname, "%s.%s%s" % (self.hostname,
tname, self.pid))
def __del__(self):
"""Paranoia: We are trying hard to not leave any file behind. This
might possibly happen in very unusual acquire exception cases."""
if os.path.exists(self.unique_name):
logging.warning("Removing unexpected file %s", self.unique_name)
os.unlink(self.unique_name)
def acquire(self, timeout=None):
"""
Acquire the lock.
* If timeout is omitted (or None), wait forever trying to lock the
file.
* If timeout > 0, try to acquire the lock for that many seconds. If
the lock period expires and the file is still locked, raise
LockTimeout.
* If timeout <= 0, raise AlreadyLocked immediately if the file is
already locked.
"""
raise NotImplementedError("implement in subclass")
def release(self):
"""
Release the lock.
If the file is not locked, raise NotLocked.
"""
raise NotImplementedError("implement in subclass")
def is_locked(self):
"""
Tell whether or not the file is locked.
"""
raise NotImplementedError("implement in subclass")
def i_am_locking(self):
"""
Return True if this object is locking the file.
"""
raise NotImplementedError("implement in subclass")
def break_lock(self):
"""
Remove a lock. Useful if a locking thread failed to unlock.
"""
raise NotImplementedError("implement in subclass")
def age_of_lock(self):
"""
Return the time since creation of lock in seconds.
"""
raise NotImplementedError("implement in subclass")
def __enter__(self):
"""
Context manager support.
"""
self.acquire()
return self
def __exit__(self, *_exc):
"""
Context manager support.
"""
self.release()
class LinkFileLock(LockBase):
"""Lock access to a file using atomic property of link(2)."""
def acquire(self, timeout=None):
try:
open(self.unique_name, "wb").close()
except IOError:
raise LockFailed("failed to create %s" % self.unique_name)
end_time = time.time()
if timeout is not None and timeout > 0:
end_time += timeout
while True:
# Try and create a hard link to it.
try:
os.link(self.unique_name, self.lock_file)
except OSError:
# Link creation failed. Maybe we've double-locked?
nlinks = os.stat(self.unique_name).st_nlink
if nlinks == 2:
# The original link plus the one I created == 2. We're
# good to go.
return
else:
# Otherwise the lock creation failed.
if timeout is not None and time.time() > end_time:
os.unlink(self.unique_name)
if timeout > 0:
raise LockTimeout
else:
raise AlreadyLocked
# IHF: The original code used integer division/10.
time.sleep(timeout is not None and timeout / 10.0 or 0.1)
else:
# Link creation succeeded. We're good to go.
return
def release(self):
# IHF: I think original cleanup was not correct when somebody else broke
# our lock and took it. Then we released the new process' lock causing
# a cascade of wrong lock releases. Notice the SQLiteFileLock::release()
# doesn't seem to run into this problem as it uses i_am_locking().
if self.i_am_locking():
# We own the lock and clean up both files.
os.unlink(self.unique_name)
os.unlink(self.lock_file)
return
if os.path.exists(self.unique_name):
# We don't own lock_file but clean up after ourselves.
os.unlink(self.unique_name)
raise UnlockError
def is_locked(self):
"""Check if anybody is holding the lock."""
return os.path.exists(self.lock_file)
def i_am_locking(self):
"""Check if we are holding the lock."""
return (self.is_locked() and
os.path.exists(self.unique_name) and
os.stat(self.unique_name).st_nlink == 2)
def break_lock(self):
"""Break (another processes) lock."""
if os.path.exists(self.lock_file):
os.unlink(self.lock_file)
def age_of_lock(self):
"""Returns the time since creation of lock in seconds."""
try:
# Creating the hard link for the lock updates the change time.
age = time.time() - os.stat(self.lock_file).st_ctime
except OSError:
age = -1.0
return age
FileLock = LinkFileLock