Files
oracle-jump-query-public/scripts/runtime_state.py
T

89 lines
3.2 KiB
Python

"""Local configuration snapshots and bounded, process-safe updates."""
import copy
import errno
import json
import os
import tempfile
import time
from contextlib import contextmanager
class ConfigSnapshot(dict):
def __init__(self, values):
super().__init__(values)
self.original = copy.deepcopy(values)
@contextmanager
def file_lock(path, timeout=45):
# An OS lock is released even if the owning process crashes. Do not delete
# the lock file: replacing its inode would break coordination on POSIX.
with open(path, "a+b") as handle:
handle.seek(0, os.SEEK_END)
if handle.tell() == 0:
handle.write(b"\0")
handle.flush()
deadline = time.monotonic() + timeout
while True:
try:
if os.name == "nt":
import msvcrt
handle.seek(0)
msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
else:
import fcntl
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except OSError as exc:
if exc.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
raise
if time.monotonic() >= deadline:
raise TimeoutError("等待本机登录/配置锁超过 %s 秒" % timeout)
time.sleep(0.05)
try:
yield
finally:
if os.name == "nt":
handle.seek(0)
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
else:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
def read_config(path):
with open(path, "r", encoding="utf-8-sig") as handle:
return ConfigSnapshot(json.load(handle))
def update_config(path, values, ignored=(), initialize=False):
"""Merge only changed fields into the latest file, then atomically replace it."""
original = getattr(values, "original", {})
changes = {key: value for key, value in values.items()
if key not in ignored and (key not in original or value != original[key])}
removed = {key for key in original if key not in values and key not in ignored}
with file_lock(path + ".lock"):
try:
latest = read_config(path)
if initialize:
return latest
except FileNotFoundError:
latest = {}
if not changes and not removed and os.path.exists(path):
return ConfigSnapshot(latest)
latest.update(changes)
for key in removed:
latest.pop(key, None)
fd, temporary = tempfile.mkstemp(prefix="config.json.", suffix=".tmp", dir=os.path.dirname(path))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(latest, handle, ensure_ascii=False, indent=2)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
if isinstance(values, ConfigSnapshot):
values.original = copy.deepcopy(dict(values))
return ConfigSnapshot(latest)