Portable file lock based on file existence. 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 treats its e
| 19 | |
| 20 | |
| 21 | class SoftFileLock(BaseFileLock): |
| 22 | """ |
| 23 | Portable file lock based on file existence. |
| 24 | |
| 25 | Unlike :class:`UnixFileLock <filelock.UnixFileLock>` and :class:`WindowsFileLock <filelock.WindowsFileLock>`, this |
| 26 | lock does not use OS-level locking primitives. Instead, it creates the lock file with ``O_CREAT | O_EXCL`` and |
| 27 | treats its existence as the lock indicator. This makes it work on any filesystem but leaves stale lock files behind |
| 28 | if the process crashes without releasing the lock. |
| 29 | |
| 30 | To mitigate stale locks, the lock file contains the PID and hostname of the holding process. On contention, if the |
| 31 | holder is on the same host and its PID no longer exists, the stale lock is broken automatically. |
| 32 | |
| 33 | """ |
| 34 | |
| 35 | def _acquire(self) -> None: |
| 36 | raise_on_not_writable_file(self.lock_file) |
| 37 | ensure_directory_exists(self.lock_file) |
| 38 | flags = ( |
| 39 | os.O_WRONLY # open for writing only |
| 40 | | os.O_CREAT |
| 41 | | os.O_EXCL # together with above raise EEXIST if the file specified by filename exists |
| 42 | | os.O_TRUNC # truncate the file to zero byte |
| 43 | ) |
| 44 | if (o_nofollow := getattr(os, "O_NOFOLLOW", None)) is not None: |
| 45 | flags |= o_nofollow |
| 46 | try: |
| 47 | file_handler = os.open(self.lock_file, flags, self._open_mode()) |
| 48 | except OSError as exception: |
| 49 | if not ( |
| 50 | exception.errno == EEXIST or (exception.errno == EACCES and sys.platform == "win32") |
| 51 | ): # pragma: win32 no cover |
| 52 | raise |
| 53 | self._try_break_stale_lock() |
| 54 | else: |
| 55 | self._write_lock_info(file_handler) |
| 56 | self._context.lock_file_fd = file_handler |
| 57 | |
| 58 | def _try_break_stale_lock(self) -> None: |
| 59 | with suppress(OSError, ValueError): |
| 60 | content, mtime, ino = _read_lock_file(self.lock_file) |
| 61 | holder = _parse_lock_holder(content) |
| 62 | |
| 63 | if holder is None: |
| 64 | # Unparsable: wrong line count, a non-integer PID or creation time, empty, oversized or not UTF-8. |
| 65 | # Self-heal only once the file is clearly not a half-written fresh lock (a peer between O_EXCL and |
| 66 | # _write_lock_info), so the brief create-then-write window is never mistaken for a stale lock. |
| 67 | if time.time() - mtime >= _MALFORMED_LOCK_AGE_THRESHOLD: |
| 68 | break_lock_file(self.lock_file, mtime, ino) |
| 69 | return |
| 70 | |
| 71 | pid, hostname, creation_time = holder |
| 72 | if hostname != socket.gethostname(): |
| 73 | return |
| 74 | |
| 75 | if self._is_process_alive(pid): |
| 76 | if sys.platform != "win32" or creation_time is None: # pragma: win32 no cover |
| 77 | return # same process, or no creation time to disambiguate a recycled PID — don't evict |
| 78 | actual = self._get_process_creation_time(pid) # pragma: win32 cover |
no outgoing calls
searching dependent graphs…