| | from __future__ import annotations |
| |
|
| | import os |
| | import socket |
| | import sys |
| | import time |
| | from contextlib import suppress |
| | from errno import EACCES, EEXIST, EPERM, ESRCH |
| | from pathlib import Path |
| |
|
| | from ._api import BaseFileLock |
| | from ._util import ensure_directory_exists, raise_on_not_writable_file |
| |
|
| | _WIN_SYNCHRONIZE = 0x100000 |
| | _WIN_ERROR_INVALID_PARAMETER = 87 |
| |
|
| |
|
| | class SoftFileLock(BaseFileLock): |
| | """ |
| | Portable file lock based on file existence. |
| | |
| | Unlike :class:`UnixFileLock <filelock.UnixFileLock>` and :class:`WindowsFileLock <filelock.WindowsFileLock>`, this |
| | lock does not use OS-level locking primitives. Instead, it creates the lock file with ``O_CREAT | O_EXCL`` and |
| | treats its existence as the lock indicator. This makes it work on any filesystem but leaves stale lock files behind |
| | if the process crashes without releasing the lock. |
| | |
| | To mitigate stale locks, the lock file contains the PID and hostname of the holding process. On contention, if the |
| | holder is on the same host and its PID no longer exists, the stale lock is broken automatically. |
| | |
| | """ |
| |
|
| | def _acquire(self) -> None: |
| | raise_on_not_writable_file(self.lock_file) |
| | ensure_directory_exists(self.lock_file) |
| | flags = ( |
| | os.O_WRONLY |
| | | os.O_CREAT |
| | | os.O_EXCL |
| | | os.O_TRUNC |
| | ) |
| | if (o_nofollow := getattr(os, "O_NOFOLLOW", None)) is not None: |
| | flags |= o_nofollow |
| | try: |
| | file_handler = os.open(self.lock_file, flags, self._open_mode()) |
| | except OSError as exception: |
| | if not ( |
| | exception.errno == EEXIST or (exception.errno == EACCES and sys.platform == "win32") |
| | ): |
| | raise |
| | if exception.errno == EEXIST and sys.platform != "win32": |
| | self._try_break_stale_lock() |
| | else: |
| | self._write_lock_info(file_handler) |
| | self._context.lock_file_fd = file_handler |
| |
|
| | def _try_break_stale_lock(self) -> None: |
| | with suppress(OSError): |
| | content = Path(self.lock_file).read_text(encoding="utf-8") |
| | lines = content.strip().splitlines() |
| | if len(lines) != 2: |
| | return |
| | pid_str, hostname = lines |
| | if hostname != socket.gethostname(): |
| | return |
| | pid = int(pid_str) |
| | if self._is_process_alive(pid): |
| | return |
| | break_path = f"{self.lock_file}.break.{os.getpid()}" |
| | Path(self.lock_file).rename(break_path) |
| | Path(break_path).unlink() |
| |
|
| | @staticmethod |
| | def _is_process_alive(pid: int) -> bool: |
| | if sys.platform == "win32": |
| | import ctypes |
| |
|
| | kernel32 = ctypes.windll.kernel32 |
| | handle = kernel32.OpenProcess(_WIN_SYNCHRONIZE, 0, pid) |
| | if handle: |
| | kernel32.CloseHandle(handle) |
| | return True |
| | return kernel32.GetLastError() != _WIN_ERROR_INVALID_PARAMETER |
| | try: |
| | os.kill(pid, 0) |
| | except OSError as exc: |
| | if exc.errno == ESRCH: |
| | return False |
| | if exc.errno == EPERM: |
| | return True |
| | raise |
| | return True |
| |
|
| | @staticmethod |
| | def _write_lock_info(fd: int) -> None: |
| | with suppress(OSError): |
| | os.write(fd, f"{os.getpid()}\n{socket.gethostname()}\n".encode()) |
| |
|
| | def _release(self) -> None: |
| | assert self._context.lock_file_fd is not None |
| | os.close(self._context.lock_file_fd) |
| | self._context.lock_file_fd = None |
| | if sys.platform == "win32": |
| | self._windows_unlink_with_retry() |
| | else: |
| | with suppress(OSError): |
| | Path(self.lock_file).unlink() |
| |
|
| | def _windows_unlink_with_retry(self) -> None: |
| | max_retries = 10 |
| | retry_delay = 0.001 |
| | for attempt in range(max_retries): |
| | |
| | try: |
| | Path(self.lock_file).unlink() |
| | except OSError as exc: |
| | if exc.errno not in {EACCES, EPERM}: |
| | return |
| | if attempt < max_retries - 1: |
| | time.sleep(retry_delay) |
| | retry_delay *= 2 |
| | else: |
| | return |
| |
|
| |
|
| | __all__ = [ |
| | "SoftFileLock", |
| | ] |
| |
|