Files
server-deploy/vps-xray/tests/test-restart-schedule.sh
T
joywayer 3fd2c89020 fix(vps-xray): 修复定时重启的四类静默失效并补齐安全不变式测试
Critical-1 timer 时区:[Timer] 段没有 TimeZone= 这个 key,systemd 只会记一条
"Unknown key name" 然后按系统本地时区解析 OnCalendar。VPS 出厂多为 UTC,
用户设「04:00 Asia/Shanghai」会在北京时间 12:00 触发且毫无提示。
改为把时区写进日历表达式本身:OnCalendar=*-*-* HH:MM:00 <TZ>(systemd >= 242)。

Critical-2 前导零:^[0-9]+$ 放行 08/09/010,该值原样写进 guard 的 DAYS=,
而 guard 里是算术展开——010 被当八进制解析成 8 天,08/09 直接报
"value too great for base",使间隔判断求值为假,落进重启分支变成每天重启。
校验通过后统一 $((10#$days)) 归一化。

Important-3 状态文件写入失败:mkdir 与写时间戳都不检查返回值,/var 写满或
只读时写入失败却照样重启,「先写时间戳」的保护完全失效,退化成每天硬重启且
无限持续。改为 fail-safe:写不进去就报错并跳过本轮。
update_env_field 的三处写入同样补上返回值检查,调用方改在条件位置。

Important-4 未来时间戳:RTC 错乱或 NTP 大幅向后校正时会写下未来的时间戳,
时钟校回后 now-last 恒为负,此后永远跳过且无自愈路径。与「内容非法按 0 处理」
同策略,读到 last > now 时按 0 处理。

Important-5 静默中止:configure_restart_timer 被 harden_system、main 一路裸调用,
set -e 下 systemctl enable 失败会让部署在「8/8 加固」之后无声退出——
start_service / save_env / print_result 全不执行,config.json 已换、防火墙已改,
但 xray 没重启、.env 没更新、链接没打印。三处 systemctl 全部改到条件位置,
enable 不再吞 stderr(规格本就要求靠 systemctl 报错暴露非法时区)。

Important-6 NEXT 语义:list-timers 的 NEXT 是 timer 的「下次唤醒」,每天一次,
不是下次重启时间。新增 print_next_restart,--restart 输出里直接算出并显示
「下次实际重启时间」,README 同步说明差异。

Important-7 安全不变式测试:configure_restart_timer 与 apply_restart_only 原本
零覆盖,「不重启 xray、不碰 vless 链接」这个最重要的承诺恰恰没有回归保护。
补上启用/关闭两个方向的断言,以及 .env 逐字段 diff 与 systemctl 调用日志断言。

Minor-9 README 补充取舍:完整部署的 systemctl restart xray 不更新状态文件。
Minor-11 save_env 注释「这两项」→「这几项」。

测试 61 → 101 项,全部通过。
2026-08-07 09:37:46 +08:00

469 lines
18 KiB
Bash
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.
#!/usr/bin/env bash
# deploy.sh 中重启计划相关逻辑的单元测试
#
# 用法: bash vps-xray/tests/test-restart-schedule.sh
#
# 不需要 root,不接触真实 systemd:所有写入路径都被重定向到临时目录,
# systemctl 用 PATH 打桩拦截。
#
# 加载方式说明:deploy.sh 结尾会直接调用 main,且顶层有 root 检查与参数解析,
# 不能整份 source。这里只抽取「多行函数定义」(形如 `name() {` 开头、`}` 结尾)。
# 单行函数(log/warn/error/step)抽不到,正好由下面的桩覆盖。
set -u
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY="${TEST_DIR}/../deploy.sh"
if [ ! -f "$DEPLOY" ]; then
echo "找不到 $DEPLOY" >&2
exit 1
fi
eval "$(sed -n '/^[a-z_][a-z_]*() {$/,/^}$/p' "$DEPLOY")"
# 日志桩:error 保留输出便于断言,其余静音
log() { :; }
warn() { :; }
step() { :; }
error() { echo "ERROR: $*" >&2; }
PASS=0
FAIL=0
ok() { PASS=$((PASS + 1)); echo " ok - $1"; }
bad() { FAIL=$((FAIL + 1)); echo " FAIL - $1"; echo " 期望: $2"; echo " 实际: $3"; }
# is <描述> <实际值> <期望值>
is() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1" "$3" "$2"; fi; }
echo "== normalize_restart_time =="
is "04:00 原样通过" "$(normalize_restart_time 04:00)" "04:00"
is "4:00 补零" "$(normalize_restart_time 4:00)" "04:00"
is "08:30 不被当八进制" "$(normalize_restart_time 08:30)" "08:30"
is "23:59 边界" "$(normalize_restart_time 23:59)" "23:59"
is "00:00 边界" "$(normalize_restart_time 00:00)" "00:00"
normalize_restart_time "24:00" >/dev/null 2>&1
is "24:00 被拒" "$?" "1"
normalize_restart_time "04:60" >/dev/null 2>&1
is "04:60 被拒" "$?" "1"
normalize_restart_time "abc" >/dev/null 2>&1
is "abc 被拒" "$?" "1"
normalize_restart_time "" >/dev/null 2>&1
is "空串被拒" "$?" "1"
echo "== resolve_restart_config =="
reset_env() {
unset XRAY_RESTART_EVERY_DAYS XRAY_RESTART_TIME XRAY_RESTART_TIMEZONE XRAY_DAILY_RESTART
RESTART_EVERY_DAYS=""; RESTART_TIME=""; RESTART_TZ=""
}
reset_env
resolve_restart_config
is "全空时天数默认 0" "$RESTART_EVERY_DAYS" "0"
is "全空时时间默认" "$RESTART_TIME" "04:00"
is "全空时时区默认" "$RESTART_TZ" "Asia/Shanghai"
reset_env; XRAY_RESTART_EVERY_DAYS=7; XRAY_RESTART_TIME=5:30
resolve_restart_config
is "读取新字段天数" "$RESTART_EVERY_DAYS" "7"
is "读取新字段时间并补零" "$RESTART_TIME" "05:30"
reset_env; XRAY_DAILY_RESTART=true
resolve_restart_config
is "迁移 true → 1" "$RESTART_EVERY_DAYS" "1"
reset_env; XRAY_DAILY_RESTART=TRUE
resolve_restart_config
is "迁移 TRUE → 1" "$RESTART_EVERY_DAYS" "1"
reset_env; XRAY_DAILY_RESTART=false
resolve_restart_config
is "迁移 false → 0" "$RESTART_EVERY_DAYS" "0"
reset_env; XRAY_DAILY_RESTART=true; XRAY_RESTART_EVERY_DAYS=3
resolve_restart_config
is "新字段优先于旧字段" "$RESTART_EVERY_DAYS" "3"
reset_env; XRAY_DAILY_RESTART=true; XRAY_RESTART_EVERY_DAYS=0
resolve_restart_config
is "新字段显式 0 覆盖旧 true" "$RESTART_EVERY_DAYS" "0"
# 前导零必须按十进制归一化:这个值会原样写进 guard 的 DAYS=
# 而 guard 里是算术展开——010 会变成 8 天,08/09 直接报错导致每天重启
reset_env; XRAY_RESTART_EVERY_DAYS=08
resolve_restart_config
is "08 归一化为 8(不当八进制)" "$RESTART_EVERY_DAYS" "8"
reset_env; XRAY_RESTART_EVERY_DAYS=09
resolve_restart_config
is "09 归一化为 9(不当八进制)" "$RESTART_EVERY_DAYS" "9"
reset_env; XRAY_RESTART_EVERY_DAYS=010
resolve_restart_config
is "010 归一化为 10(不当八进制)" "$RESTART_EVERY_DAYS" "10"
reset_env; XRAY_RESTART_EVERY_DAYS=abc
resolve_restart_config 2>/dev/null
is "天数非数字报错" "$?" "1"
reset_env; XRAY_RESTART_EVERY_DAYS=-1
resolve_restart_config 2>/dev/null
is "天数负数报错" "$?" "1"
reset_env; XRAY_RESTART_TIME=25:00
resolve_restart_config 2>/dev/null
is "时间非法报错" "$?" "1"
reset_env; XRAY_RESTART_TIMEZONE=""
resolve_restart_config 2>/dev/null
is "时区空串报错" "$?" "1"
echo "== update_env_field =="
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
cat > "${TMP}/.env" << 'ENVEOF'
# 这是用户自己加的注释
XRAY_PORT=443
XRAY_RESTART_EVERY_DAYS=0
# 尾部注释
ENVEOF
update_env_field "${TMP}/.env" XRAY_RESTART_EVERY_DAYS 7
is "替换已有字段" "$(grep '^XRAY_RESTART_EVERY_DAYS=' "${TMP}/.env")" "XRAY_RESTART_EVERY_DAYS=7"
is "保留用户注释" "$(grep -c '这是用户自己加的注释' "${TMP}/.env")" "1"
is "保留尾部注释" "$(grep -c '尾部注释' "${TMP}/.env")" "1"
is "不影响其它字段" "$(grep '^XRAY_PORT=' "${TMP}/.env")" "XRAY_PORT=443"
update_env_field "${TMP}/.env" XRAY_RESTART_TIME 05:30
is "追加缺失字段" "$(grep '^XRAY_RESTART_TIME=' "${TMP}/.env")" "XRAY_RESTART_TIME=05:30"
update_env_field "${TMP}/.env" XRAY_RESTART_TIMEZONE Asia/Shanghai
is "含斜杠的值不被 sed 分隔符干扰" \
"$(grep '^XRAY_RESTART_TIMEZONE=' "${TMP}/.env")" "XRAY_RESTART_TIMEZONE=Asia/Shanghai"
update_env_field "${TMP}/.env" XRAY_RESTART_TIMEZONE Europe/Berlin
is "含斜杠的值可被再次替换" \
"$(grep '^XRAY_RESTART_TIMEZONE=' "${TMP}/.env")" "XRAY_RESTART_TIMEZONE=Europe/Berlin"
is "字段总行数正确" "$(grep -c '^XRAY_RESTART_TIMEZONE=' "${TMP}/.env")" "1"
update_env_field "${TMP}/nonexistent" FOO bar 2>/dev/null
is "文件不存在返回 1" "$?" "1"
printf 'KEY=old' > "${TMP}/.env2"
update_env_field "${TMP}/.env2" NEWKEY val
is "缺尾换行时不黏连" "$(grep -c '^NEWKEY=val$' "${TMP}/.env2")" "1"
is "缺尾换行时原字段完好" "$(grep -c '^KEY=old$' "${TMP}/.env2")" "1"
echo "== render_restart_units =="
SYSTEMD_DIR="${TMP}/systemd"
GUARD_BIN="${TMP}/bin/xray-restart-guard"
RESTART_STATE_FILE="${TMP}/state/last-restart"
mkdir -p "${TMP}/systemd" "${TMP}/bin" "${TMP}/state"
# systemctl 打桩:记录被调用的参数,避免碰真实 systemd
STUB="${TMP}/stub"
mkdir -p "$STUB"
cat > "${STUB}/systemctl" << 'STUBEOF'
#!/bin/sh
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
STUBEOF
chmod 755 "${STUB}/systemctl"
export SYSTEMCTL_LOG="${TMP}/systemctl.log"
: > "$SYSTEMCTL_LOG"
export PATH="${STUB}:${PATH}"
render_restart_units 7 04:00 Asia/Shanghai
is "timer 文件存在" "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes)" "yes"
is "service 文件存在" "$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes)" "yes"
is "guard 可执行" "$([ -x "$GUARD_BIN" ] && echo yes)" "yes"
# 时区必须写在日历表达式里:[Timer] 段没有 TimeZone= 这个 key
# 写了会被 systemd 忽略并按系统本地时区(VPS 多为 UTC)解析,静默偏移 8 小时
is "OnCalendar 每天触发且带时区" \
"$(grep -c '^OnCalendar=\*-\*-\* 04:00:00 Asia/Shanghai$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
is "不再使用非法的 TimeZone= 指令" \
"$(grep -c '^TimeZone=' "${SYSTEMD_DIR}/xray-restart.timer")" "0"
is "Persistent 开启" \
"$(grep -c '^Persistent=true$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
is "service 指向 guard" \
"$(grep -c "^ExecStart=${GUARD_BIN}\$" "${SYSTEMD_DIR}/xray-restart.service")" "1"
is "guard 里天数正确" \
"$(grep -c '^DAYS=7$' "$GUARD_BIN")" "1"
is "guard 里状态文件路径正确" \
"$(grep -c "^STATE=\"${RESTART_STATE_FILE}\"\$" "$GUARD_BIN")" "1"
is "guard 用 PATH 查找 systemctl 而非绝对路径" \
"$(grep -c '^exec systemctl restart xray$' "$GUARD_BIN")" "1"
render_restart_units 3 23:05 Europe/Berlin
is "重复渲染覆盖天数" "$(grep -c '^DAYS=3$' "$GUARD_BIN")" "1"
is "重复渲染覆盖时间与时区" \
"$(grep -c '^OnCalendar=\*-\*-\* 23:05:00 Europe/Berlin$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
remove_restart_units
is "remove 后 timer 消失" "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "no"
is "remove 后 service 消失" "$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes || echo no)" "no"
is "remove 后 guard 消失" "$([ -f "$GUARD_BIN" ] && echo yes || echo no)" "no"
remove_restart_units
is "重复 remove 返回 1" "$?" "1"
echo "== guard 脚本行为 =="
render_restart_units 7 04:00 Asia/Shanghai
rm -f "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
# 无状态文件 → 首次必须执行
out=$("$GUARD_BIN" 2>&1)
is "首次执行重启" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "1"
is "首次写入时间戳" "$([ -f "$RESTART_STATE_FILE" ] && echo yes)" "yes"
# 刚重启过 → 必须跳过
: > "$SYSTEMCTL_LOG"
out=$("$GUARD_BIN" 2>&1)
is "刚重启过则跳过" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
is "跳过时有提示" "$(echo "$out" | grep -c '跳过')" "1"
# 距上次 6 天 → 仍未满 7 天,跳过
echo "$(( $(date +%s) - 6 * 86400 ))" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "6 天不足 7 天则跳过" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
# 距上次 7 天差 100 秒 → 300 秒容差内,必须执行
echo "$(( $(date +%s) - 7 * 86400 + 100 ))" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "容差内视为已满 7 天" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "1"
# 距上次 7 天差 600 秒 → 超出容差,跳过
echo "$(( $(date +%s) - 7 * 86400 + 600 ))" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "超出容差仍判不足" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
# 状态文件内容被写坏 → 按 0 处理,立即执行
echo "garbage" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "状态文件非法时立即执行" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "1"
is "非法内容被覆盖为数字" \
"$(grep -cE '^[0-9]+$' "$RESTART_STATE_FILE")" "1"
echo "garbage" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
guard_err=$("$GUARD_BIN" 2>&1 >/dev/null)
is "状态文件非法时不产生算术求值报错" \
"$(printf '%s' "$guard_err" | grep -c 'integer expression')" "0"
# 时间戳落在未来(RTC 错乱 / NTP 大幅向后校正)→ 必须按 0 处理立即执行,
# 否则时钟校回后 now-last 恒为负,此后永远跳过且无自愈路径
echo "$(( $(date +%s) + 30 * 86400 ))" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "未来时间戳不导致永久跳过" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "1"
is "未来时间戳被覆盖为当前时间" \
"$([ "$(cat "$RESTART_STATE_FILE")" -le "$(date +%s)" ] && echo yes || echo no)" "yes"
# 状态文件写不进去 → 必须跳过本轮。照常重启会让下一轮依旧读不到时间戳,
# 退化成每天硬重启且无限持续
guard_saved_state="$RESTART_STATE_FILE"
# (a) 目录建不出来:拿一个普通文件占住父目录的位置
: > "${TMP}/blocked"
RESTART_STATE_FILE="${TMP}/blocked/last-restart"
render_restart_units 7 04:00 Asia/Shanghai
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "状态目录建不出来时跳过重启" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
# (b) 目录能建但文件写不进去:让状态文件路径本身是个目录
mkdir -p "${TMP}/asdir/last-restart"
RESTART_STATE_FILE="${TMP}/asdir/last-restart"
render_restart_units 7 04:00 Asia/Shanghai
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "时间戳写不进去时跳过重启" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
RESTART_STATE_FILE="$guard_saved_state"
# 每日模式:距上次 1 天即执行
render_restart_units 1 04:00 Asia/Shanghai
echo "$(( $(date +%s) - 86400 ))" > "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "N=1 时满一天即执行" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "1"
# 时间戳先于重启写入:用一个必定失败的 systemctl 桩验证
cat > "${STUB}/systemctl" << 'STUBEOF'
#!/bin/sh
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
exit 1
STUBEOF
chmod 755 "${STUB}/systemctl"
rm -f "$RESTART_STATE_FILE"
: > "$SYSTEMCTL_LOG"
"$GUARD_BIN" > /dev/null 2>&1
is "重启失败时时间戳仍已写入" "$([ -s "$RESTART_STATE_FILE" ] && echo yes)" "yes"
# 恢复正常桩,避免影响后续
cat > "${STUB}/systemctl" << 'STUBEOF'
#!/bin/sh
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
STUBEOF
chmod 755 "${STUB}/systemctl"
echo "== configure_restart_timer =="
# 启用方向
RESTART_EVERY_DAYS=7
RESTART_TIME="04:00"
RESTART_TZ="Asia/Shanghai"
rm -f "${SYSTEMD_DIR}/xray-restart.timer" "${SYSTEMD_DIR}/xray-restart.service" "$GUARD_BIN"
: > "$SYSTEMCTL_LOG"
configure_restart_timer > /dev/null 2>&1
is "启用时渲染出 timer" \
"$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "yes"
is "启用时渲染出 guard" "$([ -x "$GUARD_BIN" ] && echo yes || echo no)" "yes"
is "启用时执行 daemon-reload" \
"$(grep -c '^systemctl daemon-reload$' "$SYSTEMCTL_LOG")" "1"
is "启用时执行 enable --now" \
"$(grep -c '^systemctl enable --now xray-restart.timer$' "$SYSTEMCTL_LOG")" "1"
is "启用时不重启 xray" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
# enable 失败时不得中断调用方:configure_restart_timer 在部署主干上被裸调用
cat > "${STUB}/systemctl" << 'STUBEOF'
#!/bin/sh
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
case "$1" in
enable) exit 1 ;;
esac
STUBEOF
chmod 755 "${STUB}/systemctl"
: > "$SYSTEMCTL_LOG"
configure_restart_timer > /dev/null 2>&1
is "enable 失败时仍返回 0(不中断部署主干)" "$?" "0"
cat > "${STUB}/systemctl" << 'STUBEOF'
#!/bin/sh
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
STUBEOF
chmod 755 "${STUB}/systemctl"
# 关闭方向
echo "12345" > "$RESTART_STATE_FILE"
RESTART_EVERY_DAYS=0
: > "$SYSTEMCTL_LOG"
configure_restart_timer > /dev/null 2>&1
is "关闭时删除 timer" \
"$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "no"
is "关闭时删除 service" \
"$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes || echo no)" "no"
is "关闭时删除 guard" "$([ -f "$GUARD_BIN" ] && echo yes || echo no)" "no"
is "关闭时保留状态文件" "$(cat "$RESTART_STATE_FILE")" "12345"
is "关闭时执行 disable --now" \
"$(grep -c '^systemctl disable --now xray-restart.timer$' "$SYSTEMCTL_LOG")" "1"
is "关闭时不重启 xray" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
echo "== apply_restart_only 安全不变式 =="
# 这一节守护本特性最核心的承诺:--restart 不重启 xray、不碰 vless 链接。
# 缺了它,谁在 apply_restart_only 里加一行 systemctl restart xray 都不会被发现。
APP_DIR="${TMP}/app"
mkdir -p "$APP_DIR"
SCRIPT_DIR="$APP_DIR"
make_app_env() {
cat > "${APP_DIR}/.env" << 'ENVEOF'
# Xray 部署配置(由 deploy.sh 自动生成)
XRAY_MODE=reality
XRAY_PORT=443
XRAY_UUID=11111111-2222-3333-4444-555555555555
# 这是用户自己加的注释,不许动
SERVER_IP=
XRAY_RESTART_EVERY_DAYS=0
XRAY_RESTART_TIME=04:00
XRAY_RESTART_TIMEZONE=Asia/Shanghai
XRAY_PUBLIC_KEY=pubkeyzzz
XRAY_SHORT_ID=abcdef0123456789
REALITY_SNI=www.apple.com
ENVEOF
}
# --- 启用:--restart 7 5:30 ---
reset_env
make_app_env
cp "${APP_DIR}/.env" "${TMP}/env.before"
RESTART_ARG_DAYS=7
RESTART_ARG_TIME=5:30
: > "$SYSTEMCTL_LOG"
apply_restart_only > /dev/null 2>&1
is "apply_restart_only 绝不重启 xray" \
"$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
is "天数写回 .env" "$(grep '^XRAY_RESTART_EVERY_DAYS=' "${APP_DIR}/.env")" "XRAY_RESTART_EVERY_DAYS=7"
is "时间归一化后写回" "$(grep '^XRAY_RESTART_TIME=' "${APP_DIR}/.env")" "XRAY_RESTART_TIME=05:30"
is ".env 只有这两行发生变化" \
"$(diff "${TMP}/env.before" "${APP_DIR}/.env" | grep -c '^[<>]')" "4"
is "用户注释原样保留" "$(grep -c '这是用户自己加的注释,不许动' "${APP_DIR}/.env")" "1"
is "UUID 未被改动" "$(grep -c '^XRAY_UUID=11111111-2222-3333-4444-555555555555$' "${APP_DIR}/.env")" "1"
is "SNI 未被改动" "$(grep -c '^REALITY_SNI=www\.apple\.com$' "${APP_DIR}/.env")" "1"
is "PublicKey 未被改动" "$(grep -c '^XRAY_PUBLIC_KEY=pubkeyzzz$' "${APP_DIR}/.env")" "1"
is "启用后 timer 已就位" \
"$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "yes"
# --- 不带位置参数:.env 必须完全不变 ---
reset_env
make_app_env
XRAY_RESTART_EVERY_DAYS=7
XRAY_RESTART_TIME=04:00
cp "${APP_DIR}/.env" "${TMP}/env.before2"
RESTART_ARG_DAYS=""
RESTART_ARG_TIME=""
: > "$SYSTEMCTL_LOG"
apply_restart_only > /dev/null 2>&1
is "不带位置参数时 .env 一字不改" \
"$(diff "${TMP}/env.before2" "${APP_DIR}/.env" > /dev/null && echo same || echo differ)" "same"
is "不带位置参数时也不重启 xray" \
"$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
# --- 关闭:--restart 0 ---
reset_env
echo "12345" > "$RESTART_STATE_FILE"
RESTART_ARG_DAYS=0
RESTART_ARG_TIME=""
: > "$SYSTEMCTL_LOG"
apply_restart_only > /dev/null 2>&1
is "关闭后 timer 被删" "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "no"
is "关闭后 service 被删" "$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes || echo no)" "no"
is "关闭后 guard 被删" "$([ -f "$GUARD_BIN" ] && echo yes || echo no)" "no"
is "关闭后状态文件保留" "$(cat "$RESTART_STATE_FILE")" "12345"
is "关闭时天数写回 .env" "$(grep '^XRAY_RESTART_EVERY_DAYS=' "${APP_DIR}/.env")" "XRAY_RESTART_EVERY_DAYS=0"
is "关闭时也不重启 xray" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
echo "== print_next_restart =="
RESTART_EVERY_DAYS=7
rm -f "$RESTART_STATE_FILE"
is "无状态文件时提示下次唤醒即重启" \
"$(print_next_restart | grep -c '下次唤醒时即会重启')" "1"
now_ts=$(date +%s)
echo "$(( now_ts - 6 * 86400 ))" > "$RESTART_STATE_FILE"
is "未满 N 天时给出的是真实重启日期而非明天的唤醒时间" \
"$(print_next_restart | grep -c "下次实际重启: $(date -d "@$(( now_ts + 86400 ))" '+%Y-%m-%d')")" "1"
echo "$(( $(date +%s) - 9 * 86400 ))" > "$RESTART_STATE_FILE"
is "已满 N 天时提示下次唤醒即重启" \
"$(print_next_restart | grep -c '下次唤醒时即会重启')" "1"
echo ""
echo "通过 ${PASS},失败 ${FAIL}"
[ "$FAIL" -eq 0 ]