Skip to content

Latest commit

 

History

History
221 lines (167 loc) · 11.2 KB

File metadata and controls

221 lines (167 loc) · 11.2 KB

开发

环境

Rust stable(1.98 验证过)、Node 24(CI 用的也是 24)。

rustup component add clippy rustfmt
cd web-admin && npm ci && cd ..

没有第三步。 默认主题不需要 clone、不需要 Node:cargo buildweb-theme.pin 下载发布好 的主题包、校验 sha256、解到 target/theme/scripts/theme.sh 做的,build.rs 调它)。理由见 decisions.md

改主题:在主题仓库自己的检出里改,npm run dev 起 Vite,它把 /api 和 WebSocket 代理到 9911 的 hub。发版两步:主题仓库打 tag(CI 自动发布 theme.tar.gz + .sha256),hub 这边把 web-theme.pin 换成新的 <tag> <sha256>

想拿一个还没发布的主题构建 hub:把构建好的 dist/ + theme.json 放进 target/theme/, 再往 target/theme/.pin 写一行和 web-theme.pin 一样的内容,脚本就会跳过下载。

符号链接不行:rust-embed 的 debug 模式和 frontend.rsread_inside 一样,canonicalize 之后确认文件仍在声明目录内,跟出去的一律拒绝。症状是 / 返回 404 而 /admin 正常。

构建

# 后台要先构建,hub 会把它的 dist 嵌进二进制
cd web-admin && npm run build && cd ..
# 默认主题不用管:build.rs 按 web-theme.pin 取回来放好
cargo build --release

debug 构建时 rust-embed 从磁盘读两个 dist/web-admin/disttarget/theme/dist),所以开发期间改完后台不用重新 cargo build,重跑 npm run build 就行。release 才真正嵌入。

测试

cargo test
cargo clippy --all-targets
cargo fmt --all

覆盖重点:

位置 覆盖什么
src/db.rs 流量累加、重启与缩水对齐、日/月周期滚动、级联删除、排序完整性、prune
src/auth.rs 密码哈希、限流锁定的完整生命周期、cookie 往返、OAuth 回调往返、转发头
src/agent_ws.rs RPC 分发、每分钟落盘、探测结果归属、会话拆除竞态、header 鉴权
src/api.rs 公开视图过滤、快照缓存与两个受众、读权限、密钥不回读
src/frontend.rs 主题路径穿越与越界符号链接、主题短名的两道守卫
src/main.rs cookie 标志(--siteX-Forwarded-Proto)与明文告警、对外地址推导、静态路由、账单滚动、首次运行

写测试的原则:一段非平凡逻辑留一个能跑的检查就够,不要每个函数一个测试。优先测边界和不变量, 不测 getter。

断言必须能被证伪

一条在逻辑被改坏之后依然通过的断言是摆设。加断言之前先想清楚:什么样的代码改动会让它变红? 想不出来就别写。这个仓库踩过的几种:

  • 主键替你去重。 metricping_record 都是 INSERT OR REPLACE + 复合主键,同一秒写进去 的几条记录本来就会塌成一行。所以「五份上报只落一行」这种计数断言,不管每分钟落盘的门控在不在都是 绿的。要么让各条记录带上不同的键,要么改断言别的东西(落盘时间戳是不是对齐到整分钟)。
  • 容器替你去重。 「限流表不会无限增长」曾经断言 map.len() <= 去重后的地址数,而 HashMap 的键天生保证这一点,把清扫逻辑整个删掉照样绿。
  • 常量时间窗关不掉。 锁定窗口写死成 15 分钟,「过期自动解锁」这条分支在测试里永远够不着。 Throttle 的窗口因此是字段而不是常量,生产路径取 LOCKOUT,测试塞个几十毫秒的。
  • 重建出来的东西和缓存的一模一样。 验证快照缓存生效不能拿两次读取比相等——不走缓存重建一遍 字节也一样。得在两次读取之间悄悄改掉底层数据。
  • recv().await 等的是永远不来的消息。 断言通道已关闭要用 try_recv():真出了回归, recv().await 会把整个测试挂死,CI 上是超时不是报错,比不测还难查。

