"""采购工具的本地运行目录策略。""" from __future__ import annotations from dataclasses import dataclass import os from pathlib import Path from typing import Any, Callable @dataclass(frozen=True) class RuntimePaths: """仅包含应用生成物的本地目录。 日志和证据产物不落在源码树中,避免把含有现场信息的运行数据误提交到 Git。 """ root: Path logs: Path artifacts: Path state: Path database: Path @classmethod def from_root(cls, root: Path) -> "RuntimePaths": # 路径在进程启动时一次性固化;之后 cwd 改变不能打开第二套数据库或绕过原 mutex。 resolved_root = root.expanduser().resolve(strict=False) state = resolved_root / "state" return cls( root=resolved_root, logs=resolved_root / "logs", artifacts=resolved_root / "artifacts", state=state, database=state / "client-state.sqlite3", ) @classmethod def default(cls) -> "RuntimePaths": local_app_data = os.environ.get("LOCALAPPDATA") if local_app_data: local_root = Path(local_app_data).expanduser() if not local_root.is_absolute(): raise RuntimeError("local_app_data_must_be_absolute") return cls.from_root(local_root / "cmbuyer") if os.name == "nt": # Windows 上回退到 home 会悄悄创建第二套状态库并绕开同一 mutex,必须失败闭合。 raise RuntimeError("local_app_data_required") return cls.from_root(Path.home() / ".local" / "share" / "cmbuyer") def ensure_exists(self) -> None: """创建运行目录;调用方负责向用户呈现无法创建目录的错误。""" self.logs.mkdir(parents=True, exist_ok=True) self.artifacts.mkdir(parents=True, exist_ok=True) self.state.mkdir(parents=True, exist_ok=True) @dataclass class LocalStateRuntime: """持有 named mutex 与本地状态库,保证 mutex 总是先取得。""" paths: RuntimePaths mutex: Any store: Any @classmethod def open( cls, paths: RuntimePaths | None = None, *, mutex_factory: Callable[[Path], Any] | None = None, protector_factory: Callable[[], Any] | None = None, store_factory: Callable[[Path, Any], Any] | None = None, ) -> "LocalStateRuntime": from .localstate.protection import DpapiProtector from .localstate.single_instance import NamedMutex from .localstate.store import LocalStateStore selected = paths or RuntimePaths.default() selected.ensure_exists() make_mutex = mutex_factory or NamedMutex make_protector = protector_factory or DpapiProtector make_store = store_factory or LocalStateStore mutex = make_mutex(selected.database) try: protector = make_protector() store = make_store(selected.database, protector) except Exception: mutex.close() raise return cls(selected, mutex, store) def close(self) -> None: self.mutex.close() def __enter__(self) -> "LocalStateRuntime": return self def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: self.close()