# -*- 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())