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
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 math
4import os
5from contextlib import suppress
6from typing import Final, Literal, NamedTuple
8from ._identity import host_name, process_start_token
9from ._soft import SoftFileLock, _read_lock_file
10from ._util import write_all
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"
17_MAX_PID: Final[int] = 2**31 - 1
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"]
24class OwnerRecord(NamedTuple):
25 """The owner published in a protocol 2 marker."""
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
35class MarkerSoftFileLock(SoftFileLock):
36 """An existence lock whose marker carries a protocol 2 owner record."""
38 #: Filled in by each mode so the published record states the contract its holder acquired under.
39 _owner_mode: OwnerMode
41 @property
42 def owner(self) -> OwnerRecord | None:
43 """
44 The owner named by the marker on disk.
46 :returns: the published record, or ``None`` when no marker exists or its record is malformed or protocol 1
48 """
49 return self._read_owner()
51 @property
52 def pid(self) -> int | None:
53 """
54 The PID of the process holding this lock, read from the marker.
56 :returns: the PID, or ``None`` when no marker exists or its record is unreadable
58 """
59 return None if (owner := self._read_owner()) is None else owner.pid
61 @property
62 def is_lock_held_by_us(self) -> bool:
63 """
64 Whether the marker on disk names this process.
66 :returns: ``True`` when the marker's PID and hostname match this process
68 """
69 owner = self._read_owner()
70 return owner is not None and owner.pid == os.getpid() and owner.hostname == host_name()
72 def force_break(self) -> None:
73 """
74 Remove the marker whoever holds it, so a later contender can acquire.
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()
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
86 def _write_lock_info(self, fd: int) -> None:
87 write_all(fd, encode_marker(self._published_record()))
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 )
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()
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)
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)
151__all__ = [
152 "MarkerSoftFileLock",
153 "OwnerMode",
154 "OwnerRecord",
155 "encode_marker",
156 "parse_marker",
157]