Files
drouter/_dev/run-static.py
T
liuzhuohua 2f8f09604b release: 1.0.8 — VPN 页把「模块没加载」误报成「内核不支持 WireGuard」
版本号 1.0.7 → 1.0.8(19 个文件 55 处,逐条过;历史陈述不动)。
顺带补上1.0.7 漏掉的更新日志小节(那一版只改了版本号)。

## 本轮修的缺陷

**先说结论:Debian 13 虚拟机上跑 drouter,KVM 不是容器,
内核里一直有 WireGuard —— 是面板自己判错了。**

原 _vpn_installed() 只看两件事:/sys/module/wireguard 在不在(模块当前
已加载)、/usr/bin/wg 在不在(工具已安装)。**从没查过 /lib/modules 下
有没有模块文件** → 「模块文件在但没加载」被压进「内核真没这个模块」。

而 Debian 走 udev 按需加载,/etc/modules 与 /etc/modules-load.d 默认都
不写 wireguard,模块在磁盘上但没进内存。本机实测:内核 6.12.107+deb13-amd64,
wireguard.ko.xz 在,未加载;wireguard-tools 也没装。

前端还配了一句写死的文案把它坐实:「通常说明跑在精简容器里」——
而这台是 PVE 上的 KVM 虚拟机。排查方向被这句话整个带偏。

修法:
- 判据扩到四态(ready / need_module / need_tool / unsupported),
  每态给可执行的下一步。unsupported 明确说装不了且不给一键修复。
- 模块文件名覆盖 .ko / .ko.xz / .ko.zst(Debian 默认压成 .ko.xz,
  只认 .ko 会把正常机器也判成 unsupported)
- 新增「一键修复」:modprobe → 写 /etc/modules-load.d/ 开机自启 →
  缺工具则 apt-get install,确认弹窗写清三步并告知不动网络
- apply 改为先自愈再报错,不再直接弹「内核不支持」把人挡在门外
- 老后端兜底 unknown 而非把旧的 no 猜成 unsupported(猜错=复活本 bug)
- 页面新增「运行环境」行;改掉「不需要装任何第三方软件」这句误导

## 测试基建:这轮抓出 3 条恒绿断言 + 1 处死分支

反向验证(把 bug 注入回去看它变红)逮到三条永远不会红的断言:
把探测路径改成 /nonexistent 仍全绿(docstring 里就写着那句话);
把 'env': _vpn_env() 注释掉仍全绿(我写的是 #注释,正则照样匹配);
把 if (!confirm( 改成 if (false && confirm( 仍全绿(提示文案里有那句话)。

根因:习惯在注释里引用被删掉的旧代码来解释 bug 来历,这些字面量让
「这段代码还在吗」类的判据永远成立。修法:t-107 统一走 py_code_only() /
js_code_only()(先按ast 判语义剥 docstring,再剥行注释),16 个注入全部被抓住。

死分支:用小锚点 Edit 只替换 if (d.installed === 'no') { 这一行时,
旧 body 与它的 } else if (!d.has_systemd) { 原样留在原地,拼出一个
「没有 systemd → 报内核不支持」的假分支——语法合法、静态检查也过。
防线:断言 } else if ( 的数量恰好等于分支数。

另有 4 处判据写错导致假红,共同形态是「脚本假设了一个修好之后就不成立
的前提」——比如「模块必须未加载」这条,fix 一跑完就假红。

## 验证

- t-107:126 → 167 项
- 反向验证 16/16 被抓住
- 新增 devtools/check-vpn-render.js 36 项(挂进 sync.sh 预检链)
- 新增 _dev/live-108-wg.py 真机验收 43 项;真机 _vpn_env 从 need_module
  修到 ready,wg 装上(v1.0.20210914),modules-load.d 已写入
- t-*.py 51 个全绿 / 静态检查器 39 个全绿 / 前端四查全绿 / md 0 死链
- 容器冒烟新增第 14 节:镜像里真跑 _vpn_env() 验四态判定
  (不 grep 字面量——注释里也会写这些词)
- deb 内 helper 与 app.js sha256 与本地逐字节一致
2026-10-03 12:28:21 +08:00

93 lines
3.3 KiB
Python
Raw 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.
"""drouter 静态回归驱动器(Windows 开发机用)。
为什么不用 shell 脚本跑:
这个环境里 bash 会话有 SIGTERM 风险,把整轮结果一起带走,
表现为「跑全量回归 → 命令莫名死掉」,看不出跑到第几个。
改成 Python逐个跑 + 每项前后写日志文件,崩了也能定位到具体那一项。
用法:
python _dev/run-static.py 跑全部
python _dev/run-static.py t-107 t-ca 只跑指定的
"""
import glob
import os
import re
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
# 需要真机 / 网络 / 交互 / 长时间压测的,不在静态回归范围内。
# ⚠️ 必须带 `t-` 前缀 —— ls 出来的名字就是 t-xxx。写裸名的话一个都
# 排除不掉,交互脚本照样跑,其中一个会把整个会话干掉。
EXCL = {
't-live', 't-smoke', 't-ulog-perf', 't-ulog-detail', 't-perf',
't-print', 't-cachebust', 't-cleanup', 't-webshell', 't-pty',
't-daemon-restart',
# ⚠️ 反向验证脚本**会真的改源文件**(注入 bug → 跑测试 → 还原)。
# 名字前缀也是 t-,会被 glob 自动收进来;一旦被自动执行,
# 它就会在回归过程中反复改写 backend/ 与 web/ —— 后果是
# 别的检查器读到被注入污染的代码,报出一堆假红,
# 更糟的是中途被打断会留下污染(已经发生过一次)。
# 只能手动跑:python _dev/t-107-inject-wg.py --batch 0
't-107-inject-wg',
}
def main():
args = sys.argv[1:]
if args:
names = [a if a.startswith('t-') else 't-' + a for a in args]
else:
names = sorted(os.path.basename(p)[:-3]
for p in glob.glob(os.path.join(HERE, 't-*.py')))
names = [n for n in names if n not in EXCL]
logpath = os.path.join(HERE, '.run-static.log')
log = open(logpath, 'w', encoding='utf-8')
def say(msg):
sys.stdout.write(msg + '\n')
sys.stdout.flush()
log.write(msg + '\n')
log.flush()
passed, failed = [], []
for n in names:
f = os.path.join(HERE, n + '.py')
if not os.path.isfile(f):
say(' %-20s 跳过(不存在)' % n)
continue
say(' %-20s ... ' % n)
try:
p = subprocess.run([sys.executable, f], capture_output=True,
text=True, encoding='utf-8', errors='replace',
timeout=180)
rc, out = p.returncode, (p.stdout or '')
except subprocess.TimeoutExpired:
rc, out = 124, 'TIMEOUT'
if rc == 0:
m = re.findall(r'通过 (\d+) 项', out)
say('ok%s' % ((' (%s 项)' % m[-1]) if m else ''))
passed.append(n)
else:
say('失败 rc=%s' % rc)
for line in out.splitlines():
if re.search(r'\[NG\]|Traceback|Error|✘', line):
say(' ' + line)
failed.append(n)
log.close()
print()
print('通过 %d 个检查器,失败 %d 个' % (len(passed), len(failed)))
if failed:
print('失败:%s' % ' '.join(failed))
return 1
print('(明细见 _dev/.run-static.log)')
return 0
if __name__ == '__main__':
sys.exit(main())