diff --git a/vps-xray/deploy.sh b/vps-xray/deploy.sh index b759391..d493b87 100644 --- a/vps-xray/deploy.sh +++ b/vps-xray/deploy.sh @@ -148,6 +148,85 @@ update_env_field() { return 0 } +# 生成 timer / service / guard 三个文件。 +# 完整部署与 --restart 共用本函数,避免两条路径各写一份实现后漂移。 +render_restart_units() { + local days="$1" time="$2" tz="$3" + + mkdir -p "$SYSTEMD_DIR" "$(dirname "$GUARD_BIN")" + + # 守卫脚本:systemd 的 OnCalendar 表达不了「每 N 天」—— + # *-*-1/7 是「每月 1/8/15/22/29 号」,跨月会重置成 2~3 天。 + # 所以 timer 只负责每天到点唤醒,间隔判断放到这里。 + cat > "$GUARD_BIN" << EOF +#!/bin/bash +# 由 deploy.sh 生成,请勿手工编辑 +STATE="${RESTART_STATE_FILE}" +DAYS=${days} + +now=\$(date +%s) +last=0 +if [ -f "\$STATE" ]; then + last=\$(cat "\$STATE" 2>/dev/null || echo 0) + # 内容被写坏时按 0 处理,即允许立即重启 + [[ "\$last" =~ ^[0-9]+\$ ]] || last=0 +fi + +# 300 秒容差:systemd timer 默认 AccuracySec=1min,第 N 次触发的实际间隔 +# 可能是 N*86400-30s。严格比较会判「不满」从而顺延整整一天,且误差会累积。 +if [ "\$last" -gt 0 ] && [ \$(( now - last )) -lt \$(( DAYS * 86400 - 300 )) ]; then + echo "距上次重启不足 \${DAYS} 天(已过 \$(( (now - last) / 86400 )) 天),跳过" + exit 0 +fi + +mkdir -p "\$(dirname "\$STATE")" +# 必须先写时间戳:restart 会中断本脚本所在的 systemd 事务, +# 顺序反了会丢记录,退化成每天都重启。 +# 代价是重启失败时本轮仍被记为已执行——这比陷入每日重启循环可接受。 +echo "\$now" > "\$STATE" +echo "距上次重启已满 \${DAYS} 天,执行重启" +exec systemctl restart xray +EOF + chmod 755 "$GUARD_BIN" + + cat > "${SYSTEMD_DIR}/xray-restart.service" << EOF +[Unit] +Description=Scheduled restart of Xray (every ${days} day(s)) +After=network.target + +[Service] +Type=oneshot +Environment=PATH=/usr/sbin:/usr/bin:/sbin:/bin +ExecStart=${GUARD_BIN} +EOF + + cat > "${SYSTEMD_DIR}/xray-restart.timer" << EOF +[Unit] +Description=Wake xray-restart daily at ${time} ${tz}; the guard enforces the ${days}-day interval + +[Timer] +OnCalendar=*-*-* ${time}:00 +TimeZone=${tz} +Persistent=true + +[Install] +WantedBy=timers.target +EOF +} + +# 删除 timer / service / guard。状态文件刻意保留: +# 以后重新启用时,距上次已超 N 天则首次触发即重启,符合直觉。 +remove_restart_units() { + if [ ! -f "${SYSTEMD_DIR}/xray-restart.timer" ] && [ ! -f "$GUARD_BIN" ]; then + return 1 + fi + systemctl disable --now xray-restart.timer >/dev/null 2>&1 || true + rm -f "${SYSTEMD_DIR}/xray-restart.timer" \ + "${SYSTEMD_DIR}/xray-restart.service" \ + "$GUARD_BIN" + return 0 +} + resolve_restart_config || exit 1 # 过渡兼容:harden_system / start_service / save_env 仍引用 DAILY_RESTART, diff --git a/vps-xray/tests/test-restart-schedule.sh b/vps-xray/tests/test-restart-schedule.sh index 4775c37..f8a557f 100644 --- a/vps-xray/tests/test-restart-schedule.sh +++ b/vps-xray/tests/test-restart-schedule.sh @@ -144,6 +144,59 @@ 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" + +is "OnCalendar 每天触发" \ + "$(grep -c '^OnCalendar=\*-\*-\* 04:00:00$' "${SYSTEMD_DIR}/xray-restart.timer")" "1" +is "TimeZone 写入" \ + "$(grep -c '^TimeZone=Asia/Shanghai$' "${SYSTEMD_DIR}/xray-restart.timer")" "1" +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$' "${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 "" echo "通过 ${PASS},失败 ${FAIL}" [ "$FAIL" -eq 0 ]