Cooperative file lock based on a shared existence marker. Unlike :class:`UnixFileLock ` and :class:`WindowsFileLock `, this lock does not use OS-level locking primitives. Instead, it creates the lock file with ``O_CREAT | O_EXCL`` and
| 21 | |
| 22 | |
| 23 | class SoftFileLock(BaseFileLock): |
| 24 | """ |
| 25 | Cooperative file lock based on a shared existence marker. |
| 26 | |
| 27 | Unlike :class:`UnixFileLock <filelock.UnixFileLock>` and :class:`WindowsFileLock <filelock.WindowsFileLock>`, this |
| 28 | lock does not use OS-level locking primitives. Instead, it creates the lock file with ``O_CREAT | O_EXCL`` and |
| 29 | treats its existence as the lock indicator. The filesystem must provide coherent exclusive creation and directory |
| 30 | updates to each participating process. A crash can leave the marker behind. |
| 31 | |
| 32 | The marker contains the holder's PID and hostname. A contender may remove it when it can no longer find a same-host |
| 33 | process with that PID. A configured :attr:`~filelock.BaseFileLock.lifetime` also permits removal based on marker |
| 34 | age, including while the holder remains alive. Age-based expiry can overlap protected operations and does not |
| 35 | provide strict mutual exclusion. |
| 36 | |
| 37 | """ |
| 38 | |
| 39 | #: Existence locks reclaim by unlinking a pathname, so an age-based lease may break one; a native inode lock cannot. |
| 40 | _lifetime_supported: bool = True |
| 41 | |
| 42 | #: Age-based expiry preserves historical behavior but does not provide strict mutual exclusion. |
| 43 | _lifetime_replacements: tuple[str, str] | None = ("StrictSoftFileLock", "SoftFileLease") |
| 44 | |
| 45 | #: An existence lock unlinks its marker to release, so it cannot promise to keep the pathname. |
| 46 | _preserve_lock_file_supported: bool = False |
| 47 | |
| 48 | #: An existence lock keeps protocol state in its marker, so it cannot lend the descriptor to an on_acquired hook. |
| 49 | _on_acquired_supported: bool = False |
| 50 | |
| 51 | def _acquire(self) -> None: |
| 52 | raise_on_not_writable_file(self.lock_file) |
| 53 | ensure_directory_exists(self.lock_file) |
| 54 | # O_CREAT | O_EXCL makes the create fail with EEXIST when the file already exists, so a successful open |
| 55 | # means this process now holds the lock. |
| 56 | flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_TRUNC |
| 57 | if (o_nofollow := getattr(os, "O_NOFOLLOW", None)) is not None: # pragma: needs o-nofollow |
| 58 | flags |= o_nofollow |
| 59 | try: |
| 60 | fd = os.open(self.lock_file, flags, self._open_mode()) |
| 61 | except OSError as exception: |
| 62 | if not ( |
| 63 | exception.errno == EEXIST or (exception.errno == EACCES and sys.platform == "win32") |
| 64 | ): # pragma: win32 no cover |
| 65 | raise |
| 66 | self._try_break_stale_lock() |
| 67 | return |
| 68 | self._mark_descriptor_pending(fd) |
| 69 | self._publish_held_marker(fd) |
| 70 | |
| 71 | def _publish_held_marker(self, fd: int) -> None: |
| 72 | # Publish held state only once the record is fully on disk. On any failure, including cancellation, close the |
| 73 | # descriptor and unlink the path only while it still names the file we opened, so a rollback never deletes a |
| 74 | # successor's marker that replaced ours at the same path after our lease expired. |
| 75 | identity: tuple[int, int] | None = None |
| 76 | try: |
| 77 | identity = _file_identity(os.fstat(fd)) |
| 78 | self._write_lock_info(fd) |
| 79 | except BaseException: |
| 80 | self._mark_descriptor_released() |
no outgoing calls