mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 17:10:43 +00:00
208 lines
7.3 KiB
Python
208 lines
7.3 KiB
Python
# -*- 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())
|