Files
ykxmz/_build/fix_os_import_and_pass.py
xmz7891 c97d7e8961 修「去密码硬编码」的第三批机械替换伤
上一轮的批量替换又留下四处伤,全都表现成"测试红了/文件坏了",
根子都在替换脚本不看上下文。

== 1. import os 插到了 __future__ 前面 ==

fix_password_round2.py 给文件补 `import os` 时,直接插在第一个
import 之前。但那个文件是:

    '''
    from __future__ import annotations
    import ast

被插成了 `import os` 在 `from __future__` 之前 ——
Python 要求 __future__ 必须是第一条语句,于是整个文件语法错:

    SyntaxError: from __future__ imports must occur at the beginning

受影响:_build/test_response_headers.py

== 2. 把中文说明塞进了字符串字面量 ==

同一个脚本里:
    t = t.replace(OLD, "(从 RELAY_ADMIN_PASSWORD 环境变量读)")

凡 `"@<老密码>"` 这种形状(@ 在引号里)都被替换成字符串:

    PASS = "@(从 RELAY_ADMIN_PASSWORD 环境变量读)"
    ADMIN_PASS = "@(从 RELAY_ADMIN_PASSWORD 环境变量读)"

本该是 `(os.environ.get("RELAY_ADMIN_PASSWORD") or "").strip()`。

受影响:_build/test_response_headers.py、
        _build/split_site_admin2.py

== 3. 测试自己起的中转拿不到密码(第二批漏了一个文件)==

test_response_headers.py 也是自己起本地中转,然后用 PASS 登录。
PASS 从环境变量取,本地跑时是空的 -> 登录失败 -> 401
-> 表现成「登录拿到 cookie(没拿到)」,看起来像登录功能坏了。

和上一批同样的修法:没设环境变量就**生成一个本次运行专用的随机密码**,
同时通过 env 和 data/admin_password.txt 告诉那台测试中转。

== 4. 模块级调用写在函数定义之前 ==

test_relay_admin_login.py 里 `PASSWORD = _admin_password()`
在模块级第 29 行,而 `def _admin_password()` 在第 64 行 ——
执行时 NameError(pyflakes 也报 undefined name)。
把这个赋值挪到 helper 定义之后。

== 5. 顺手清掉一个语法就是坏的死脚本 ==

_build/poll60_patch.py 第 49 行有个未闭合字符串,从来跑不起来,
也没人引用 —— 它是一次性补丁,改完就该删。留着只会让静态检查
一直报错。已 git rm(历史里还能查到内容)。

== 6. verify_all.ps1 的 Show-Fails 会崩 ==

    $lines | Select-Object -Last 1 | ForEach-Object {
        Write-Host "  $($_.Trim())"
    }

子进程的 stderr 被塞进输出流时,$_ 是 ErrorRecord,没有 .Trim()
方法 -> 抛 "does not contain a method named 'Trim'",
反而把真正的失败信息盖住了。改成先转字符串再 Trim。

== 验证 ==

    网页响应头      12/12 通过
    管理页设备控制  23/23 通过
    管理页批删      24/24 通过
    管理页按钮      10/10 通过
    管理页网络体检  15/15 通过
    密码随机生成    11/11 通过
    全仓语法检查    0 个问题

教训再记一笔:**机械替换必须替换完就验证**,
而且兜底的 str.replace 一定要慎用 —— 它不看上下文,
"函数调用进引号" 和 "中文说明进字符串" 都是这么来的。
2026-10-07 21:04:59 +08:00

141 lines
4.6 KiB
Python
Raw Permalink 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'''收尾:把「去密码硬编码」那一步留下的机械替换伤全部治好。
== 病灶 ==
fix_password_round2.py 里对 `os.environ` 的替换太粗:
t = t.replace(OLD, "(从 RELAY_ADMIN_PASSWORD 环境变量读)")
于是凡是 `"@<老密码>"` 这种形状(@ 在引号里)的地方,
都变成了一个**字符串字面量**:
PASS = "@(从 RELAY_ADMIN_PASSWORD 环境变量读)"
ADMIN_PASS = "@(从 RELAY_ADMIN_PASSWORD 环境变量读)"
它们本该是:
PASS = (os.environ.get("RELAY_ADMIN_PASSWORD") or "").strip()
另外我在给文件插 `import os` 时,插到了 `from __future__ import` 前面 ——
Python 要求 __future__ 必须是第一个语句,于是整个文件语法错。
== 这个脚本做三件事 ==
1. 把那两个被污染的赋值改成正确的 os.environ 读法
2. 确保每个用到 os.environ 的文件都**正确位置**导入了 os
3. 全仓复查:有没有「字符串里含中文提示但本该是表达式」的残留
'''
from __future__ import annotations
import ast
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
TARGETS = ["_build/test_response_headers.py", "_build/split_site_admin2.py"]
POLLUTED = re.compile(
r'(\b\w*PASS\w*)\s*=\s*"@?(从 RELAY_ADMIN_PASSWORD[^"]*"')
def ensure_import_os(text: str) -> str:
"""确保有 import os,且 __future__ 在最前面。"""
has_os = re.search(r"^import os\b", text, re.M) is not None
m = re.search(r"^from __future__ import .*$", text, re.M)
if m:
# 把 __future__ 提到所有 import 之前(如果它前面还有 import)
before = text[:m.start()]
# 前面的 import os 先摘掉
if re.search(r"^import os\s*$", before, re.M):
before = re.sub(r"^import os\s*\n", "", before, flags=re.M)
text = before + text[m.start():]
has_os = False
# 在 __future__ 之后插 import os
if not has_os:
m2 = re.search(r"^from __future__ import .*$", text, re.M)
insert_at = m2.end()
text = (text[:insert_at] + "\nimport os" + text[insert_at:])
return text
if not has_os:
# 没有 __future__:插在第一个 import 之前
m3 = re.search(r"^(?:import |from )", text, re.M)
if m3:
text = text[:m3.start()] + "import os\n" + text[m3.start():]
else:
text = "import os\n" + text
return text
def main() -> int:
changed = 0
for rel in TARGETS:
p = ROOT / rel
if not p.is_file():
print(" ✗ %s 不存在" % rel)
continue
text = p.read_text(encoding="utf-8")
before = text
# 1) 修被污染的赋值
text = POLLUTED.sub(
r'\1 = (os.environ.get("RELAY_ADMIN_PASSWORD") or "").strip()',
text)
# 2) 保证 import os 在正确位置
text = ensure_import_os(text)
if text == before:
print(" • %s 不需要改" % rel)
continue
# 3) 写完先自己 parse 一遍,语法错就别写
try:
ast.parse(text)
except SyntaxError as exc:
print(" ✗ %s 改完语法错(不写回):第 %s 行 %s"
% (rel, exc.lineno, exc.msg))
continue
p.write_text(text, encoding="utf-8", newline="\n")
print(" ✓ %s" % rel)
changed += 1
print()
print(" 改了 %d 个" % changed)
# 全仓复查
print()
print(" ── 全仓复查 ──")
problems = []
for pat in ("_tests/*.py", "_build/*.py", "relay/*.py"):
for f in ROOT.glob(pat):
try:
text = f.read_text(encoding="utf-8")
except Exception: # noqa: BLE001
continue
rel = str(f.relative_to(ROOT))
# 被污染的赋值
if POLLUTED.search(text):
problems.append("%s: 还有被污染的 PASS 赋值" % rel)
# 语法
try:
ast.parse(text)
except SyntaxError as exc:
if "__future__" in (exc.msg or ""):
problems.append("%s: __future__ 位置错" % rel)
else:
problems.append("%s: 第 %s 行 %s"
% (rel, exc.lineno, exc.msg))
if problems:
for x in problems:
print(" ⚠ %s" % x)
return 1
print(" ✓ 没有残留")
return 0
if __name__ == "__main__":
sys.exit(main())