"""Atomic per-attempt state and an OS lock shared by client processes.""" import json import os from pathlib import Path import re import secrets from contextlib import contextmanager from .errors import StateError class StateStore: def __init__(self, directory): self.directory = Path(directory) try: self.directory.mkdir(parents=True, mode=0o700, exist_ok=True) except OSError: raise StateError("Cannot create the local state directory") from None def _path(self, handle, suffix=".json"): if not isinstance(handle, str) or not re.fullmatch(r"[0-9a-f]{32}", handle): raise StateError("Invalid local handle") return self.directory / (handle + suffix) @contextmanager def locked(self, handle): path = self._path(handle, ".lock") try: descriptor = os.open(path, os.O_CREAT | os.O_RDWR, 0o600) stream = os.fdopen(descriptor, "r+b") except OSError: raise StateError("Cannot open the local state lock") from None acquired = False try: if os.name == "nt": import msvcrt if os.fstat(stream.fileno()).st_size == 0: stream.write(b"\0") stream.flush() stream.seek(0) msvcrt.locking(stream.fileno(), msvcrt.LK_NBLCK, 1) else: import fcntl fcntl.flock(stream.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) acquired = True except OSError: stream.close() raise StateError("Another process is using this local handle") from None try: yield finally: if acquired: if os.name == "nt": stream.seek(0) msvcrt.locking(stream.fileno(), msvcrt.LK_UNLCK, 1) else: fcntl.flock(stream.fileno(), fcntl.LOCK_UN) stream.close() def load(self, handle): try: data = json.loads(self._path(handle).read_text(encoding="utf-8")) if not isinstance(data, dict) or data.get("version") != 1: raise ValueError return data except (OSError, ValueError, UnicodeError): raise StateError("Local state is missing or invalid") from None def save(self, handle, data): target = self._path(handle) temporary = self.directory / (handle + "." + secrets.token_hex(8) + ".tmp") try: encoded = json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode("utf-8") with os.fdopen(os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "wb") as out: out.write(encoded) out.flush() os.fsync(out.fileno()) os.replace(temporary, target) if os.name != "nt": os.chmod(target, 0o600) directory = os.open(self.directory, os.O_RDONLY) try: os.fsync(directory) finally: os.close(directory) except (OSError, ValueError, UnicodeError): raise StateError("Cannot save local state") from None finally: try: temporary.unlink(missing_ok=True) except OSError: pass