Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/filelock/_soft.py: 28%
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
6from contextlib import suppress
7from errno import EACCES, EEXIST, EPERM, ESRCH
8from pathlib import Path
10from ._api import BaseFileLock
11from ._util import ensure_directory_exists, raise_on_not_writable_file
13_WIN_SYNCHRONIZE = 0x100000
14_WIN_ERROR_INVALID_PARAMETER = 87
17class SoftFileLock(BaseFileLock):
18 """
19 Portable file lock based on file existence.
21 Unlike :class:`UnixFileLock <filelock.UnixFileLock>` and :class:`WindowsFileLock <filelock.WindowsFileLock>`,
22 this lock does not use OS-level locking primitives. Instead, it creates the lock file with ``O_CREAT | O_EXCL``
23 and treats its existence as the lock indicator. This makes it work on any filesystem but leaves stale lock files
24 behind if the process crashes without releasing the lock.
26 To mitigate stale locks, the lock file contains the PID and hostname of the holding process. On contention, if the
27 holder is on the same host and its PID no longer exists, the stale lock is broken automatically.
28 """
30 def _acquire(self) -> None:
31 raise_on_not_writable_file(self.lock_file)
32 ensure_directory_exists(self.lock_file)
33 flags = (
34 os.O_WRONLY # open for writing only
35 | os.O_CREAT
36 | os.O_EXCL # together with above raise EEXIST if the file specified by filename exists
37 | os.O_TRUNC # truncate the file to zero byte
38 )
39 if (o_nofollow := getattr(os, "O_NOFOLLOW", None)) is not None:
40 flags |= o_nofollow
41 try:
42 file_handler = os.open(self.lock_file, flags, self._open_mode())
43 except OSError as exception: # re-raise unless expected exception
44 if not (
45 exception.errno == EEXIST # lock already exist
46 or (exception.errno == EACCES and sys.platform == "win32") # has no access to this lock
47 ): # pragma: win32 no cover
48 raise
49 # On Windows, stale detection is skipped: Python's os.open uses _wopen which cannot set
50 # FILE_SHARE_DELETE, so any read handle blocks DeleteFileW in _release — causing a livelock
51 # under threaded contention. EACCES already signals the holder is alive (fd still open), and
52 # EEXIST means the file will be cleaned up by the releasing thread shortly.
53 if exception.errno == EEXIST and sys.platform != "win32": # pragma: win32 no cover
54 self._try_break_stale_lock()
55 else:
56 self._write_lock_info(file_handler)
57 self._context.lock_file_fd = file_handler
59 def _try_break_stale_lock(self) -> None:
60 with suppress(OSError):
61 content = Path(self.lock_file).read_text(encoding="utf-8")
62 lines = content.strip().splitlines()
63 if len(lines) != 2: # noqa: PLR2004
64 return
65 pid_str, hostname = lines
66 if hostname != socket.gethostname():
67 return
68 pid = int(pid_str)
69 if self._is_process_alive(pid):
70 return
71 break_path = f"{self.lock_file}.break.{os.getpid()}"
72 Path(self.lock_file).rename(break_path)
73 Path(break_path).unlink()
75 @staticmethod
76 def _is_process_alive(pid: int) -> bool:
77 if sys.platform == "win32": # pragma: win32 cover
78 import ctypes # noqa: PLC0415
80 kernel32 = ctypes.windll.kernel32
81 handle = kernel32.OpenProcess(_WIN_SYNCHRONIZE, 0, pid)
82 if handle:
83 kernel32.CloseHandle(handle)
84 return True
85 return kernel32.GetLastError() != _WIN_ERROR_INVALID_PARAMETER
86 try:
87 os.kill(pid, 0)
88 except OSError as exc:
89 if exc.errno == ESRCH:
90 return False
91 if exc.errno == EPERM:
92 return True
93 raise
94 return True
96 @staticmethod
97 def _write_lock_info(fd: int) -> None:
98 with suppress(OSError):
99 os.write(fd, f"{os.getpid()}\n{socket.gethostname()}\n".encode())
101 def _release(self) -> None:
102 assert self._context.lock_file_fd is not None # noqa: S101
103 os.close(self._context.lock_file_fd) # the lock file is definitely not None
104 self._context.lock_file_fd = None
105 with suppress(OSError): # the file is already deleted and that's what we want
106 Path(self.lock_file).unlink()
109__all__ = [
110 "SoftFileLock",
111]