一套跑在 Debian 13 (trixie) 上的软路由管理系统,提供 43 个功能页面的 Web 管理面板:WAN 接入、PPPoE 多拨、DHCP/DNS、IPv6、防火墙、QoS 限速、 访问控制、DDNS、文件共享、Docker 管理等。 核心设计原则:不重新实现任何协议 PPPoE 交给 ppp,DHCP 交给 dnsmasq,IPv6 RA 交给 radvd,NAT 交给 nftables。 Web 面板只做三件事:渲染原生配置文件 → 语法预检 → 原子写入并 reload。 面板删掉后,系统仍是一台配置完好的 Debian 路由器。 安全默认值 · 安装后不接管网络:不改默认路由、不开 IP 转发、不启动 DHCP 服务 · 所有改网络的动作都需要用户在面板上亲手点「应用」 · 避开 5900 端口,不触碰 VNC / X 会话 · 配置写入前先做语法预检,失败则回滚 架构 · backend/ Python 3 标准库,零第三方依赖(9 个模块) · web/ 原生 JS/CSS,无构建步骤 · 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时回退 subprocess · 鉴权走 X-Token 请求头 打包 · packaging/build-deb.sh → drouter_1.0.0_all.deb(含 6 个 systemd 单元) · scripts/make-offline-deps.sh → 236 个依赖 .deb + 本地仓库索引 · packaging/build-offline-bundle.sh → 自包含离线包,目标机零外网可装 · packaging/build-docker.sh → 容器镜像(体验用) 测试 40 套单元测试 + 3 个前端检查脚本(菜单映射 / 离线渲染 / 手机适配), 全部挂在 devtools/sync.sh 的预检链里。 许可:MIT
185 lines
8.6 KiB
Python
185 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""性能回归(#85):钉死本轮修掉的性能问题,防止改回去。
|
||
|
||
这个测试专门盯三类「看起来没事、实际一直在浪费」的问题:
|
||
① 页面切换后定时器泄漏(切走还在轮询 → 后端被空打);
|
||
② 高频接口里的 subprocess 滥用(每次切页面都要 fork 好几次);
|
||
③ 死代码分支(写了 quick 参数却两个分支一样)。
|
||
|
||
放这里而不是散在各模块测试里,是因为它们都是「跨模块的性能约定」,
|
||
改任何一个视图或读接口都可能破坏,需要一处集中守住。
|
||
"""
|
||
import io
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
HELPER = os.path.join(ROOT, 'backend', 'drouter-helper.py')
|
||
APP = os.path.join(ROOT, 'web', 'app.js')
|
||
|
||
PASS = FAIL = 0
|
||
|
||
|
||
def chk(name, cond, extra=''):
|
||
global PASS, FAIL
|
||
if cond:
|
||
PASS += 1
|
||
print('[OK] %s %s' % (name, extra))
|
||
else:
|
||
FAIL += 1
|
||
print('[FAIL] %s %s' % (name, extra))
|
||
|
||
|
||
HELPER_SRC = io.open(HELPER, encoding='utf-8').read()
|
||
APP_SRC = io.open(APP, encoding='utf-8').read()
|
||
HELPER_LINES = HELPER_SRC.split('\n')
|
||
APP_LINES = APP_SRC.split('\n')
|
||
|
||
|
||
def hseg(name):
|
||
"""取 helper 里某个顶层函数的函数体(按行切片)。"""
|
||
try:
|
||
i = next(k for k, l in enumerate(HELPER_LINES) if l.startswith('def %s(' % name))
|
||
except StopIteration:
|
||
return ''
|
||
j = next((k for k in range(i + 1, len(HELPER_LINES))
|
||
if HELPER_LINES[k].startswith(('def ', 'class ', '@'))), len(HELPER_LINES))
|
||
return '\n'.join(HELPER_LINES[i:j])
|
||
|
||
|
||
def strip_comment_lines(text):
|
||
"""去掉整行注释与行尾注释,只留可执行代码。
|
||
|
||
为什么需要:源码里保留「原来是 sh(['cat', '/proc/loadavg'])」这类**说明性
|
||
注释**是好事(后人知道为什么改),但如果断言直接对原始文本做 substring
|
||
匹配,就会被自己的注释骗过 —— 本轮 t-perf 已经踩过一次。
|
||
这里做一个够用的近似:逐行截断到第一个未出现在字符串字面量里的 '#'。
|
||
"""
|
||
out = []
|
||
for line in text.split('\n'):
|
||
q = None
|
||
cut = len(line)
|
||
for k, c in enumerate(line):
|
||
if q:
|
||
if c == '\\':
|
||
continue
|
||
if c == q:
|
||
q = None
|
||
elif c in ('"', "'"):
|
||
q = c
|
||
elif c == '#':
|
||
cut = k
|
||
break
|
||
out.append(line[:cut])
|
||
return '\n'.join(out)
|
||
|
||
|
||
def main():
|
||
print('--- ① 页面切换不得泄漏定时器(前端)---')
|
||
chk('定义了 stopPageTimers', 'function stopPageTimers(' in APP_SRC)
|
||
chk('go() 首件事就是停表',
|
||
re.search(r'function go\(k\) \{\s*\n\s*(//[^\n]*\n\s*)*stopPageTimers\(\);', APP_SRC) is not None)
|
||
# 所有模块级 interval 定时器变量都必须出现在 stopPageTimers 里
|
||
for v in ('DASH_TIMER', 'WANLOG_TIMER', 'FWL_TIMER', 'DK_TIMER'):
|
||
seg = APP_SRC[APP_SRC.index('function stopPageTimers('):]
|
||
seg = seg[:seg.index('\nfunction go(')]
|
||
chk('stopPageTimers 里清了 %s' % v, v in seg)
|
||
chk('stopPageTimers 调用了 pubip 的停止函数', 'pubipProbeStopPoll' in
|
||
APP_SRC[APP_SRC.index('function stopPageTimers('):APP_SRC.index('\nfunction go(')])
|
||
# 闭包内定时器通过全局钩子暴露
|
||
chk('声明了 stopLeaseTimer 钩子', re.search(r'let stopLeaseTimer\s*=', APP_SRC) is not None)
|
||
chk('声明了 stopV6Timer 钩子', re.search(r'let stopV6Timer\s*=', APP_SRC) is not None)
|
||
chk('viewDhcp 挂上了 stopLeaseTimer', 'stopLeaseTimer = () =>' in APP_SRC)
|
||
chk('viewV6Test 挂上了 stopV6Timer', 'stopV6Timer = () =>' in APP_SRC)
|
||
|
||
print('\n--- ② 轮询函数必须先判 DOM 再发请求 ---')
|
||
# liveWanLog:不能出现「先 api(...) 再 if (!box) return」
|
||
seg = APP_SRC[APP_SRC.index('async function liveWanLog('):]
|
||
seg = seg[:seg.index('\n}') + 2]
|
||
i_dom = seg.find("$('#pp-live')")
|
||
i_api = seg.find('api(')
|
||
chk('liveWanLog 判 DOM 在发请求之前', 0 <= i_dom < i_api,
|
||
'dom@%d api@%d' % (i_dom, i_api))
|
||
# loadLeases 同理
|
||
seg2 = APP_SRC[APP_SRC.index('const loadLeases = ()'):]
|
||
seg2 = seg2[:seg2.index('stopLeaseTimer =')]
|
||
chk('loadLeases 先判 #dh-leases 再请求',
|
||
seg2.find("if (!$('#dh-leases'))") < seg2.find('api('))
|
||
|
||
print('\n--- ③ 导航代次:旧请求不得覆盖新页面 ---')
|
||
chk('定义了 NAV_GEN', re.search(r'let NAV_GEN\s*=\s*0', APP_SRC) is not None)
|
||
chk('go() 里递增代次', re.search(r'function go\(k\) \{.*?NAV_GEN\+\+', APP_SRC, re.S) is not None)
|
||
chk('有 navStale 判断函数', 'function navStale(' in APP_SRC)
|
||
chk('渲染链路校验代次', 'if (navStale(gen)) return;' in APP_SRC)
|
||
|
||
print('\n--- ④ 高频接口不得滥用 subprocess ---')
|
||
seg_si = hseg('read_sysinfo')
|
||
# 断言只看「可执行代码」,先剥掉注释——否则解释性注释里写一句
|
||
# 「原来是 sh(['cat', '/proc/loadavg'])」就会把测试自己骗过(本轮已踩)。
|
||
code_si = strip_comment_lines(seg_si)
|
||
chk('read_sysinfo 不用 sh([\'hostname\'])', "sh(['hostname'])" not in code_si)
|
||
chk("read_sysinfo 不用 sh(['cat', '/proc/loadavg'])",
|
||
'loadavg' not in code_si or "sh(['cat'" not in code_si)
|
||
chk('read_sysinfo 用 socket.gethostname()', 'socket.gethostname()' in seg_si)
|
||
chk('read_sysinfo 用 os.uname()', 'os.uname()' in seg_si)
|
||
chk('read_sysinfo 改用 _read_disks()', '_read_disks()' in seg_si)
|
||
chk('read_sysinfo 不再直接调 df', "sh(['df'" not in code_si)
|
||
|
||
print('\n--- ⑤ _read_disks 实现正确性 ---')
|
||
seg_d = hseg('_read_disks')
|
||
chk('_read_disks 读 /proc/mounts', "'/proc/mounts'" in seg_d)
|
||
chk('_read_disks 用 statvfs', 'os.statvfs(' in seg_d)
|
||
chk('_read_disks 过滤 tmpfs/devtmpfs',
|
||
"'tmpfs'" in seg_d and "'devtmpfs'" in seg_d)
|
||
chk('_read_disks 输出与 df 同结构(含 mount/pct)',
|
||
"x['mount']" in seg_d and "'pct'" in seg_d)
|
||
chk('_read_disks 根分区排最前', "x['mount'] != '/'" in seg_d)
|
||
chk('_read_disks 注册在 read_sysinfo 之前',
|
||
HELPER_SRC.index('def _read_disks(') < HELPER_SRC.index('def read_sysinfo('))
|
||
|
||
print('\n--- ⑥ 死代码分支(quick 参数)---')
|
||
seg_m = strip_comment_lines(hseg('read_metrics'))
|
||
# 注意:不能只判 'if not quick else',因为合法写法 `x if quick else y` 也存在。
|
||
# 真正的「死代码」是**条件与两侧完全颠倒成同一结果**,即 `A if not quick else A`。
|
||
chk('read_metrics 不再出现「两个分支一样」的写法',
|
||
re.search(r'(\w[\w\.\(\)\[\]]*)\s+if\s+not\s+quick\s+else\s+\1\b', seg_m) is None)
|
||
# quick 仍应被读取(保留参数兼容),但不得造成同义分支
|
||
chk('quick 参数仍被读取(向后兼容)', "quick = bool(" in seg_m)
|
||
|
||
print('\n--- ⑦ 批量取服务状态(不再逐条 fork systemctl)---')
|
||
chk('定义了 _svc_states 批量函数', 'def _svc_states(' in HELPER_SRC)
|
||
seg_ss = hseg('_svc_states')
|
||
chk('_svc_states 用一次 systemctl show', "'show'" in seg_ss and "'--value'" in seg_ss)
|
||
chk('_svc_states 同时请求两个属性',
|
||
"'-p'" in seg_ss and 'ActiveState' in seg_ss and 'UnitFileState' in seg_ss)
|
||
chk('_svc_states 按属性个数分段切分',
|
||
'len(props)' in seg_ss)
|
||
seg_rs = strip_comment_lines(hseg('read_services'))
|
||
chk('read_services 不再循环调 systemctl is-active',
|
||
"'is-active'" not in seg_rs and "'is-enabled'" not in seg_rs)
|
||
chk('read_services 改用 _svc_states', '_svc_states(' in seg_rs)
|
||
# read_share / read_logd / read_docker 三处同样的逐条 fork 也要收口
|
||
for fn in ('read_share', 'read_logd', 'read_docker'):
|
||
seg = strip_comment_lines(hseg(fn))
|
||
chk('%s 不再逐条 systemctl is-active/is-enabled' % fn,
|
||
"'is-active'" not in seg and "'is-enabled'" not in seg
|
||
and '_svc_states(' in seg)
|
||
# which / command -v 也不该在循环里反复 fork
|
||
chk('read_ulog_conf 用 shutil.which(零 fork)',
|
||
'shutil.which(' in strip_comment_lines(hseg('read_ulog_conf')))
|
||
chk('_dep_installed 用 shutil.which',
|
||
'shutil.which(' in strip_comment_lines(hseg('_dep_installed')))
|
||
seg_dpi = strip_comment_lines(hseg('_dpi_installed'))
|
||
chk('_dpi_installed 不再循环 fork bash -lc',
|
||
"bash', '-lc'" not in seg_dpi)
|
||
|
||
print('\n' + '=' * 58)
|
||
print('通过 %d / 失败 %d' % (PASS, FAIL))
|
||
return 1 if FAIL else 0
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|