#!/usr/bin/env bash # ============================================================ # 定时重启功能的真实环境验收脚本 # # 用法(在 VPS 上,root): # cd /opt/vps-xray && bash tests/verify-restart-on-vps.sh # # 为什么必须在真机上跑:本地的 62→101 项单元测试都是把生成的文件写进临时目录 # 再比对内容——它们能验证「我们写出了预期的字节」,但验证不了「systemd 认不认 # 这些字节」。TimeZone= 那个缺陷就是这么漏过去的:文件里确实有那一行,断言通过, # 而 systemd 加载时只会记一条 Unknown key name 然后按系统本地时区解析。 # # 本脚本只做安全操作:不升级 Xray、不重写 config.json、不重启 xray。 # 会临时改动 .env 的重启字段与三个 systemd 单元文件,结束时恢复原状。 # ============================================================ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$SCRIPT_DIR" PASS=0; FAIL=0 ok() { PASS=$((PASS+1)); echo " ✅ $1"; } bad() { FAIL=$((FAIL+1)); echo " ❌ $1"; [ -n "${2:-}" ] && echo " 实际: $2"; } sec() { echo ""; echo "===== $* ====="; } [ "$(id -u)" -eq 0 ] || { echo "需要 root"; exit 1; } [ -f .env ] || { echo "找不到 .env,这台机器还没部署过"; exit 1; } ENV_BAK="$(mktemp)"; cp .env "$ENV_BAK" restore() { echo "" echo "===== 恢复原状 =====" cp "$ENV_BAK" .env && echo " .env 已还原" rm -f "$ENV_BAK" bash deploy.sh --restart >/dev/null 2>&1 && echo " timer 已按还原后的 .env 重建" \ || echo " timer 重建失败,请手工检查 systemctl list-timers xray-restart.timer" } trap restore EXIT sec "0. 环境" systemctl --version | head -1 timedatectl 2>/dev/null | grep -i "time zone" || cat /etc/timezone 2>/dev/null sec "1. Critical-1:OnCalendar 尾随时区是否被 systemd 真正接受" # 这是全套验证里最关键的一条。旧实现用 [Timer] 段的 TimeZone=, # 而 systemd.timer 根本没有这个 key——写了会被忽略并按系统本地时区解析。 if out=$(systemd-analyze calendar "*-*-* 04:00:00 Asia/Shanghai" 2>&1); then ok "systemd 接受带时区的日历表达式" echo "$out" | sed 's/^/ /' else bad "systemd 不接受带时区的日历表达式(该 systemd 版本可能低于 242)" "$out" fi sec "2. 启用:--restart 7 04:00" BEFORE_TS=$(systemctl show xray -p ActiveEnterTimestamp --value) bash deploy.sh --restart 7 04:00 AFTER_TS=$(systemctl show xray -p ActiveEnterTimestamp --value) [ "$BEFORE_TS" = "$AFTER_TS" ] \ && ok "xray 未被重启(ActiveEnterTimestamp 前后一致)" \ || bad "xray 被重启了!--restart 不该碰服务" "before=$BEFORE_TS after=$AFTER_TS" grep -q '^XRAY_RESTART_EVERY_DAYS=7$' .env && ok ".env 天数已写入" || bad ".env 天数未写入" "$(grep '^XRAY_RESTART_EVERY_DAYS=' .env)" grep -q '^XRAY_RESTART_TIME=04:00$' .env && ok ".env 时间已写入" || bad ".env 时间未写入" "$(grep '^XRAY_RESTART_TIME=' .env)" sec "3. 生成的 timer 是否被 systemd 正确加载" systemd-analyze verify /etc/systemd/system/xray-restart.timer 2>&1 | sed 's/^/ /' if journalctl -b --no-pager 2>/dev/null | grep -q "Unknown key name.*xray-restart"; then bad "journal 里有 Unknown key name 告警,说明 timer 里仍有非法指令" else ok "journal 无 Unknown key name 告警" fi CAL=$(systemctl show xray-restart.timer -p TimersCalendar --value 2>/dev/null) echo " TimersCalendar: $CAL" case "$CAL" in *Asia/Shanghai*) ok "解析后的日历规格里含时区" ;; *) bad "解析后的日历规格里没有时区——时区没生效" "$CAL" ;; esac grep -q '^TimeZone=' /etc/systemd/system/xray-restart.timer \ && bad "timer 里仍有非法的 TimeZone= 指令" \ || ok "timer 里无 TimeZone= 指令" sec "4. 守卫脚本行为" rm -f /var/lib/xray/last-restart echo " -- 首次(无状态文件)应执行重启 --" /usr/local/bin/xray-restart-guard 2>&1 | sed 's/^/ /' [ -s /var/lib/xray/last-restart ] && ok "时间戳已写入" || bad "时间戳未写入" echo " -- 第二次(刚重启过)应跳过 --" G2=$(/usr/local/bin/xray-restart-guard 2>&1); echo "$G2" | sed 's/^/ /' echo "$G2" | grep -q "跳过" && ok "正确跳过" || bad "未跳过" "$G2" echo " -- Important-4:时间戳落在未来时应视为 0 并执行重启 --" TS_BEFORE_FUTURE=$(systemctl show xray -p ActiveEnterTimestamp --value) echo "$(( $(date +%s) + 86400*365 ))" > /var/lib/xray/last-restart G3=$(/usr/local/bin/xray-restart-guard 2>&1); echo "$G3" | sed 's/^/ /' if echo "$G3" | grep -q "跳过"; then bad "未来时间戳导致永久跳过——保护未生效" else ok "未来时间戳被正确当成 0" fi echo " (这一步会真的重启一次 xray,是预期内的)" sleep 2 systemctl is-active --quiet xray && ok "重启后 xray 仍然存活" || bad "xray 未能重新启动!" sec "5. Important-6:下次实际重启时间是否被正确显示" echo "$(( $(date +%s) - 3*86400 ))" > /var/lib/xray/last-restart OUT=$(bash deploy.sh --restart 2>&1); echo "$OUT" | sed 's/^/ /' echo " (N=7、距上次 3 天,所以真实的下次重启应在 4 天后,而非次日)" sec "6. 关闭:--restart 0" bash deploy.sh --restart 0 >/dev/null 2>&1 for f in /etc/systemd/system/xray-restart.timer /etc/systemd/system/xray-restart.service /usr/local/bin/xray-restart-guard; do [ -f "$f" ] && bad "未删除: $f" || ok "已删除: $(basename "$f")" done [ -f /var/lib/xray/last-restart ] && ok "状态文件按设计保留" || bad "状态文件被误删(关闭≠卸载)" sec "7. Critical-2:前导零天数" bash deploy.sh --restart 09 >/dev/null 2>&1 D=$(grep '^XRAY_RESTART_EVERY_DAYS=' .env | cut -d= -f2) [ "$D" = "9" ] && ok "09 被归一化成 9(未被当八进制)" || bad "09 未被正确归一化" "$D" if [ -f /usr/local/bin/xray-restart-guard ]; then grep -q '^DAYS=9$' /usr/local/bin/xray-restart-guard \ && ok "guard 里 DAYS=9" || bad "guard 里天数不对" "$(grep '^DAYS=' /usr/local/bin/xray-restart-guard)" OUT=$(/usr/local/bin/xray-restart-guard 2>&1) echo "$OUT" | grep -qi "value too great\|error token" \ && bad "guard 仍有八进制算术错误" "$OUT" \ || ok "guard 无八进制算术错误" fi sec "8. 参数校验与互斥" bash deploy.sh --restart 7 25:00 >/dev/null 2>&1; [ $? -eq 1 ] && ok "非法时间退出码 1" || bad "非法时间退出码不是 1" bash deploy.sh --badarg >/dev/null 2>&1; [ $? -eq 1 ] && ok "未知参数退出码 1" || bad "未知参数退出码不是 1" bash deploy.sh --help >/dev/null 2>&1; [ $? -eq 0 ] && ok "--help 退出码 0" || bad "--help 退出码不是 0" # 注意:不能写成 `bash deploy.sh ... | grep -q ...`。本脚本开了 pipefail, # 而 deploy.sh 在这里本就该以退出码 1 结束,管道会因此被判为失败—— # 于是断言恒为「未生效」,与被测行为无关。必须先取输出再匹配。 MX_OUT=$(bash deploy.sh --restart 7 --redetect 2>&1 || true) echo "$MX_OUT" | grep -q "不能与" && ok "互斥校验生效" || bad "互斥校验未生效" "$MX_OUT" echo "" echo "============================================================" echo " 通过 ${PASS},失败 ${FAIL}" echo "============================================================" echo "" echo "还需要你手动做一步(本脚本刻意不做,因为它会重启服务、断掉所有连接):" echo "" echo " # 旧字段迁移验证 —— 建议挑一台当前没在用的机器" echo " cp .env /tmp/env.bak" echo " sed -i '/^XRAY_RESTART_/d' .env" echo " echo 'XRAY_DAILY_RESTART=true' >> .env" echo " bash deploy.sh # 完整部署,会重启 xray" echo " grep -E '^XRAY_RESTART_|^XRAY_DAILY' .env" echo " # 预期:出现 XRAY_RESTART_EVERY_DAYS=1,且 XRAY_DAILY_RESTART 已消失" echo " cp /tmp/env.bak .env && bash deploy.sh --restart" echo "" [ "$FAIL" -eq 0 ]