变异检查

确认一批测试到底护住了什么:把逻辑挨个改坏,看有没有测试变红。

# 例:让同 boot 的读数缩水时不再钳零,「总流量永不回退」应该立刻报警
sed -i 's/(rx - last_rx).max(0), (tx - last_tx).max(0)/rx - last_rx, tx - last_tx/' src/db.rs
cargo test        # 期望 db::tests::a_shrinking_reading... 变红
git checkout -- src/db.rs

批量跑有两个坑:改完源码要让 mtime 前进一秒(同一秒内连着改,cargo 会拿旧产物顶替,好测试 也显示成漏网),以及别用 git checkout 还原——没提交的改动会跟着一起没。存一份文件内容再 写回去。

本地跑起来

# hub
cargo run -- --listen 127.0.0.1:9911 --db /tmp/dev.db --themes /tmp/themes
# 记下打印出来的一次性密码

# 后台热更新(API 代理到 9911)
cd web-admin && npm run dev

后台开发服务器使用 /admin/。默认主题在它自己的检出里改,那边 npm run dev 的 Vite 同样把 /api 和 WebSocket 代理到 9911。

第三方主题不用加入 hub 仓库。构建后按下面的形状复制到 --themes 指向的目录,再到后台「主题」页 切换:

/tmp/themes/<short>/theme.json
/tmp/themes/<short>/dist/index.html
/tmp/themes/<short>/preview.png     # 可选,后台主题卡片上的缩略图

目录名必须等于 theme.jsonshort;短名只允许字母、数字、-_。完整接口契约见默认主题 仓库的 README。

theme.json 的字段全部必填,缺任一个主题就不会出现在列表里(hub 日志有一条 warn 说缺哪个)。 不想填源码地址写 "url": "",卡片自动不显示「源码」链接。

本地开发就用上面这个形状直接复制,改一次看一次;发布给别人装的时候打成 tar czf theme.tar.gz dist theme.json preview.png,对方在后台「主题」页上传即可。

想让用户能点更新按钮url 写成 https://github.com/<owner>/<repo>,仓库的 release 里放一个 名叫 theme.tar.gz 的资产,tag 写成 v<theme.json 里的 version>v 可有可无)。hub 比 tag 和已装 版本,相同就不下载。资产必须叫这个名字——hub 不取 release 里的第一个资产。

登录后建一个节点,拿到 token,在 agent 仓库 里跑一个指向本机 hub 的 agent:

cd /path/to/agent
cargo run -- --server http://127.0.0.1:9911 --token <token> --interval 1

http:// 只对回环地址放行,正好够本地调试。

端到端验证

改完核心逻辑后跑一遍这个:

H=http://127.0.0.1:9911

# 1. 未登录不能碰面板接口
curl -s -o /dev/null -w "%{http_code}\n" -X POST $H/api/nodes -d '{"name":"x"}'   # 期望 401

