| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """一次性修复: 观澜·如东样板 v2 启动器的 UTF-8 BOM 崩溃 + serve.json 中文乱码。
- 报错: guanlan.py cfg() -> json.loads(p.read_text(encoding="utf-8"))
- json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig)
- 根因 (install.ps1 第 4 步 "写配置", Windows PowerShell 5.1):
- 1) `Set-Content configs\\serve.json -Encoding UTF8` 写出的是 **带 BOM** 的 UTF-8;
- guanlan.py 用裸 utf-8 读 -> 直接 JSONDecodeError, 启动器连 check 都跑不了。
- 2) 紧随其前的 `Get-Content` 对无 BOM 的 UTF-8 按 ANSI 代码页 (中文机 = GBK) 解码,
- 把 serve.json 里的中文注释读成乱码再写回 -> 注释字段损坏 (部分字节已被替换成 '?', 不可逆)。
- 修复:
- A. guanlan.py : 所有 JSON 配置改走 jload() = utf-8-sig (兼容有/无 BOM); 配置损坏时打印提示并退回
- 内置默认端口, 不再抛栈 (hand-edit 配置写坏 BOM 也不该让产品起不来)。
- B. install.ps1 : 第 4 步改由 venv 里的 Python 读写配置 (读 utf-8-sig / 写无 BOM 的 UTF-8),
- 不再用 PS 的 cmdlet 碰 serve.json; 保持文件 UTF-8 带 BOM + CRLF 的既有约定。
- C. serve.json : 去掉 BOM, 还原两个 _note 中文注释 (依据原文件残留可逆部分 + 说明书 §7)。
- 幂等: 重复运行只报 "已修过"。原文件备份为 *.bak-bomfix。
- """
- from __future__ import annotations
- import json
- import pathlib
- import shutil
- import sys
- ROOT = pathlib.Path(r"F:\temp\guanlan-rudong-v2_0.2.0")
- GUANLAN = ROOT / "guanlan.py"
- INSTALL_PS1 = ROOT / "install.ps1"
- SERVE_JSON = ROOT / "configs" / "serve.json"
- BAK = ".bak-bomfix"
- report: list[str] = []
- def fail(msg: str) -> None:
- print(" [X] " + msg)
- sys.exit(1)
- def backup(p: pathlib.Path) -> None:
- b = p.with_name(p.name + BAK)
- if not b.exists():
- shutil.copy2(p, b)
- print(f" 备份 {p.name} -> {b.name}")
- def read_bytes(p: pathlib.Path) -> bytes:
- if not p.exists():
- fail(f"文件不存在: {p}")
- return p.read_bytes()
- def has_bom(b: bytes) -> bool:
- return b.startswith(b"\xef\xbb\xbf")
- # ---------------------------------------------------------------- A. guanlan.py
- def fix_guanlan() -> None:
- raw = read_bytes(GUANLAN)
- if has_bom(raw):
- fail("guanlan.py 竟然带 BOM, 先人工确认再修")
- src = raw.decode("utf-8") # 该文件是 UTF-8 无 BOM + LF
- if "def jload(" in src:
- print(" [=] guanlan.py 已修过 (存在 jload), 跳过")
- return
- backup(GUANLAN)
- anchor_cfg = (
- 'def cfg():\n'
- ' c = dict(DEFAULT); p = ROOT / "configs/serve.json"\n'
- ' if p.exists(): c.update(json.loads(p.read_text(encoding="utf-8")))\n'
- ' return c\n'
- )
- new_cfg = (
- 'def jload(p: Path):\n'
- ' """读 JSON 配置。必须用 utf-8-sig: 记事本和 PowerShell 5.1 的 `Set-Content -Encoding UTF8`\n'
- ' 写出的都是 **带 BOM** 的 UTF-8, 裸 utf-8 读会 JSONDecodeError (Unexpected UTF-8 BOM)。"""\n'
- ' return json.loads(Path(p).read_text(encoding="utf-8-sig"))\n'
- '\n'
- '\n'
- 'def cfg():\n'
- ' c = dict(DEFAULT); p = ROOT / "configs/serve.json"\n'
- ' if p.exists():\n'
- ' try: c.update(jload(p))\n'
- ' except Exception as ex: # 配置写坏不该让整个产品起不来: 退回内置默认端口, 但要说清楚\n'
- ' print(f" [X] {p} 读不了 ({ex.__class__.__name__}: {str(ex)[:100]}); 本次用内置默认端口。"\n'
- ' f" 重跑 install 脚本可重写该文件")\n'
- ' return c\n'
- )
- # (旧串, 新串, 期望出现次数)
- edits = [
- (anchor_cfg, new_cfg, 1),
- ('mcfg = json.loads((ROOT / "configs/models.json").read_text(encoding="utf-8"))',
- 'mcfg = jload(ROOT / "configs/models.json")', 1),
- ('pids = json.loads(PIDS.read_text(encoding="utf-8")) if PIDS.exists() else {}',
- 'pids = jload(PIDS) if PIDS.exists() else {}', 2),
- ('pids = json.loads(PIDS.read_text(encoding="utf-8"))', 'pids = jload(PIDS)', 1),
- ]
- for old, new, want in edits:
- got = src.count(old)
- if got != want:
- fail(f"guanlan.py 锚点命中 {got} 次 (期望 {want}), 未改动: {old[:60]!r}")
- src = src.replace(old, new)
- left = src.count('read_text(encoding="utf-8")') # 修完不该再有裸 utf-8 的 JSON 读
- if left:
- fail(f"guanlan.py 仍有 {left} 处裸 utf-8 读 JSON")
- compile(src, str(GUANLAN), "exec") # 语法自检, 不过就不落盘
- GUANLAN.write_bytes(src.encode("utf-8"))
- report.append("guanlan.py: JSON 配置改走 utf-8-sig; 配置损坏时给提示不抛栈")
- print(" [OK] guanlan.py 已修")
- # --------------------------------------------------------------- B. install.ps1
- NEW_STEP4 = '''Say "== 4/5 写配置"
- # 别改回 Get-Content / Set-Content -Encoding UTF8 (2026-09-10 实机踩过):
- # PS 5.1 的 Get-Content 把无 BOM 的 UTF-8 当 ANSI(GBK) 解码 -> 中文注释变乱码;
- # Set-Content -Encoding UTF8 又写出 BOM -> Python 侧用裸 utf-8 json.loads 直接 JSONDecodeError。
- # 交给 venv 里的 Python 读写: 读 utf-8-sig (有 BOM 也认), 写无 BOM 的 UTF-8。
- # 下面 Python 代码故意只用单引号, 避开 PS 5.1 传参时的引号转义坑。
- $cfgFix = @'
- import json, pathlib, sys
- p = pathlib.Path('configs/serve.json')
- c = json.loads(p.read_text(encoding='utf-8-sig'))
- c['python'] = sys.executable
- p.write_text(json.dumps(c, ensure_ascii=False, indent=2) + '\\n', encoding='utf-8')
- print(' configs/serve.json -> ' + sys.executable)
- '@
- & $vpy -c $cfgFix
- if ($LASTEXITCODE -ne 0) { Say " X 写 configs\\serve.json 失败"; exit 4 }
- '''
- OLD_STEP4 = (
- 'Say "== 4/5 写配置"\n'
- '$cfg = Get-Content configs\\serve.json -Raw | ConvertFrom-Json\n'
- '$cfg.python = $vpy\n'
- '$cfg | ConvertTo-Json -Depth 5 | Set-Content configs\\serve.json -Encoding UTF8\n'
- )
- def fix_install_ps1() -> None:
- raw = read_bytes(INSTALL_PS1)
- if not has_bom(raw):
- fail("install.ps1 丢了 BOM —— 文件头注释要求 UTF-8 带 BOM + CRLF, 先人工确认")
- text = raw.decode("utf-8-sig").replace("\r\n", "\n")
- if "cfgFix" in text:
- print(" [=] install.ps1 已修过 (存在 cfgFix), 跳过")
- return
- backup(INSTALL_PS1)
- if text.count(OLD_STEP4) != 1:
- fail(f"install.ps1 第 4 步锚点命中 {text.count(OLD_STEP4)} 次, 未改动")
- text = text.replace(OLD_STEP4, NEW_STEP4.replace("\n", "\n"))
- # 恢复该文件的编码约定: UTF-8 带 BOM + CRLF
- out = text.replace("\r\n", "\n").replace("\n", "\r\n")
- INSTALL_PS1.write_bytes(out.encode("utf-8-sig"))
- report.append("install.ps1: 第 4 步改由 venv Python 写配置 (utf-8-sig 读 / 无 BOM 写)")
- print(" [OK] install.ps1 已修 (保持 UTF-8 BOM + CRLF)")
- # ------------------------------------------------------ C. configs/serve.json
- NOTE = ("端口只在这里改 (网关路由表 scripts/guanlan_gateway.py 里的上游端口须同步; "
- "v0.1 仍是硬编码, 见说明书 §7)")
- NOTE_RAW = (r"原始 SCADA/技术资料目录 (相对本目录或绝对路径, 如 D:\guanlan\data\raw); "
- r"启动器导出为 WINDSCADA_RUDONG_SRC; 原始件不随包分发")
- def fix_serve_json() -> None:
- raw = read_bytes(SERVE_JSON)
- was_bom = has_bom(raw)
- try:
- cfg = json.loads(raw.decode("utf-8-sig"))
- except Exception as ex:
- fail(f"serve.json 内容已经是坏 JSON ({ex}); 从 configs\\serve.json{BAK} 或重跑 install 恢复")
- if not isinstance(cfg, dict):
- fail("serve.json 不是对象")
- if not was_bom and cfg.get("_note") == NOTE and cfg.get("_note_raw") == NOTE_RAW:
- print(" [=] serve.json 已修过 (无 BOM 且注释正常), 跳过")
- return
- backup(SERVE_JSON)
- cfg["_note"] = NOTE
- cfg["_note_raw"] = NOTE_RAW
- SERVE_JSON.write_text(json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
- report.append("serve.json: 去掉 BOM, 还原 _note/_note_raw 中文注释")
- print(f" [OK] serve.json 已修 (原 BOM={was_bom}, python={cfg.get('python')})")
- def verify() -> None:
- print("\n== 校验")
- # 1) serve.json: 无 BOM, 且裸 utf-8 也能解析 (即 guanlan.py 老写法也不会再炸)
- b = SERVE_JSON.read_bytes()
- print(f" serve.json BOM={has_bom(b)} bytes={len(b)}")
- if has_bom(b):
- fail("serve.json 仍带 BOM")
- cfg = json.loads(b.decode("utf-8"))
- for k in ("_note", "_note_raw"):
- if "\ufffd" in cfg[k] or "?" in cfg[k]:
- fail(f"{k} 仍有乱码: {cfg[k]!r}")
- if not cfg.get("python", "").lower().endswith(r".venv\scripts\python.exe"):
- fail(f"python 路径不对: {cfg.get('python')!r}")
- print(f" python -> {cfg['python']}")
- print(f" _note : {cfg['_note']}")
- print(f" _note_raw : {cfg['_note_raw']}")
- # 2) guanlan.py: 编译 + 现场跑一遍 cfg()
- src = GUANLAN.read_bytes().decode("utf-8")
- compile(src, str(GUANLAN), "exec")
- ns: dict = {"__name__": "not_main", "__file__": str(GUANLAN)}
- exec(compile(src, str(GUANLAN), "exec"), ns)
- c = ns["cfg"]()
- print(f" guanlan.cfg() -> gateway={c['gateway']} python={c['python']} (无异常)")
- # 3) install.ps1: 字节约定
- pb = INSTALL_PS1.read_bytes()
- txt = pb.decode("utf-8-sig")
- crlf, lf = txt.count("\r\n"), txt.count("\n")
- print(f" install.ps1 BOM={has_bom(pb)} CRLF={crlf} LF-only={lf - crlf}")
- if not has_bom(pb) or crlf != lf:
- fail("install.ps1 不再是 UTF-8 BOM + 全 CRLF")
- def main() -> int:
- print(f"目标: {ROOT}")
- for p in (GUANLAN, INSTALL_PS1, SERVE_JSON):
- if not p.exists():
- fail(f"缺文件 {p}")
- print("\n== A guanlan.py"); fix_guanlan()
- print("\n== B install.ps1"); fix_install_ps1()
- print("\n== C configs/serve.json"); fix_serve_json()
- verify()
- print("\n完成:")
- for r in report:
- print(" - " + r)
- if not report:
- print(" (无需改动, 之前已修)")
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|