162 from ctypes
import wintypes
164 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
165 ERROR_INVALID_PARAMETER = 87
166 kernel32 = ctypes.WinDLL(
"kernel32", use_last_error=
True)
167 kernel32.OpenProcess.restype = wintypes.HANDLE
168 kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
169 kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
170 kernel32.GetProcessTimes.argtypes = [
172 ctypes.POINTER(wintypes.FILETIME),
173 ctypes.POINTER(wintypes.FILETIME),
174 ctypes.POINTER(wintypes.FILETIME),
175 ctypes.POINTER(wintypes.FILETIME),
177 handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
False, pid)
179 err = ctypes.get_last_error()
180 if err == ERROR_INVALID_PARAMETER:
181 return (PROBE_DEAD,
None)
182 return (PROBE_INCONCLUSIVE,
None)
184 creation = wintypes.FILETIME()
185 exit_t = wintypes.FILETIME()
186 kernel_t = wintypes.FILETIME()
187 user_t = wintypes.FILETIME()
188 ok = kernel32.GetProcessTimes(
189 handle, ctypes.byref(creation), ctypes.byref(exit_t),
190 ctypes.byref(kernel_t), ctypes.byref(user_t),
193 return (PROBE_INCONCLUSIVE,
None)
194 return (PROBE_ALIVE, f
"{creation.dwHighDateTime}:{creation.dwLowDateTime}")
196 kernel32.CloseHandle(handle)
206 """Parse the lock record, or ``None`` if missing/unreadable/malformed (H6)."""
208 data = json.loads(lock_path.read_text(encoding=
"utf-8"))
210 pid=int(data[
"pid"]),
211 process_start_time=str(data[
"process_start_time"]),
212 acquired_at=str(data[
"acquired_at"]),
214 except (OSError, ValueError, KeyError, TypeError):
218def _atomic_create(lock_path: Path, pid: int, start_time: str, acquired_at: str) ->
None:
219 """Create the lock file atomically (``O_CREAT|O_EXCL``) — raises
220 ``FileExistsError`` if a lock already exists (the no-race create)."""
221 payload = json.dumps(
222 {
"pid": pid,
"process_start_time": start_time,
"acquired_at": acquired_at},
225 fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
227 os.write(fd, payload.encode(
"utf-8"))
233 lock_path: Path, record: Optional[_LockRecord], reason: str, *, force: bool, clock: Clock
234) -> LockReclaimEvent:
236 lock_path=str(lock_path),
237 pid=record.pid
if record
is not None else None,
238 process_start_time=record.process_start_time
if record
is not None else None,
239 acquired_at=record.acquired_at
if record
is not None else None,
247 lock_path: Path, record: Optional[_LockRecord], force: bool, probe: Probe, clock: Clock
248) -> LockReclaimEvent:
249 """Return a ``LockReclaimEvent`` if reclaimable; else raise ``REGEN_LOCK_HELD``.
251 Reclaimable ⇔ provable holder death (dead PID or start-time mismatch), OR
252 ``force`` on a live/inconclusive holder. A malformed record (``None``) is
253 inconclusive — never auto-reclaimed.
255 reason: Optional[str] =
None
256 if record
is not None:
257 outcome, current_start = probe(record.pid)
258 if outcome == PROBE_DEAD:
259 reason = REASON_DEAD_PID
261 outcome == PROBE_ALIVE
263 and record.process_start_time
264 and current_start != record.process_start_time
266 reason = REASON_START_TIME_MISMATCH
267 if reason
is not None:
268 return _make_event(lock_path, record, reason, force=
False, clock=clock)
270 return _make_event(lock_path, record, REASON_FORCED, force=
True, clock=clock)
272 f
"regenerate lock held by a live-or-inconclusive process at {lock_path}",
273 code=REGEN_LOCK_HELD,
274 hint=
"another regenerate is running; wait for it, or pass --force to reclaim (logged)",
282 clock: Optional[Clock] =
None,
283 probe: Optional[Probe] =
None,
285 """Acquire the regenerate lock at ``lock_path``.
287 Raises ``REGEN_LOCK_HELD`` when the lock is held by a live-or-inconclusive
288 process and ``force`` is false. Creates ``lock_path.parent`` first (H5).
291 lock_path = Path(lock_path)
292 clock = clock
or _default_clock
293 probe = probe
or _default_probe
294 lock_path.parent.mkdir(parents=
True, exist_ok=
True)
295 self_pid = os.getpid()
296 self_start = probe(self_pid)[1]
or ""
297 acquired_at = clock()
301 except FileExistsError:
307 lock_path.unlink(missing_ok=
True)
310 except FileExistsError
as exc:
312 "regenerate lock re-acquired by another process during reclamation",
313 code=REGEN_LOCK_HELD,
314 hint=
"a concurrent regenerate won the race; retry",
316 _dbg(
"OC_CHAT.MAIN", func, f
"reclaimed lock ({event.reason})", 2)