# 2. 无效 token 连不上
WSH=(-H 'Connection: Upgrade' -H 'Upgrade: websocket' \
     -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==')
curl -s -o /dev/null -w "%{http_code}\n" "${WSH[@]}" $H/api/agent/ws                        # 401
curl -s -o /dev/null -w "%{http_code}\n" "${WSH[@]}" -H 'Authorization: Bearer bad' $H/api/agent/ws  # 401
curl -s -o /dev/null -w "%{http_code}\n" "${WSH[@]}" -H "Authorization: Bearer $TOKEN" $H/api/agent/ws # 101

# 3. 公开页不含敏感字段
curl -s $H/api/nodes | python3 -c '
import sys,json; n=json.load(sys.stdin)["nodes"][0]
assert all(k not in n for k in ("ip","remark","hostname")), "泄露了敏感字段"
print("公开视图 OK")'

# 4. 数据口径对得上系统工具
cd /path/to/agent && cargo test crosscheck -- --nocapture
free -b | awk 'NR==2{printf "free used=%.2fG\n", $3/1073741824}'
df -B1 --output=size,used / | tail -1

# 5. 累计流量跨 hub 重启不回退(见 traffic.md 的完整步骤)

# 6. 备份能导出,也能原样恢复(C 是 cookie jar)
#    上传是分片的:?offset= 必须等于服务端已经收到的字节数,收满 total 的那一片返回最终结果
curl -s -b $C -o /tmp/b.db $H/api/db/backup && file /tmp/b.db        # SQLite 3.x database
curl -s -o /dev/null -w "%{http_code}\n" $H/api/db/backup            # 401,五条 /api/db/* 都是
S=$(stat -c%s /tmp/b.db)
curl -s -b $C -c $C -X POST --data-binary @/tmp/b.db \
     "$H/api/db/restore?offset=0&total=$S"                           # {"ok":true} + 新 cookie
head -c 200000 /dev/urandom > /tmp/junk
curl -s -b $C -X POST --data-binary @/tmp/junk \
     "$H/api/db/restore?offset=0&total=200000"                       # 400,不是这个 hub 的备份
curl -s -b $C -X POST --data-binary @/tmp/junk \
     "$H/api/db/restore?offset=99&total=200000"                      # 400,分片接不上
head -c 9000000 /dev/zero | curl -s -o /dev/null -w "%{http_code}\n" -b $C -X POST \
     --data-binary @- "$H/api/db/restore?offset=0&total=9000000"     # 413,单请求超 8 MiB

# 7. 主题能上传、当场生效、能删(拿主题仓库发布的 theme.tar.gz,或者把 target/theme/ 打一个)
S=$(stat -c%s /tmp/t.tar.gz)
curl -s -b $C -X POST --data-binary @/tmp/t.tar.gz "$H/api/themes?offset=0&total=$S"
curl -s -b $C $H/api/themes                                          # 列表里多一个
curl -s -b $C -X PUT -d '{"theme":"<short>"}' -H 'content-type: application/json' $H/api/settings
curl -s $H/ | head -c 200                                            # 公开页已经是新主题,hub 没重启
command ls -1a /tmp/themes                                           # 只有正式目录,无 .staging-*
curl -s -b $C -X DELETE $H/api/themes/<short>                        # 204,公开页回落内置主题

截图检查前端

装有 puppeteer 的 Chrome 时可以直接驱动:

CHROME=$(find / -path '*/chrome-linux64/chrome' -type f 2>/dev/null | head -1)
"$CHROME" --headless --disable-gpu --no-sandbox --hide-scrollbars \
  --window-size=1280,1400 --screenshot=out.png --virtual-time-budget=6000 \
  http://127.0.0.1:9911/

需要登录后的页面就用 puppeteer-core 写个几十行的脚本走一遍登录表单。图表用整页截图会拍成空白 ——ResponsiveContainer 在整页模式改视口后来不及重绘,要截图表就用视口截图。

发布

推一个 v* tag 触发 .github/workflows/release.yml:构建内置后台、按 web-theme.pin 取回并校验 默认主题,再构建 hub 的 musl 静态二进制。agent 和主题由各自仓库独立发布。

install.sh 只走 hub 的 /agent/{arch} 拿二进制。hub 那一端拼的是 src/main.rsAGENT_REPO 常量,前面可以再加上面板设置的 github_proxymain::release_url)。

几个容易踩的坑

  • 别用 pkill -f 停进程:它会匹配到正在跑这条命令的 shell 自己,把会话一起干掉。用 ss -lptn "sport = :9911" 找 PID 再 kill
  • shadcn 的组件(web-admin/src/components/ui/ 和主题的 src/components/ui/)是 CLI 生成的,不要手改。改样式改各自 src/index.css 的 CSS 变量
  • tsconfig 里不要加 baseUrl,TypeScript 6 里已废弃会直接报错。paths 单独用就行
  • erasableSyntaxOnly 开着,构造函数参数属性(constructor(public x: number))不能用
  • lucide-react v1 删掉了品牌图标,没有 Github 组件。登录页里是手写的内联 SVG