123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- # This is free and unencumbered software released into the public domain.
- #
- # Anyone is free to copy, modify, publish, use, compile, sell, or
- # distribute this software, either in source code form or as a compiled
- # binary, for any purpose, commercial or non-commercial, and by any
- # means.
- #
- # In jurisdictions that recognize copyright laws, the author or authors
- # of this software dedicate any and all copyright interest in the
- # software to the public domain. We make this dedication for the benefit
- # of the public at large and to the detriment of our heirs and
- # successors. We intend this dedication to be an overt act of
- # relinquishment in perpetuity of all present and future rights to this
- # software under copyright law.
- #
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- # IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
- # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
- # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
- # OTHER DEALINGS IN THE SOFTWARE.
- #
- # For more information, please refer to <http://unlicense.org>
- """
- A platform independent file lock that supports the with-statement.
- """
- # Modules
- # ------------------------------------------------
- import logging
- import os
- import threading
- import time
- try:
- import warnings
- except ImportError:
- warnings = None
- try:
- import msvcrt
- except ImportError:
- msvcrt = None
- try:
- import fcntl
- except ImportError:
- fcntl = None
- # Backward compatibility
- # ------------------------------------------------
- try:
- TimeoutError
- except NameError:
- TimeoutError = OSError
- # Data
- # ------------------------------------------------
- __all__ = [
- "Timeout",
- "BaseFileLock",
- "WindowsFileLock",
- "UnixFileLock",
- "SoftFileLock",
- "FileLock"
- ]
- __version__ = "3.0.12"
- _logger = None
- def logger():
- """Returns the logger instance used in this module."""
- global _logger
- _logger = _logger or logging.getLogger(__name__)
- return _logger
- # Exceptions
- # ------------------------------------------------
- class Timeout(TimeoutError):
- """
- Raised when the lock could not be acquired in *timeout*
- seconds.
- """
- def __init__(self, lock_file):
- """
- """
- #: The path of the file lock.
- self.lock_file = lock_file
- return None
- def __str__(self):
- temp = "The file lock '{}' could not be acquired."\
- .format(self.lock_file)
- return temp
- # Classes
- # ------------------------------------------------
- # This is a helper class which is returned by :meth:`BaseFileLock.acquire`
- # and wraps the lock to make sure __enter__ is not called twice when entering
- # the with statement.
- # If we would simply return *self*, the lock would be acquired again
- # in the *__enter__* method of the BaseFileLock, but not released again
- # automatically.
- #
- # :seealso: issue #37 (memory leak)
- class _Acquire_ReturnProxy(object):
- def __init__(self, lock):
- self.lock = lock
- return None
- def __enter__(self):
- return self.lock
- def __exit__(self, exc_type, exc_value, traceback):
- self.lock.release()
- return None
- class BaseFileLock(object):
- """
- Implements the base class of a file lock.
- """
- def __init__(self, lock_file, timeout = -1):
- """
- """
- # The path to the lock file.
- self._lock_file = lock_file
- # The file descriptor for the *_lock_file* as it is returned by the
- # os.open() function.
- # This file lock is only NOT None, if the object currently holds the
- # lock.
- self._lock_file_fd = None
- # The default timeout value.
- self.timeout = timeout
- # We use this lock primarily for the lock counter.
- self._thread_lock = threading.Lock()
- # The lock counter is used for implementing the nested locking
- # mechanism. Whenever the lock is acquired, the counter is increased and
- # the lock is only released, when this value is 0 again.
- self._lock_counter = 0
- return None
- @property
- def lock_file(self):
- """
- The path to the lock file.
- """
- return self._lock_file
- @property
- def timeout(self):
- """
- You can set a default timeout for the filelock. It will be used as
- fallback value in the acquire method, if no timeout value (*None*) is
- given.
- If you want to disable the timeout, set it to a negative value.
- A timeout of 0 means, that there is exactly one attempt to acquire the
- file lock.
- .. versionadded:: 2.0.0
- """
- return self._timeout
- @timeout.setter
- def timeout(self, value):
- """
- """
- self._timeout = float(value)
- return None
- # Platform dependent locking
- # --------------------------------------------
- def _acquire(self):
- """
- Platform dependent. If the file lock could be
- acquired, self._lock_file_fd holds the file descriptor
- of the lock file.
- """
- raise NotImplementedError()
- def _release(self):
- """
- Releases the lock and sets self._lock_file_fd to None.
- """
- raise NotImplementedError()
- # Platform independent methods
- # --------------------------------------------
- @property
- def is_locked(self):
- """
- True, if the object holds the file lock.
- .. versionchanged:: 2.0.0
- This was previously a method and is now a property.
- """
- return self._lock_file_fd is not None
- def acquire(self, timeout=None, poll_intervall=0.05):
- """
- Acquires the file lock or fails with a :exc:`Timeout` error.
- .. code-block:: python
- # You can use this method in the context manager (recommended)
- with lock.acquire():
- pass
- # Or use an equivalent try-finally construct:
- lock.acquire()
- try:
- pass
- finally:
- lock.release()
- :arg float timeout:
- The maximum time waited for the file lock.
- If ``timeout < 0``, there is no timeout and this method will
- block until the lock could be acquired.
- If ``timeout`` is None, the default :attr:`~timeout` is used.
- :arg float poll_intervall:
- We check once in *poll_intervall* seconds if we can acquire the
- file lock.
- :raises Timeout:
- if the lock could not be acquired in *timeout* seconds.
- .. versionchanged:: 2.0.0
- This method returns now a *proxy* object instead of *self*,
- so that it can be used in a with statement without side effects.
- """
- # Use the default timeout, if no timeout is provided.
- if timeout is None:
- timeout = self.timeout
- # Increment the number right at the beginning.
- # We can still undo it, if something fails.
- with self._thread_lock:
- self._lock_counter += 1
- lock_id = id(self)
- lock_filename = self._lock_file
- start_time = time.time()
- try:
- while True:
- with self._thread_lock:
- if not self.is_locked:
- logger().debug('Attempting to acquire lock %s on %s', lock_id, lock_filename)
- self._acquire()
- if self.is_locked:
- logger().info('Lock %s acquired on %s', lock_id, lock_filename)
- break
- elif timeout >= 0 and time.time() - start_time > timeout:
- logger().debug('Timeout on acquiring lock %s on %s', lock_id, lock_filename)
- raise Timeout(self._lock_file)
- else:
- logger().debug(
- 'Lock %s not acquired on %s, waiting %s seconds ...',
- lock_id, lock_filename, poll_intervall
- )
- time.sleep(poll_intervall)
- except:
- # Something did go wrong, so decrement the counter.
- with self._thread_lock:
- self._lock_counter = max(0, self._lock_counter - 1)
- raise
- return _Acquire_ReturnProxy(lock = self)
- def release(self, force = False):
- """
- Releases the file lock.
- Please note, that the lock is only completly released, if the lock
- counter is 0.
- Also note, that the lock file itself is not automatically deleted.
- :arg bool force:
- If true, the lock counter is ignored and the lock is released in
- every case.
- """
- with self._thread_lock:
- if self.is_locked:
- self._lock_counter -= 1
- if self._lock_counter == 0 or force:
- lock_id = id(self)
- lock_filename = self._lock_file
- logger().debug('Attempting to release lock %s on %s', lock_id, lock_filename)
- self._release()
- self._lock_counter = 0
- logger().info('Lock %s released on %s', lock_id, lock_filename)
- return None
- def __enter__(self):
- self.acquire()
- return self
- def __exit__(self, exc_type, exc_value, traceback):
- self.release()
- return None
- def __del__(self):
- self.release(force = True)
- return None
- # Windows locking mechanism
- # ~~~~~~~~~~~~~~~~~~~~~~~~~
- class WindowsFileLock(BaseFileLock):
- """
- Uses the :func:`msvcrt.locking` function to hard lock the lock file on
- windows systems.
- """
- def _acquire(self):
- open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
- try:
- fd = os.open(self._lock_file, open_mode)
- except OSError:
- pass
- else:
- try:
- msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
- except (IOError, OSError):
- os.close(fd)
- else:
- self._lock_file_fd = fd
- return None
- def _release(self):
- fd = self._lock_file_fd
- self._lock_file_fd = None
- msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
- os.close(fd)
- try:
- os.remove(self._lock_file)
- # Probably another instance of the application
- # that acquired the file lock.
- except OSError:
- pass
- return None
- # Unix locking mechanism
- # ~~~~~~~~~~~~~~~~~~~~~~
- class UnixFileLock(BaseFileLock):
- """
- Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.
- """
- def _acquire(self):
- open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
- fd = os.open(self._lock_file, open_mode)
- try:
- fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
- except (IOError, OSError):
- os.close(fd)
- else:
- self._lock_file_fd = fd
- return None
- def _release(self):
- # Do not remove the lockfile:
- #
- # https://github.com/benediktschmitt/py-filelock/issues/31
- # https://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition
- fd = self._lock_file_fd
- self._lock_file_fd = None
- fcntl.flock(fd, fcntl.LOCK_UN)
- os.close(fd)
- return None
- # Soft lock
- # ~~~~~~~~~
- class SoftFileLock(BaseFileLock):
- """
- Simply watches the existence of the lock file.
- """
- def _acquire(self):
- open_mode = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_TRUNC
- try:
- fd = os.open(self._lock_file, open_mode)
- except (IOError, OSError):
- pass
- else:
- self._lock_file_fd = fd
- return None
- def _release(self):
- os.close(self._lock_file_fd)
- self._lock_file_fd = None
- try:
- os.remove(self._lock_file)
- # The file is already deleted and that's what we want.
- except OSError:
- pass
- return None
- # Platform filelock
- # ~~~~~~~~~~~~~~~~~
- #: Alias for the lock, which should be used for the current platform. On
- #: Windows, this is an alias for :class:`WindowsFileLock`, on Unix for
- #: :class:`UnixFileLock` and otherwise for :class:`SoftFileLock`.
- FileLock = None
- if msvcrt:
- FileLock = WindowsFileLock
- elif fcntl:
- FileLock = UnixFileLock
- else:
- FileLock = SoftFileLock
- if warnings is not None:
- warnings.warn("only soft file lock is available")
|