Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/filelock/_marker.py: 43%

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

79 statements  

1from __future__ import annotations 

2 

3import math 

4import os 

5from contextlib import suppress 

6from typing import Final, Literal, NamedTuple 

7 

8from ._identity import host_name, process_start_token 

9from ._soft import SoftFileLock, _read_lock_file 

10from ._util import write_all 

11 

12#: Protocol 1 is the legacy ``<pid>\n<hostname>\n[<start_token>\n]`` marker that :class:`SoftFileLock` still writes. 

13#: Protocol 2 carries the owner mode and the lease claim. A protocol 1 reader treats a protocol 2 marker as malformed 

14#: and evicts it after its grace period, so the two never guarantee mutual exclusion against each other. 

15_PROTOCOL: Final[str] = "filelock/2" 

16 

17_MAX_PID: Final[int] = 2**31 - 1 

18 

19#: ``unknown`` is never published: it names a mode some other filelock wrote that this version cannot interpret. Such a 

20#: record still identifies a live owner, so it is parsed rather than read as malformed and aged out. 

21OwnerMode = Literal["lease", "unknown"] 

22 

23 

24class OwnerRecord(NamedTuple): 

25 """The owner published in a protocol 2 marker.""" 

26 

27 pid: int 

28 hostname: str 

29 mode: OwnerMode 

30 token: str | None = None 

31 lease_duration: float | None = None 

32 start: int | None = None 

33 

34 

35class MarkerSoftFileLock(SoftFileLock): 

36 """An existence lock whose marker carries a protocol 2 owner record.""" 

37 

38 #: Filled in by each mode so the published record states the contract its holder acquired under. 

39 _owner_mode: OwnerMode 

40 

41 @property 

42 def owner(self) -> OwnerRecord | None: 

43 """ 

44 The owner named by the marker on disk. 

45 

46 :returns: the published record, or ``None`` when no marker exists or its record is malformed or protocol 1 

47 

48 """ 

49 return self._read_owner() 

50 

51 @property 

52 def pid(self) -> int | None: 

53 """ 

54 The PID of the process holding this lock, read from the marker. 

55 

56 :returns: the PID, or ``None`` when no marker exists or its record is unreadable 

57 

58 """ 

59 return None if (owner := self._read_owner()) is None else owner.pid 

60 

61 @property 

62 def is_lock_held_by_us(self) -> bool: 

63 """ 

64 Whether the marker on disk names this process. 

65 

66 :returns: ``True`` when the marker's PID and hostname match this process 

67 

68 """ 

69 owner = self._read_owner() 

70 return owner is not None and owner.pid == os.getpid() and owner.hostname == host_name() 

71 

72 def force_break(self) -> None: 

73 """ 

74 Remove the marker whoever holds it, so a later contender can acquire. 

75 

76 Forced breaking voids mutual exclusion: the previous holder keeps running and keeps using whatever the lock 

77 protects. Reserve it for an operator clearing a marker whose holder is known to be gone. 

78 """ 

79 self.break_lock() 

80 

81 def _read_owner(self) -> OwnerRecord | None: 

82 with suppress(OSError, ValueError): 

83 return parse_marker(_read_lock_file(self.lock_file)[0]) 

84 return None 

85 

86 def _write_lock_info(self, fd: int) -> None: 

87 write_all(fd, encode_marker(self._published_record())) 

88 

89 def _published_record(self) -> OwnerRecord: 

90 return OwnerRecord( 

91 pid=os.getpid(), 

92 hostname=host_name(), 

93 mode=self._owner_mode, 

94 start=process_start_token(os.getpid()), 

95 ) 

96 

97 

98def encode_marker(record: OwnerRecord) -> bytes: 

99 """Render an owner record as the bytes a protocol 2 marker holds.""" 

100 lines = [_PROTOCOL, f"pid={record.pid}", f"host={record.hostname}", f"mode={record.mode}"] 

101 if record.token is not None: 

102 lines.append(f"token={record.token}") 

103 if record.lease_duration is not None: 

104 lines.append(f"duration={record.lease_duration!r}") 

105 if record.start is not None: 

106 lines.append(f"start={record.start}") 

107 return "".join(f"{line}\n" for line in lines).encode() 

108 

109 

110def parse_marker(content: str | None) -> OwnerRecord | None: 

111 """Return the owner a protocol 2 marker names, or ``None`` when the record is malformed or protocol 1.""" 

112 if not content or not (lines := content.strip().splitlines()) or lines[0] != _PROTOCOL: 

113 return None 

114 fields: dict[str, str] = {} 

115 for line in lines[1:]: 

116 key, separator, value = line.partition("=") 

117 if not separator: 

118 return None 

119 fields[key] = value 

120 return _build_record(fields) 

121 

122 

123def _build_record(fields: dict[str, str]) -> OwnerRecord | None: 

124 # An unknown key is a field a newer filelock published, so ignore it rather than read the record as malformed. An 

125 # unrecognized mode is the same story one level up: a contract this version does not implement. Reading it as 

126 # malformed would age the marker out of a live owner's hands, so keep it and let the caller refuse to reclaim it. 

127 # A record naming no mode at all states no contract and stays malformed. 

128 if (published := fields.get("mode")) is None: 

129 return None 

130 mode: OwnerMode = "lease" if published == "lease" else "unknown" 

131 hostname = fields.get("host") 

132 if not hostname or "pid" not in fields: 

133 return None 

134 try: 

135 pid = int(fields["pid"]) 

136 duration = float(fields["duration"]) if "duration" in fields else None 

137 start = int(fields["start"]) if "start" in fields else None 

138 except ValueError: 

139 return None 

140 if not 1 <= pid <= _MAX_PID: 

141 return None 

142 token = fields.get("token") 

143 # float() accepts "nan" and "inf", and neither is non-positive, so a duration <= 0 guard alone would read such a 

144 # marker as a valid lease. A nan duration mismatches every configured duration and so wedges reclaim, where a 

145 # malformed marker ages out through the grace window. 

146 if mode == "lease" and (token is None or duration is None or not (math.isfinite(duration) and duration > 0)): 

147 return None 

148 return OwnerRecord(pid=pid, hostname=hostname, mode=mode, token=token, lease_duration=duration, start=start) 

149 

150 

151__all__ = [ 

152 "MarkerSoftFileLock", 

153 "OwnerMode", 

154 "OwnerRecord", 

155 "encode_marker", 

156 "parse_marker", 

157]