fix_guanlan_bom.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """一次性修复: 观澜·如东样板 v2 启动器的 UTF-8 BOM 崩溃 + serve.json 中文乱码。
  4. 报错: guanlan.py cfg() -> json.loads(p.read_text(encoding="utf-8"))
  5. json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig)
  6. 根因 (install.ps1 第 4 步 "写配置", Windows PowerShell 5.1):
  7. 1) `Set-Content configs\\serve.json -Encoding UTF8` 写出的是 **带 BOM** 的 UTF-8;
  8. guanlan.py 用裸 utf-8 读 -> 直接 JSONDecodeError, 启动器连 check 都跑不了。
  9. 2) 紧随其前的 `Get-Content` 对无 BOM 的 UTF-8 按 ANSI 代码页 (中文机 = GBK) 解码,
  10. 把 serve.json 里的中文注释读成乱码再写回 -> 注释字段损坏 (部分字节已被替换成 '?', 不可逆)。
  11. 修复:
  12. A. guanlan.py : 所有 JSON 配置改走 jload() = utf-8-sig (兼容有/无 BOM); 配置损坏时打印提示并退回
  13. 内置默认端口, 不再抛栈 (hand-edit 配置写坏 BOM 也不该让产品起不来)。
  14. B. install.ps1 : 第 4 步改由 venv 里的 Python 读写配置 (读 utf-8-sig / 写无 BOM 的 UTF-8),
  15. 不再用 PS 的 cmdlet 碰 serve.json; 保持文件 UTF-8 带 BOM + CRLF 的既有约定。
  16. C. serve.json : 去掉 BOM, 还原两个 _note 中文注释 (依据原文件残留可逆部分 + 说明书 §7)。
  17. 幂等: 重复运行只报 "已修过"。原文件备份为 *.bak-bomfix。
  18. """
  19. from __future__ import annotations
  20. import json
  21. import pathlib
  22. import shutil
  23. import sys
  24. ROOT = pathlib.Path(r"F:\temp\guanlan-rudong-v2_0.2.0")
  25. GUANLAN = ROOT / "guanlan.py"
  26. INSTALL_PS1 = ROOT / "install.ps1"
  27. SERVE_JSON = ROOT / "configs" / "serve.json"
  28. BAK = ".bak-bomfix"
  29. report: list[str] = []
  30. def fail(msg: str) -> None:
  31. print(" [X] " + msg)
  32. sys.exit(1)
  33. def backup(p: pathlib.Path) -> None:
  34. b = p.with_name(p.name + BAK)
  35. if not b.exists():
  36. shutil.copy2(p, b)
  37. print(f" 备份 {p.name} -> {b.name}")
  38. def read_bytes(p: pathlib.Path) -> bytes:
  39. if not p.exists():
  40. fail(f"文件不存在: {p}")
  41. return p.read_bytes()
  42. def has_bom(b: bytes) -> bool:
  43. return b.startswith(b"\xef\xbb\xbf")
  44. # ---------------------------------------------------------------- A. guanlan.py
  45. def fix_guanlan() -> None:
  46. raw = read_bytes(GUANLAN)
  47. if has_bom(raw):
  48. fail("guanlan.py 竟然带 BOM, 先人工确认再修")
  49. src = raw.decode("utf-8") # 该文件是 UTF-8 无 BOM + LF
  50. if "def jload(" in src:
  51. print(" [=] guanlan.py 已修过 (存在 jload), 跳过")
  52. return
  53. backup(GUANLAN)
  54. anchor_cfg = (
  55. 'def cfg():\n'
  56. ' c = dict(DEFAULT); p = ROOT / "configs/serve.json"\n'
  57. ' if p.exists(): c.update(json.loads(p.read_text(encoding="utf-8")))\n'
  58. ' return c\n'
  59. )
  60. new_cfg = (
  61. 'def jload(p: Path):\n'
  62. ' """读 JSON 配置。必须用 utf-8-sig: 记事本和 PowerShell 5.1 的 `Set-Content -Encoding UTF8`\n'
  63. ' 写出的都是 **带 BOM** 的 UTF-8, 裸 utf-8 读会 JSONDecodeError (Unexpected UTF-8 BOM)。"""\n'
  64. ' return json.loads(Path(p).read_text(encoding="utf-8-sig"))\n'
  65. '\n'
  66. '\n'
  67. 'def cfg():\n'
  68. ' c = dict(DEFAULT); p = ROOT / "configs/serve.json"\n'
  69. ' if p.exists():\n'
  70. ' try: c.update(jload(p))\n'
  71. ' except Exception as ex: # 配置写坏不该让整个产品起不来: 退回内置默认端口, 但要说清楚\n'
  72. ' print(f" [X] {p} 读不了 ({ex.__class__.__name__}: {str(ex)[:100]}); 本次用内置默认端口。"\n'
  73. ' f" 重跑 install 脚本可重写该文件")\n'
  74. ' return c\n'
  75. )
  76. # (旧串, 新串, 期望出现次数)
  77. edits = [
  78. (anchor_cfg, new_cfg, 1),
  79. ('mcfg = json.loads((ROOT / "configs/models.json").read_text(encoding="utf-8"))',
  80. 'mcfg = jload(ROOT / "configs/models.json")', 1),
  81. ('pids = json.loads(PIDS.read_text(encoding="utf-8")) if PIDS.exists() else {}',
  82. 'pids = jload(PIDS) if PIDS.exists() else {}', 2),
  83. ('pids = json.loads(PIDS.read_text(encoding="utf-8"))', 'pids = jload(PIDS)', 1),
  84. ]
  85. for old, new, want in edits:
  86. got = src.count(old)
  87. if got != want:
  88. fail(f"guanlan.py 锚点命中 {got} 次 (期望 {want}), 未改动: {old[:60]!r}")
  89. src = src.replace(old, new)
  90. left = src.count('read_text(encoding="utf-8")') # 修完不该再有裸 utf-8 的 JSON 读
  91. if left:
  92. fail(f"guanlan.py 仍有 {left} 处裸 utf-8 读 JSON")
  93. compile(src, str(GUANLAN), "exec") # 语法自检, 不过就不落盘
  94. GUANLAN.write_bytes(src.encode("utf-8"))
  95. report.append("guanlan.py: JSON 配置改走 utf-8-sig; 配置损坏时给提示不抛栈")
  96. print(" [OK] guanlan.py 已修")
  97. # --------------------------------------------------------------- B. install.ps1
  98. NEW_STEP4 = '''Say "== 4/5 写配置"
  99. # 别改回 Get-Content / Set-Content -Encoding UTF8 (2026-09-10 实机踩过):
  100. # PS 5.1 的 Get-Content 把无 BOM 的 UTF-8 当 ANSI(GBK) 解码 -> 中文注释变乱码;
  101. # Set-Content -Encoding UTF8 又写出 BOM -> Python 侧用裸 utf-8 json.loads 直接 JSONDecodeError。
  102. # 交给 venv 里的 Python 读写: 读 utf-8-sig (有 BOM 也认), 写无 BOM 的 UTF-8。
  103. # 下面 Python 代码故意只用单引号, 避开 PS 5.1 传参时的引号转义坑。
  104. $cfgFix = @'
  105. import json, pathlib, sys
  106. p = pathlib.Path('configs/serve.json')
  107. c = json.loads(p.read_text(encoding='utf-8-sig'))
  108. c['python'] = sys.executable
  109. p.write_text(json.dumps(c, ensure_ascii=False, indent=2) + '\\n', encoding='utf-8')
  110. print(' configs/serve.json -> ' + sys.executable)
  111. '@
  112. & $vpy -c $cfgFix
  113. if ($LASTEXITCODE -ne 0) { Say " X 写 configs\\serve.json 失败"; exit 4 }
  114. '''
  115. OLD_STEP4 = (
  116. 'Say "== 4/5 写配置"\n'
  117. '$cfg = Get-Content configs\\serve.json -Raw | ConvertFrom-Json\n'
  118. '$cfg.python = $vpy\n'
  119. '$cfg | ConvertTo-Json -Depth 5 | Set-Content configs\\serve.json -Encoding UTF8\n'
  120. )
  121. def fix_install_ps1() -> None:
  122. raw = read_bytes(INSTALL_PS1)
  123. if not has_bom(raw):
  124. fail("install.ps1 丢了 BOM —— 文件头注释要求 UTF-8 带 BOM + CRLF, 先人工确认")
  125. text = raw.decode("utf-8-sig").replace("\r\n", "\n")
  126. if "cfgFix" in text:
  127. print(" [=] install.ps1 已修过 (存在 cfgFix), 跳过")
  128. return
  129. backup(INSTALL_PS1)
  130. if text.count(OLD_STEP4) != 1:
  131. fail(f"install.ps1 第 4 步锚点命中 {text.count(OLD_STEP4)} 次, 未改动")
  132. text = text.replace(OLD_STEP4, NEW_STEP4.replace("\n", "\n"))
  133. # 恢复该文件的编码约定: UTF-8 带 BOM + CRLF
  134. out = text.replace("\r\n", "\n").replace("\n", "\r\n")
  135. INSTALL_PS1.write_bytes(out.encode("utf-8-sig"))
  136. report.append("install.ps1: 第 4 步改由 venv Python 写配置 (utf-8-sig 读 / 无 BOM 写)")
  137. print(" [OK] install.ps1 已修 (保持 UTF-8 BOM + CRLF)")
  138. # ------------------------------------------------------ C. configs/serve.json
  139. NOTE = ("端口只在这里改 (网关路由表 scripts/guanlan_gateway.py 里的上游端口须同步; "
  140. "v0.1 仍是硬编码, 见说明书 §7)")
  141. NOTE_RAW = (r"原始 SCADA/技术资料目录 (相对本目录或绝对路径, 如 D:\guanlan\data\raw); "
  142. r"启动器导出为 WINDSCADA_RUDONG_SRC; 原始件不随包分发")
  143. def fix_serve_json() -> None:
  144. raw = read_bytes(SERVE_JSON)
  145. was_bom = has_bom(raw)
  146. try:
  147. cfg = json.loads(raw.decode("utf-8-sig"))
  148. except Exception as ex:
  149. fail(f"serve.json 内容已经是坏 JSON ({ex}); 从 configs\\serve.json{BAK} 或重跑 install 恢复")
  150. if not isinstance(cfg, dict):
  151. fail("serve.json 不是对象")
  152. if not was_bom and cfg.get("_note") == NOTE and cfg.get("_note_raw") == NOTE_RAW:
  153. print(" [=] serve.json 已修过 (无 BOM 且注释正常), 跳过")
  154. return
  155. backup(SERVE_JSON)
  156. cfg["_note"] = NOTE
  157. cfg["_note_raw"] = NOTE_RAW
  158. SERVE_JSON.write_text(json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
  159. report.append("serve.json: 去掉 BOM, 还原 _note/_note_raw 中文注释")
  160. print(f" [OK] serve.json 已修 (原 BOM={was_bom}, python={cfg.get('python')})")
  161. def verify() -> None:
  162. print("\n== 校验")
  163. # 1) serve.json: 无 BOM, 且裸 utf-8 也能解析 (即 guanlan.py 老写法也不会再炸)
  164. b = SERVE_JSON.read_bytes()
  165. print(f" serve.json BOM={has_bom(b)} bytes={len(b)}")
  166. if has_bom(b):
  167. fail("serve.json 仍带 BOM")
  168. cfg = json.loads(b.decode("utf-8"))
  169. for k in ("_note", "_note_raw"):
  170. if "\ufffd" in cfg[k] or "?" in cfg[k]:
  171. fail(f"{k} 仍有乱码: {cfg[k]!r}")
  172. if not cfg.get("python", "").lower().endswith(r".venv\scripts\python.exe"):
  173. fail(f"python 路径不对: {cfg.get('python')!r}")
  174. print(f" python -> {cfg['python']}")
  175. print(f" _note : {cfg['_note']}")
  176. print(f" _note_raw : {cfg['_note_raw']}")
  177. # 2) guanlan.py: 编译 + 现场跑一遍 cfg()
  178. src = GUANLAN.read_bytes().decode("utf-8")
  179. compile(src, str(GUANLAN), "exec")
  180. ns: dict = {"__name__": "not_main", "__file__": str(GUANLAN)}
  181. exec(compile(src, str(GUANLAN), "exec"), ns)
  182. c = ns["cfg"]()
  183. print(f" guanlan.cfg() -> gateway={c['gateway']} python={c['python']} (无异常)")
  184. # 3) install.ps1: 字节约定
  185. pb = INSTALL_PS1.read_bytes()
  186. txt = pb.decode("utf-8-sig")
  187. crlf, lf = txt.count("\r\n"), txt.count("\n")
  188. print(f" install.ps1 BOM={has_bom(pb)} CRLF={crlf} LF-only={lf - crlf}")
  189. if not has_bom(pb) or crlf != lf:
  190. fail("install.ps1 不再是 UTF-8 BOM + 全 CRLF")
  191. def main() -> int:
  192. print(f"目标: {ROOT}")
  193. for p in (GUANLAN, INSTALL_PS1, SERVE_JSON):
  194. if not p.exists():
  195. fail(f"缺文件 {p}")
  196. print("\n== A guanlan.py"); fix_guanlan()
  197. print("\n== B install.ps1"); fix_install_ps1()
  198. print("\n== C configs/serve.json"); fix_serve_json()
  199. verify()
  200. print("\n完成:")
  201. for r in report:
  202. print(" - " + r)
  203. if not report:
  204. print(" (无需改动, 之前已修)")
  205. return 0
  206. if __name__ == "__main__":
  207. sys.exit(main())