_unix.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  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. """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems."""
  30. def _acquire(self) -> None: # noqa: C901, PLR0912
  31. ensure_directory_exists(self.lock_file)
  32. open_flags = os.O_RDWR | os.O_TRUNC
  33. o_nofollow = getattr(os, "O_NOFOLLOW", None)
  34. if o_nofollow is not None:
  35. open_flags |= o_nofollow
  36. open_flags |= os.O_CREAT
  37. open_mode = self._open_mode()
  38. try:
  39. fd = os.open(self.lock_file, open_flags, open_mode)
  40. except FileNotFoundError:
  41. # On FUSE/NFS, os.open(O_CREAT) is not atomic: LOOKUP + CREATE can be split, allowing a concurrent
  42. # unlink() to delete the file between them. For valid paths, treat ENOENT as transient contention.
  43. # For invalid paths (e.g., empty string), re-raise to avoid infinite retry loops.
  44. if self.lock_file and Path(self.lock_file).parent.exists():
  45. return
  46. raise
  47. except PermissionError:
  48. # Sticky-bit dirs (e.g. /tmp): O_CREAT fails if the file is owned by another user (#317).
  49. # Fall back to opening the existing file without O_CREAT.
  50. if not Path(self.lock_file).exists():
  51. raise
  52. try:
  53. fd = os.open(self.lock_file, open_flags & ~os.O_CREAT, open_mode)
  54. except FileNotFoundError:
  55. return
  56. if self.has_explicit_mode:
  57. with suppress(PermissionError):
  58. os.fchmod(fd, self._context.mode)
  59. try:
  60. fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  61. except OSError as exception:
  62. os.close(fd)
  63. if exception.errno == ENOSYS:
  64. with suppress(OSError):
  65. Path(self.lock_file).unlink()
  66. self._fallback_to_soft_lock()
  67. self._acquire()
  68. return
  69. if exception.errno not in {EAGAIN, EWOULDBLOCK}:
  70. raise
  71. else:
  72. # The file may have been unlinked by a concurrent _release() between our open() and flock().
  73. # A lock on an unlinked inode is useless — discard and let the retry loop start fresh.
  74. if os.fstat(fd).st_nlink == 0:
  75. os.close(fd)
  76. else:
  77. self._context.lock_file_fd = fd
  78. def _fallback_to_soft_lock(self) -> None:
  79. from ._soft import SoftFileLock # noqa: PLC0415
  80. warnings.warn("flock not supported on this filesystem, falling back to SoftFileLock", stacklevel=2)
  81. from .asyncio import AsyncSoftFileLock, BaseAsyncFileLock # noqa: PLC0415
  82. self.__class__ = AsyncSoftFileLock if isinstance(self, BaseAsyncFileLock) else SoftFileLock
  83. def _release(self) -> None:
  84. fd = cast("int", self._context.lock_file_fd)
  85. self._context.lock_file_fd = None
  86. with suppress(OSError):
  87. Path(self.lock_file).unlink()
  88. fcntl.flock(fd, fcntl.LOCK_UN)
  89. os.close(fd)
  90. __all__ = [
  91. "UnixFileLock",
  92. "has_fcntl",
  93. ]