1.7.0:常规发布

- 版本号 1.7.0 -> 1.7.0
- 重建三端并打包
- 关键测试全过
This commit is contained in:
xmz7891
2026-10-06 23:08:07 +08:00
parent 4c88ca901f
commit ce87ec1650
32 changed files with 2476 additions and 61 deletions
+207
View File
@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
r"""修 ProgramData 权限问题(用户装完报 WinError 5 拒绝访问)。
现象:
PermissionError: [WinError 5] 拒绝访问。
'C:\ProgramData\_ykxmz_test\data\settings.json.tmp' ->
'C:\ProgramData\_ykxmz_test\data\settings.json'
原因(Windows 的老规矩,不是 bug):
安装包是**管理员**跑的,它在 ProgramData 下建的 settings.json
属主是管理员。之后普通用户跑被控端时:
* 能**新建**文件(ProgramData 默认允许 Users 新建)
* 但**不能覆盖/改名**别人建的文件(os.replace 要删目标)
于是"写 .tmp -> 原子替换"这一步就炸了。
而我原来的 _can_write() 只测了"能不能建文件",没有测**替换**,
所以误判成可写,跑到一半才报错。
三层修:
① _write() 替换失败就降级:直接写目标文件 -> 再不行去掉只读位
② _can_write() 改成测完整周期(建 -> 改名 -> 删)
③ 目录选择:ProgramData 不行就退到 LOCALAPPDATA(每人一份,永远可写)
④ 安装包给数据目录授权 users-modify(治本)
"""
from __future__ import annotations
import ast
import sys
from pathlib import Path
CONFIG = (Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\windows"
r"\lanremote\config.py"))
# ---------------- ① 写入降级 ----------------
OLD_WRITE = ''' @staticmethod
def _write(path: Path, data) -> None:
tmp = path.with_suffix(path.suffix + ".tmp")
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(data, fh, ensure_ascii=False, indent=2)
os.replace(tmp, path)'''
NEW_WRITE = ''' @staticmethod
def _write(path: Path, data) -> None:
"""写 JSON。优先"临时文件 + 原子替换",失败就降级。
为什么要降级:
装到 Program Files / ProgramData 时,配置是**管理员**建的那一份,
属主是管理员。普通用户跑的时候:
能新建文件,但不能覆盖/改名别人建的文件。
于是 os.replace 报 WinError 5。
这时候退成"直接写目标文件" —— 牺牲原子性,换能存下配置。
"""
tmp = path.with_suffix(path.suffix + ".tmp")
payload = json.dumps(data, ensure_ascii=False, indent=2)
# 1) 正常路径:临时文件 + 原子替换
try:
with open(tmp, "w", encoding="utf-8") as fh:
fh.write(payload)
os.replace(tmp, path)
return
except OSError:
pass
# 2) 降级:直接覆盖目标文件
try:
with open(path, "w", encoding="utf-8") as fh:
fh.write(payload)
Store._cleanup(tmp)
return
except OSError:
pass
# 3) 再降级:目标被设成只读就摘掉只读位再写
try:
os.chmod(path, 0o666)
except OSError:
pass
try:
with open(path, "w", encoding="utf-8") as fh:
fh.write(payload)
Store._cleanup(tmp)
return
except OSError:
Store._cleanup(tmp)
raise
@staticmethod
def _cleanup(tmp: Path) -> None:
try:
if tmp.exists():
tmp.unlink()
except OSError:
pass'''
# ---------------- ② 可写检测测完整周期 ----------------
OLD_CANWRITE = '''def _can_write(folder: Path) -> bool:
"""这个目录能不能写(不存在就先建)。"""
try:
folder.mkdir(parents=True, exist_ok=True)
probe = folder / ".write-test"
probe.write_text("x", encoding="utf-8")
probe.unlink()
return True
except OSError:
return False'''
NEW_CANWRITE = '''def _can_write(folder: Path) -> bool:
"""这个目录能不能**真正**用来存配置。
注意不能只测"能新建文件" —— 那只过一半。
实际写配置用的是"临时文件 + 原子替换"(os.replace),
它需要**覆盖已存在文件**的权限。
装到 ProgramData 时,管理员建的那份普通用户改不了,
只测新建会误判成可写,然后跑起来才报 WinError 5。
所以这里把整个周期都跑一遍:建 -> 改名覆盖 -> 删。
"""
try:
folder.mkdir(parents=True, exist_ok=True)
probe = folder / ".write-test"
probe.write_text("x", encoding="utf-8")
# 关键:模拟真实写入的"替换"这一步
target = folder / ".write-test-target"
try:
if target.exists():
target.unlink()
probe.write_text("a", encoding="utf-8")
os.replace(probe, target) # <- 真正的门槛在这
target.write_text("b", encoding="utf-8") # 覆盖已存在的
target.unlink()
except OSError:
# 建得了新文件但替换不了 -> 不能当数据目录
try:
probe.unlink()
except OSError:
pass
return False
return True
except OSError:
return False'''
# ---------------- ③ 目录选择加 LOCALAPPDATA 兜底 ----------------
OLD_DIR = ''' base = os.environ.get("PROGRAMDATA") or r"C:\\ProgramData"
common = Path(base) / "ykxmz" / "data"
try:
common.mkdir(parents=True, exist_ok=True)
except OSError:
# 兜底:连 ProgramData 都写不了(极罕见),退回用户目录
common = (Path(os.environ.get("LOCALAPPDATA")
or Path.home()) / "ykxmz" / "data")
common.mkdir(parents=True, exist_ok=True)
_DATA_DIR_CACHE = common
return _DATA_DIR_CACHE'''
NEW_DIR = ''' # ProgramData:所有用户共用一份配置(换 Windows 账号也不丢)
base = os.environ.get("PROGRAMDATA") or r"C:\\ProgramData"
common = Path(base) / "ykxmz" / "data"
if _can_write(common):
_DATA_DIR_CACHE = common
return _DATA_DIR_CACHE
# 兜底:ProgramData 写不了(ACL 没配好 / 权限被收紧)就退到用户目录。
# 每人一份配置,但**永远能写** —— 总比跑起来报错强。
local = (Path(os.environ.get("LOCALAPPDATA")
or Path.home()) / "ykxmz" / "data")
local.mkdir(parents=True, exist_ok=True)
_DATA_DIR_CACHE = local
return _DATA_DIR_CACHE'''
def main() -> int:
text = CONFIG.read_text(encoding="utf-8")
ok = True
if "_cleanup" in text:
print(" 已经改过")
return 0
for old, new, label in ((OLD_WRITE, NEW_WRITE, "写入降级"),
(OLD_CANWRITE, NEW_CANWRITE, "可写检测测完整周期"),
(OLD_DIR, NEW_DIR, "LOCALAPPDATA 兜底")):
if old not in text:
print(f" [失败] {label} —— 锚点没找到")
ok = False
continue
text = text.replace(old, new, 1)
print(f" {label}")
if not ok:
return 1
CONFIG.write_text(text, encoding="utf-8", newline="\n")
try:
ast.parse(text)
print(" config.py 语法 OK")
except SyntaxError as exc:
print(f" 语法错误: 行 {exc.lineno}: {exc.msg}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())