Files
ykxmz/_build/fix_programdata_perm.py
T
xmz7891 b86eeb349b 1.7.0:常规发布
- 版本号 1.7.0 -> 1.7.0
- 重建三端并打包
- 关键测试全过
2026-10-06 22:49:14 +08:00

208 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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())