Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_soft.py215 linesDownload Raw Back to filelock
1from __future__ import annotations2 3import os4import socket5import sys6import time7from contextlib import suppress8from errno import EACCES, EEXIST, EPERM, ESRCH9from pathlib import Path10 11from ._api import BaseFileLock12from ._util import ensure_directory_exists, raise_on_not_writable_file13 14_WIN_SYNCHRONIZE = 0x10000015_WIN_ERROR_INVALID_PARAMETER = 8716_WIN_PROCESS_QUERY_LIMITED_INFORMATION = 0x100017_MALFORMED_LOCK_AGE_THRESHOLD = 2.018 19 20class SoftFileLock(BaseFileLock):21    """22    Portable file lock based on file existence.23 24    Unlike :class:`UnixFileLock <filelock.UnixFileLock>` and :class:`WindowsFileLock <filelock.WindowsFileLock>`, this25    lock does not use OS-level locking primitives. Instead, it creates the lock file with ``O_CREAT | O_EXCL`` and26    treats its existence as the lock indicator. This makes it work on any filesystem but leaves stale lock files behind27    if the process crashes without releasing the lock.28 29    To mitigate stale locks, the lock file contains the PID and hostname of the holding process. On contention, if the30    holder is on the same host and its PID no longer exists, the stale lock is broken automatically.31 32    """33 34    def _acquire(self) -> None:35        raise_on_not_writable_file(self.lock_file)36        ensure_directory_exists(self.lock_file)37        flags = (38            os.O_WRONLY  # open for writing only39            | os.O_CREAT40            | os.O_EXCL  # together with above raise EEXIST if the file specified by filename exists41            | os.O_TRUNC  # truncate the file to zero byte42        )43        if (o_nofollow := getattr(os, "O_NOFOLLOW", None)) is not None:44            flags |= o_nofollow45        try:46            file_handler = os.open(self.lock_file, flags, self._open_mode())47        except OSError as exception:48            if not (49                exception.errno == EEXIST or (exception.errno == EACCES and sys.platform == "win32")50            ):  # pragma: win32 no cover51                raise52            self._try_break_stale_lock()53        else:54            self._write_lock_info(file_handler)55            self._context.lock_file_fd = file_handler56 57    def _try_break_stale_lock(self) -> None:58        with suppress(OSError, ValueError):59            lock_path = Path(self.lock_file)60            stat_result = lock_path.stat()61            content = lock_path.read_text(encoding="utf-8")62            lines = content.strip().splitlines()63 64            if len(lines) not in {2, 3}:65                if time.time() - stat_result.st_mtime >= _MALFORMED_LOCK_AGE_THRESHOLD:66                    self._evict_lock_file()67                return68 69            pid_str, hostname = lines[0], lines[1]70            creation_time_str = lines[2] if len(lines) == 3 else None  # noqa: PLR200471 72            if hostname != socket.gethostname():73                return74 75            pid = int(pid_str)76 77            if self._is_process_alive(pid):78                if sys.platform == "win32" and creation_time_str is not None:  # pragma: win32 cover79                    stored = int(creation_time_str)80                    actual = self._get_process_creation_time(pid)81                    if actual is not None and actual != stored:82                        pass  # PID recycled, fall through to evict83                    else:84                        return  # same process or can't verify — don't evict85                else:86                    return87 88            self._evict_lock_file()89 90    def _evict_lock_file(self) -> None:91        break_path = f"{self.lock_file}.break.{os.getpid()}"92        Path(self.lock_file).rename(break_path)93        Path(break_path).unlink()94 95    @staticmethod96    def _is_process_alive(pid: int) -> bool:97        if sys.platform == "win32":  # pragma: win32 cover98            import ctypes  # noqa: PLC041599 100            kernel32 = ctypes.windll.kernel32101            handle = kernel32.OpenProcess(_WIN_SYNCHRONIZE, 0, pid)102            if handle:103                kernel32.CloseHandle(handle)104                return True105            return kernel32.GetLastError() != _WIN_ERROR_INVALID_PARAMETER106        try:107            os.kill(pid, 0)108        except OSError as exc:109            if exc.errno == ESRCH:110                return False111            if exc.errno == EPERM:112                return True113            raise114        return True115 116    @staticmethod117    def _get_process_creation_time(pid: int) -> int | None:118        """Return the process creation FILETIME as an integer on Windows, ``None`` otherwise."""119        if sys.platform != "win32":  # pragma: win32 no cover120            return None121        import ctypes  # pragma: win32 cover  # noqa: PLC0415122        from ctypes import wintypes  # noqa: PLC0415123 124        kernel32 = ctypes.windll.kernel32125        handle = kernel32.OpenProcess(_WIN_PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)126        if not handle:127            return None128        try:129            creation = wintypes.FILETIME()130            exit_time = wintypes.FILETIME()131            kernel_time = wintypes.FILETIME()132            user_time = wintypes.FILETIME()133            if not kernel32.GetProcessTimes(134                handle,135                ctypes.byref(creation),136                ctypes.byref(exit_time),137                ctypes.byref(kernel_time),138                ctypes.byref(user_time),139            ):140                return None141        finally:142            kernel32.CloseHandle(handle)143        return (creation.dwHighDateTime << 32) | creation.dwLowDateTime144 145    @staticmethod146    def _write_lock_info(fd: int) -> None:147        with suppress(OSError):148            info = f"{os.getpid()}\n{socket.gethostname()}\n"149            if sys.platform == "win32" and (ct := SoftFileLock._get_process_creation_time(os.getpid())) is not None:150                info += f"{ct}\n"151            os.write(fd, info.encode())152 153    @property154    def pid(self) -> int | None:155        """156        The PID of the process holding this lock, read from the lock file.157 158        :returns: the PID as an integer, or ``None`` if the lock file does not exist or cannot be parsed159 160        """161        try:162            content = Path(self.lock_file).read_text(encoding="utf-8")163            lines = content.strip().splitlines()164            if lines:165                return int(lines[0])166        except (OSError, ValueError):167            pass168        return None169 170    @property171    def is_lock_held_by_us(self) -> bool:172        """173        Whether this lock is held by the current process.174 175        :returns: ``True`` if the lock file exists and contains the current process's PID176 177        """178        return self.pid == os.getpid()179 180    def break_lock(self) -> None:181        """Forcibly break the lock by removing the lock file, regardless of who holds it."""182        with suppress(OSError):183            Path(self.lock_file).unlink()184 185    def _release(self) -> None:186        assert self._context.lock_file_fd is not None  # noqa: S101187        os.close(self._context.lock_file_fd)188        self._context.lock_file_fd = None189        if sys.platform == "win32":190            self._windows_unlink_with_retry()191        else:192            with suppress(OSError):193                Path(self.lock_file).unlink()194 195    def _windows_unlink_with_retry(self) -> None:196        max_retries = 10197        retry_delay = 0.001198        for attempt in range(max_retries):199            # Windows doesn't immediately release file handles after close, causing EACCES/EPERM on unlink200            try:201                Path(self.lock_file).unlink()202            except OSError as exc:  # noqa: PERF203203                if exc.errno not in {EACCES, EPERM}:204                    return205                if attempt < max_retries - 1:206                    time.sleep(retry_delay)207                    retry_delay *= 2208            else:209                return210 211 212__all__ = [213    "SoftFileLock",214]215 
codekingpro/portable-devtools · Team Ai