mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 19:30:42 +00:00
1.7.0:常规发布
- 版本号 1.7.0 -> 1.7.0 - 重建三端并打包 - 关键测试全过
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user