Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/filelock/_soft.py: 25%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import os
4import socket
5import sys
6import time
7from contextlib import suppress
8from errno import EACCES, EEXIST, EPERM, ESRCH
9from pathlib import Path
11from ._api import BaseFileLock
12from ._util import ensure_directory_exists, raise_on_not_writable_file
14_WIN_SYNCHRONIZE = 0x100000
15_WIN_ERROR_INVALID_PARAMETER = 87
18class SoftFileLock(BaseFileLock):
19 """
20 Portable file lock based on file existence.
22 Unlike :class:`UnixFileLock <filelock.UnixFileLock>` and :class:`WindowsFileLock <filelock.WindowsFileLock>`, this
23 lock does not use OS-level locking primitives. Instead, it creates the lock file with ``O_CREAT | O_EXCL`` and
24 treats its existence as the lock indicator. This makes it work on any filesystem but leaves stale lock files behind
25 if the process crashes without releasing the lock.
27 To mitigate stale locks, the lock file contains the PID and hostname of the holding process. On contention, if the
28 holder is on the same host and its PID no longer exists, the stale lock is broken automatically.
30 """
32 def _acquire(self) -> None:
33 raise_on_not_writable_file(self.lock_file)
34 ensure_directory_exists(self.lock_file)
35 flags = (
36 os.O_WRONLY # open for writing only
37 | os.O_CREAT
38 | os.O_EXCL # together with above raise EEXIST if the file specified by filename exists
39 | os.O_TRUNC # truncate the file to zero byte
40 )
41 if (o_nofollow := getattr(os, "O_NOFOLLOW", None)) is not None:
42 flags |= o_nofollow
43 try:
44 file_handler = os.open(self.lock_file, flags, self._open_mode())
45 except OSError as exception:
46 if not (
47 exception.errno == EEXIST or (exception.errno == EACCES and sys.platform == "win32")
48 ): # pragma: win32 no cover
49 raise
50 if exception.errno == EEXIST and sys.platform != "win32": # pragma: win32 no cover
51 self._try_break_stale_lock()
52 else:
53 self._write_lock_info(file_handler)
54 self._context.lock_file_fd = file_handler
56 def _try_break_stale_lock(self) -> None:
57 with suppress(OSError):
58 content = Path(self.lock_file).read_text(encoding="utf-8")
59 lines = content.strip().splitlines()
60 if len(lines) != 2: # noqa: PLR2004
61 return
62 pid_str, hostname = lines
63 if hostname != socket.gethostname():
64 return
65 pid = int(pid_str)
66 if self._is_process_alive(pid):
67 return
68 break_path = f"{self.lock_file}.break.{os.getpid()}"
69 Path(self.lock_file).rename(break_path)
70 Path(break_path).unlink()
72 @staticmethod
73 def _is_process_alive(pid: int) -> bool:
74 if sys.platform == "win32": # pragma: win32 cover
75 import ctypes # noqa: PLC0415
77 kernel32 = ctypes.windll.kernel32
78 handle = kernel32.OpenProcess(_WIN_SYNCHRONIZE, 0, pid)
79 if handle:
80 kernel32.CloseHandle(handle)
81 return True
82 return kernel32.GetLastError() != _WIN_ERROR_INVALID_PARAMETER
83 try:
84 os.kill(pid, 0)
85 except OSError as exc:
86 if exc.errno == ESRCH:
87 return False
88 if exc.errno == EPERM:
89 return True
90 raise
91 return True
93 @staticmethod
94 def _write_lock_info(fd: int) -> None:
95 with suppress(OSError):
96 os.write(fd, f"{os.getpid()}\n{socket.gethostname()}\n".encode())
98 def _release(self) -> None:
99 assert self._context.lock_file_fd is not None # noqa: S101
100 os.close(self._context.lock_file_fd)
101 self._context.lock_file_fd = None
102 if sys.platform == "win32":
103 self._windows_unlink_with_retry()
104 else:
105 with suppress(OSError):
106 Path(self.lock_file).unlink()
108 def _windows_unlink_with_retry(self) -> None:
109 max_retries = 10
110 retry_delay = 0.001
111 for attempt in range(max_retries):
112 # Windows doesn't immediately release file handles after close, causing EACCES/EPERM on unlink
113 try:
114 Path(self.lock_file).unlink()
115 except OSError as exc: # noqa: PERF203
116 if exc.errno not in {EACCES, EPERM}:
117 return
118 if attempt < max_retries - 1:
119 time.sleep(retry_delay)
120 retry_delay *= 2
121 else:
122 return
125__all__ = [
126 "SoftFileLock",
127]