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 项,全部通过。
469 lines
18 KiB
Bash
469 lines
18 KiB
Bash
#!/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 ]
|