_unix.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. from __future__ import annotations
  2. import os
  3. import sys
  4. import warnings
  5. from contextlib import suppress
  6. from errno import EAGAIN, ENOSYS, EWOULDBLOCK
  7. from pathlib import Path
  8. from typing import cast
  9. from ._api import BaseFileLock
  10. from ._util import ensure_directory_exists
  11. #: a flag to indicate if the fcntl API is available
  12. has_fcntl = False
  13. if sys.platform == "win32": # pragma: win32 cover
  14. class UnixFileLock(BaseFileLock):
  15. """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems."""
  16. def _acquire(self) -> None:
  17. raise NotImplementedError
  18. def _release(self) -> None:
  19. raise NotImplementedError
  20. else: # pragma: win32 no cover
  21. try:
  22. import fcntl
  23. _ = (fcntl.flock, fcntl.LOCK_EX, fcntl.LOCK_NB, fcntl.LOCK_UN)
  24. except (ImportError, AttributeError):
  25. pass
  26. else:
  27. has_fcntl = True
  28. class UnixFileLock(BaseFileLock):
  29. """
  30. Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.
  31. Lock file cleanup: Unix and macOS delete the lock file reliably after release, even in
  32. multi-threaded scenarios. Unlike Windows, Unix allows unlinking files that other processes
  33. have open.
  34. """
  35. def _acquire(self) -> None: # noqa: C901, PLR0912
  36. ensure_directory_exists(self.lock_file)
  37. open_flags = os.O_RDWR | os.O_TRUNC
  38. o_nofollow = getattr(os, "O_NOFOLLOW", None)
  39. if o_nofollow is not None:
  40. open_flags |= o_nofollow
  41. open_flags |= os.O_CREAT
  42. open_mode = self._open_mode()
  43. try:
  44. fd = os.open(self.lock_file, open_flags, open_mode)
  45. except FileNotFoundError:
  46. # On FUSE/NFS, os.open(O_CREAT) is not atomic: LOOKUP + CREATE can be split, allowing a concurrent
  47. # unlink() to delete the file between them. For valid paths, treat ENOENT as transient contention.
  48. # For invalid paths (e.g., empty string), re-raise to avoid infinite retry loops.
  49. if self.lock_file and Path(self.lock_file).parent.exists():
  50. return
  51. raise
  52. except PermissionError:
  53. # Sticky-bit dirs (e.g. /tmp): O_CREAT fails if the file is owned by another user (#317).
  54. # Fall back to opening the existing file without O_CREAT.
  55. if not Path(self.lock_file).exists():
  56. raise
  57. try:
  58. fd = os.open(self.lock_file, open_flags & ~os.O_CREAT, open_mode)
  59. except FileNotFoundError:
  60. return
  61. if self.has_explicit_mode:
  62. with suppress(PermissionError):
  63. os.fchmod(fd, self._context.mode)
  64. try:
  65. fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  66. except OSError as exception:
  67. os.close(fd)
  68. if exception.errno == ENOSYS:
  69. with suppress(OSError):
  70. Path(self.lock_file).unlink()
  71. self._fallback_to_soft_lock()
  72. self._acquire()
  73. return
  74. if exception.errno not in {EAGAIN, EWOULDBLOCK}:
  75. raise
  76. else:
  77. # The file may have been unlinked by a concurrent _release() between our open() and flock().
  78. # A lock on an unlinked inode is useless — discard and let the retry loop start fresh.
  79. if os.fstat(fd).st_nlink == 0:
  80. os.close(fd)
  81. else:
  82. self._context.lock_file_fd = fd
  83. def _fallback_to_soft_lock(self) -> None:
  84. from ._soft import SoftFileLock # noqa: PLC0415
  85. warnings.warn("flock not supported on this filesystem, falling back to SoftFileLock", stacklevel=2)
  86. from .asyncio import AsyncSoftFileLock, BaseAsyncFileLock # noqa: PLC0415
  87. self.__class__ = AsyncSoftFileLock if isinstance(self, BaseAsyncFileLock) else SoftFileLock
  88. def _release(self) -> None:
  89. fd = cast("int", self._context.lock_file_fd)
  90. self._context.lock_file_fd = None
  91. with suppress(OSError):
  92. Path(self.lock_file).unlink()
  93. fcntl.flock(fd, fcntl.LOCK_UN)
  94. with suppress(OSError): # close can raise EIO on FUSE/Docker bind-mount filesystems after unlink
  95. os.close(fd)
  96. __all__ = [
  97. "UnixFileLock",
  98. "has_fcntl",
  99. ]