filelock.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. # This is free and unencumbered software released into the public domain.
  2. #
  3. # Anyone is free to copy, modify, publish, use, compile, sell, or
  4. # distribute this software, either in source code form or as a compiled
  5. # binary, for any purpose, commercial or non-commercial, and by any
  6. # means.
  7. #
  8. # In jurisdictions that recognize copyright laws, the author or authors
  9. # of this software dedicate any and all copyright interest in the
  10. # software to the public domain. We make this dedication for the benefit
  11. # of the public at large and to the detriment of our heirs and
  12. # successors. We intend this dedication to be an overt act of
  13. # relinquishment in perpetuity of all present and future rights to this
  14. # software under copyright law.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  18. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  19. # IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
  20. # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
  21. # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  22. # OTHER DEALINGS IN THE SOFTWARE.
  23. #
  24. # For more information, please refer to <http://unlicense.org>
  25. """
  26. A platform independent file lock that supports the with-statement.
  27. """
  28. # Modules
  29. # ------------------------------------------------
  30. import logging
  31. import os
  32. import threading
  33. import time
  34. try:
  35. import warnings
  36. except ImportError:
  37. warnings = None
  38. try:
  39. import msvcrt
  40. except ImportError:
  41. msvcrt = None
  42. try:
  43. import fcntl
  44. except ImportError:
  45. fcntl = None
  46. # Backward compatibility
  47. # ------------------------------------------------
  48. try:
  49. TimeoutError
  50. except NameError:
  51. TimeoutError = OSError
  52. # Data
  53. # ------------------------------------------------
  54. __all__ = [
  55. "Timeout",
  56. "BaseFileLock",
  57. "WindowsFileLock",
  58. "UnixFileLock",
  59. "SoftFileLock",
  60. "FileLock"
  61. ]
  62. __version__ = "3.0.12"
  63. _logger = None
  64. def logger():
  65. """Returns the logger instance used in this module."""
  66. global _logger
  67. _logger = _logger or logging.getLogger(__name__)
  68. return _logger
  69. # Exceptions
  70. # ------------------------------------------------
  71. class Timeout(TimeoutError):
  72. """
  73. Raised when the lock could not be acquired in *timeout*
  74. seconds.
  75. """
  76. def __init__(self, lock_file):
  77. """
  78. """
  79. #: The path of the file lock.
  80. self.lock_file = lock_file
  81. return None
  82. def __str__(self):
  83. temp = "The file lock '{}' could not be acquired."\
  84. .format(self.lock_file)
  85. return temp
  86. # Classes
  87. # ------------------------------------------------
  88. # This is a helper class which is returned by :meth:`BaseFileLock.acquire`
  89. # and wraps the lock to make sure __enter__ is not called twice when entering
  90. # the with statement.
  91. # If we would simply return *self*, the lock would be acquired again
  92. # in the *__enter__* method of the BaseFileLock, but not released again
  93. # automatically.
  94. #
  95. # :seealso: issue #37 (memory leak)
  96. class _Acquire_ReturnProxy(object):
  97. def __init__(self, lock):
  98. self.lock = lock
  99. return None
  100. def __enter__(self):
  101. return self.lock
  102. def __exit__(self, exc_type, exc_value, traceback):
  103. self.lock.release()
  104. return None
  105. class BaseFileLock(object):
  106. """
  107. Implements the base class of a file lock.
  108. """
  109. def __init__(self, lock_file, timeout = -1):
  110. """
  111. """
  112. # The path to the lock file.
  113. self._lock_file = lock_file
  114. # The file descriptor for the *_lock_file* as it is returned by the
  115. # os.open() function.
  116. # This file lock is only NOT None, if the object currently holds the
  117. # lock.
  118. self._lock_file_fd = None
  119. # The default timeout value.
  120. self.timeout = timeout
  121. # We use this lock primarily for the lock counter.
  122. self._thread_lock = threading.Lock()
  123. # The lock counter is used for implementing the nested locking
  124. # mechanism. Whenever the lock is acquired, the counter is increased and
  125. # the lock is only released, when this value is 0 again.
  126. self._lock_counter = 0
  127. return None
  128. @property
  129. def lock_file(self):
  130. """
  131. The path to the lock file.
  132. """
  133. return self._lock_file
  134. @property
  135. def timeout(self):
  136. """
  137. You can set a default timeout for the filelock. It will be used as
  138. fallback value in the acquire method, if no timeout value (*None*) is
  139. given.
  140. If you want to disable the timeout, set it to a negative value.
  141. A timeout of 0 means, that there is exactly one attempt to acquire the
  142. file lock.
  143. .. versionadded:: 2.0.0
  144. """
  145. return self._timeout
  146. @timeout.setter
  147. def timeout(self, value):
  148. """
  149. """
  150. self._timeout = float(value)
  151. return None
  152. # Platform dependent locking
  153. # --------------------------------------------
  154. def _acquire(self):
  155. """
  156. Platform dependent. If the file lock could be
  157. acquired, self._lock_file_fd holds the file descriptor
  158. of the lock file.
  159. """
  160. raise NotImplementedError()
  161. def _release(self):
  162. """
  163. Releases the lock and sets self._lock_file_fd to None.
  164. """
  165. raise NotImplementedError()
  166. # Platform independent methods
  167. # --------------------------------------------
  168. @property
  169. def is_locked(self):
  170. """
  171. True, if the object holds the file lock.
  172. .. versionchanged:: 2.0.0
  173. This was previously a method and is now a property.
  174. """
  175. return self._lock_file_fd is not None
  176. def acquire(self, timeout=None, poll_intervall=0.05):
  177. """
  178. Acquires the file lock or fails with a :exc:`Timeout` error.
  179. .. code-block:: python
  180. # You can use this method in the context manager (recommended)
  181. with lock.acquire():
  182. pass
  183. # Or use an equivalent try-finally construct:
  184. lock.acquire()
  185. try:
  186. pass
  187. finally:
  188. lock.release()
  189. :arg float timeout:
  190. The maximum time waited for the file lock.
  191. If ``timeout < 0``, there is no timeout and this method will
  192. block until the lock could be acquired.
  193. If ``timeout`` is None, the default :attr:`~timeout` is used.
  194. :arg float poll_intervall:
  195. We check once in *poll_intervall* seconds if we can acquire the
  196. file lock.
  197. :raises Timeout:
  198. if the lock could not be acquired in *timeout* seconds.
  199. .. versionchanged:: 2.0.0
  200. This method returns now a *proxy* object instead of *self*,
  201. so that it can be used in a with statement without side effects.
  202. """
  203. # Use the default timeout, if no timeout is provided.
  204. if timeout is None:
  205. timeout = self.timeout
  206. # Increment the number right at the beginning.
  207. # We can still undo it, if something fails.
  208. with self._thread_lock:
  209. self._lock_counter += 1
  210. lock_id = id(self)
  211. lock_filename = self._lock_file
  212. start_time = time.time()
  213. try:
  214. while True:
  215. with self._thread_lock:
  216. if not self.is_locked:
  217. logger().debug('Attempting to acquire lock %s on %s', lock_id, lock_filename)
  218. self._acquire()
  219. if self.is_locked:
  220. logger().info('Lock %s acquired on %s', lock_id, lock_filename)
  221. break
  222. elif timeout >= 0 and time.time() - start_time > timeout:
  223. logger().debug('Timeout on acquiring lock %s on %s', lock_id, lock_filename)
  224. raise Timeout(self._lock_file)
  225. else:
  226. logger().debug(
  227. 'Lock %s not acquired on %s, waiting %s seconds ...',
  228. lock_id, lock_filename, poll_intervall
  229. )
  230. time.sleep(poll_intervall)
  231. except:
  232. # Something did go wrong, so decrement the counter.
  233. with self._thread_lock:
  234. self._lock_counter = max(0, self._lock_counter - 1)
  235. raise
  236. return _Acquire_ReturnProxy(lock = self)
  237. def release(self, force = False):
  238. """
  239. Releases the file lock.
  240. Please note, that the lock is only completly released, if the lock
  241. counter is 0.
  242. Also note, that the lock file itself is not automatically deleted.
  243. :arg bool force:
  244. If true, the lock counter is ignored and the lock is released in
  245. every case.
  246. """
  247. with self._thread_lock:
  248. if self.is_locked:
  249. self._lock_counter -= 1
  250. if self._lock_counter == 0 or force:
  251. lock_id = id(self)
  252. lock_filename = self._lock_file
  253. logger().debug('Attempting to release lock %s on %s', lock_id, lock_filename)
  254. self._release()
  255. self._lock_counter = 0
  256. logger().info('Lock %s released on %s', lock_id, lock_filename)
  257. return None
  258. def __enter__(self):
  259. self.acquire()
  260. return self
  261. def __exit__(self, exc_type, exc_value, traceback):
  262. self.release()
  263. return None
  264. def __del__(self):
  265. self.release(force = True)
  266. return None
  267. # Windows locking mechanism
  268. # ~~~~~~~~~~~~~~~~~~~~~~~~~
  269. class WindowsFileLock(BaseFileLock):
  270. """
  271. Uses the :func:`msvcrt.locking` function to hard lock the lock file on
  272. windows systems.
  273. """
  274. def _acquire(self):
  275. open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
  276. try:
  277. fd = os.open(self._lock_file, open_mode)
  278. except OSError:
  279. pass
  280. else:
  281. try:
  282. msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
  283. except (IOError, OSError):
  284. os.close(fd)
  285. else:
  286. self._lock_file_fd = fd
  287. return None
  288. def _release(self):
  289. fd = self._lock_file_fd
  290. self._lock_file_fd = None
  291. msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
  292. os.close(fd)
  293. try:
  294. os.remove(self._lock_file)
  295. # Probably another instance of the application
  296. # that acquired the file lock.
  297. except OSError:
  298. pass
  299. return None
  300. # Unix locking mechanism
  301. # ~~~~~~~~~~~~~~~~~~~~~~
  302. class UnixFileLock(BaseFileLock):
  303. """
  304. Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.
  305. """
  306. def _acquire(self):
  307. open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
  308. fd = os.open(self._lock_file, open_mode)
  309. try:
  310. fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  311. except (IOError, OSError):
  312. os.close(fd)
  313. else:
  314. self._lock_file_fd = fd
  315. return None
  316. def _release(self):
  317. # Do not remove the lockfile:
  318. #
  319. # https://github.com/benediktschmitt/py-filelock/issues/31
  320. # https://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition
  321. fd = self._lock_file_fd
  322. self._lock_file_fd = None
  323. fcntl.flock(fd, fcntl.LOCK_UN)
  324. os.close(fd)
  325. return None
  326. # Soft lock
  327. # ~~~~~~~~~
  328. class SoftFileLock(BaseFileLock):
  329. """
  330. Simply watches the existence of the lock file.
  331. """
  332. def _acquire(self):
  333. open_mode = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_TRUNC
  334. try:
  335. fd = os.open(self._lock_file, open_mode)
  336. except (IOError, OSError):
  337. pass
  338. else:
  339. self._lock_file_fd = fd
  340. return None
  341. def _release(self):
  342. os.close(self._lock_file_fd)
  343. self._lock_file_fd = None
  344. try:
  345. os.remove(self._lock_file)
  346. # The file is already deleted and that's what we want.
  347. except OSError:
  348. pass
  349. return None
  350. # Platform filelock
  351. # ~~~~~~~~~~~~~~~~~
  352. #: Alias for the lock, which should be used for the current platform. On
  353. #: Windows, this is an alias for :class:`WindowsFileLock`, on Unix for
  354. #: :class:`UnixFileLock` and otherwise for :class:`SoftFileLock`.
  355. FileLock = None
  356. if msvcrt:
  357. FileLock = WindowsFileLock
  358. elif fcntl:
  359. FileLock = UnixFileLock
  360. else:
  361. FileLock = SoftFileLock
  362. if warnings is not None:
  363. warnings.warn("only soft file lock is available")