同步客户绑定与登录恢复优化
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user