Files
WeChatDataAnalysis/src/wechat_decrypt_tool/runtime_settings.py
T

176 lines
4.7 KiB
Python

from __future__ import annotations
import json
import os
import re
from pathlib import Path
RUNTIME_SETTINGS_FILENAME = "runtime_settings.json"
BACKEND_PORT_KEY = "backend_port"
ENV_PORT_KEY = "WECHAT_TOOL_PORT"
ENV_FILE_KEY = "WECHAT_TOOL_ENV_FILE"
DEFAULT_ENV_FILENAME = ".env"
def _parse_port(value: object) -> int | None:
if value is None:
return None
try:
raw = str(value).strip()
except Exception:
return None
if not raw:
return None
try:
port = int(raw, 10)
except Exception:
return None
if port < 1 or port > 65535:
return None
return port
def get_runtime_settings_path() -> Path:
from .app_paths import get_output_dir
return get_output_dir() / RUNTIME_SETTINGS_FILENAME
def read_backend_port_setting() -> int | None:
path = get_runtime_settings_path()
try:
if not path.is_file():
return None
data = json.loads(path.read_text(encoding="utf-8") or "{}")
if not isinstance(data, dict):
return None
return _parse_port(data.get(BACKEND_PORT_KEY))
except Exception:
return None
def write_backend_port_setting(port: int | None) -> None:
path = get_runtime_settings_path()
safe_port = _parse_port(port)
try:
path.parent.mkdir(parents=True, exist_ok=True)
except Exception:
return
try:
data: dict = {}
if path.is_file():
try:
existing = json.loads(path.read_text(encoding="utf-8") or "{}")
if isinstance(existing, dict):
data = existing
except Exception:
data = {}
if safe_port is None:
data.pop(BACKEND_PORT_KEY, None)
else:
data[BACKEND_PORT_KEY] = safe_port
# Keep the file small and stable; remove if empty.
if not data:
try:
path.unlink(missing_ok=True)
except Exception:
pass
return
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
except Exception:
return
def read_effective_backend_port(default: int) -> tuple[int, str]:
"""Return (port, source) where source is one of: env | settings | default."""
env_raw = str(os.environ.get("WECHAT_TOOL_PORT", "") or "").strip()
env_port = _parse_port(env_raw)
if env_port is not None:
return env_port, "env"
settings_port = read_backend_port_setting()
if settings_port is not None:
return settings_port, "settings"
return int(default), "default"
def get_env_file_path() -> Path | None:
"""Best-effort env file path for `uv run` (defaults to repo root `.env`)."""
v = str(os.environ.get(ENV_FILE_KEY, "") or "").strip()
if v:
try:
return Path(v)
except Exception:
return None
cwd = Path.cwd()
# Heuristic: only write `.env` in a project root (avoid polluting random dirs).
try:
if (cwd / "pyproject.toml").is_file():
return cwd / DEFAULT_ENV_FILENAME
except Exception:
return None
return None
def _set_env_var_in_file(env_file: Path, key: str, value: str | None) -> bool:
try:
env_file.parent.mkdir(parents=True, exist_ok=True)
except Exception:
return False
pattern = re.compile(rf"^\s*(?:export\s+)?{re.escape(key)}\s*=")
try:
raw = env_file.read_text(encoding="utf-8") if env_file.is_file() else ""
except Exception:
raw = ""
lines = raw.splitlines(keepends=True) if raw else []
out: list[str] = []
replaced = False
for line in lines:
if pattern.match(line):
if value is None:
continue
if not replaced:
out.append(f"{key}={value}\n")
replaced = True
continue
out.append(line)
if value is not None and not replaced:
if out and not out[-1].endswith("\n"):
out[-1] = out[-1] + "\n"
out.append(f"{key}={value}\n")
try:
env_file.write_text("".join(out), encoding="utf-8")
return True
except Exception:
return False
def write_backend_port_env_file(port: int | None) -> Path | None:
"""Write `WECHAT_TOOL_PORT` into a `.env` file so `uv run main.py` picks it up on restart.
Note: `uv` doesn't override already-set env vars; `.env` only applies when the variable is not
present in the current shell/session.
"""
env_file = get_env_file_path()
if not env_file:
return None
safe_port = _parse_port(port)
ok = _set_env_var_in_file(env_file, ENV_PORT_KEY, str(safe_port) if safe_port is not None else None)
return env_file if ok else None