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

72 statements  

1from __future__ import annotations 

2 

3import os 

4import socket 

5import sys 

6from contextlib import suppress 

7from errno import EACCES, EEXIST, EPERM, ESRCH 

8from pathlib import Path 

9 

10from ._api import BaseFileLock 

11from ._util import ensure_directory_exists, raise_on_not_writable_file 

12 

13_WIN_SYNCHRONIZE = 0x100000 

14_WIN_ERROR_INVALID_PARAMETER = 87 

15 

16 

17class SoftFileLock(BaseFileLock): 

18 """ 

19 Portable file lock based on file existence. 

20 

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. 

25 

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 """ 

29 

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 

58 

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() 

74 

75 @staticmethod 

76 def _is_process_alive(pid: int) -> bool: 

77 if sys.platform == "win32": # pragma: win32 cover 

78 import ctypes # noqa: PLC0415 

79 

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 

95 

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()) 

100 

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() 

107 

108 

109__all__ = [ 

110 "SoftFileLock", 

111]