版本号 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 与本地逐字节一致
93 lines
3.3 KiB
Python
93 lines
3.3 KiB
Python
"""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())
|