Files
server-deploy/docs/superpowers/plans/2026-08-06-xray-restart-schedule.md
2026-08-07 11:47:52 +08:00

1187 lines
42 KiB
Markdown
Raw Permalink 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.
# Xray 定时重启计划 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:**`vps-xray/deploy.sh` 支持「每 N 天在固定时间点重启 Xray」,并提供一个不触碰 vless 链接的轻量命令行入口 `--restart`
**Architecture:** systemd timer 每天到点唤醒一个 oneshot serviceservice 调用守卫脚本 `xray-restart-guard`;守卫读取状态文件里的上次重启时间戳,满 N 天才真正执行 `systemctl restart xray`。这样绕开了 `OnCalendar` 无法表达任意 N 天间隔(`*-*-1/7` 会跨月重置)的限制。timer / service / guard 三个文件的生成逻辑收敛在一个函数里,完整部署与 `--restart` 共用。
**Tech Stack:** Bash 4+、systemd、Debian/Ubuntu。无测试框架——本计划新增一个自包含的 bash 断言脚本 `vps-xray/tests/test-restart-schedule.sh`,可在 Windows 的 Git Bash 或 Linux 上直接跑,不需要 root、不接触真实 systemd。
## Global Constraints
- 规格文档:`docs/superpowers/specs/2026-08-06-xray-restart-schedule-design.md`,本计划的所有行为以它为准
- 默认关闭:首次部署 `XRAY_RESTART_EVERY_DAYS=0`,与当前行为一致
- 旧字段 `XRAY_DAILY_RESTART` 仅在新字段**完全未设置**时才回退读取:`true|yes|on|1`(大小写不敏感)→ `1`,其它 → `0`
- 时间接受 `4:00``04:00`,落盘前一律归一化为两位小时 `HH:MM`
- 参数不合法一律报错退出,**不得静默回退默认值**
- 守卫脚本必须**先写时间戳再重启**
- 守卫脚本间隔判断使用 **300 秒容差**`(now - last) < DAYS*86400 - 300` 才跳过
- 状态文件 `/var/lib/xray/last-restart`;关闭定时重启时**保留**该文件
- `--restart` 路径禁止:装依赖、升级 Xray、生成密钥、选伪装目标、重写 `config.json`、改防火墙与 sysctl、重启 xray
- 所有可写路径通过可覆盖变量暴露,便于测试:`SYSTEMD_DIR``GUARD_BIN``RESTART_STATE_FILE`
- 提交信息用中文,与仓库现有风格一致
- **每个中间提交都必须是可部署的。** 本工作直接在 `master` 上进行,而 `master` 随时可能被 `upload.sh` 推到生产服务器。`deploy.sh` 开头是 `set -euo pipefail`,删除某个变量的赋值处而下游仍在引用,会让部署在中途崩溃(Task 1 已踩过一次)。拆任务时若某处的清理要延后,必须留过渡兼容代码,并在做清理的那个任务里显式删除。
- `set -u` 推论:**任何返回非 0 的函数都必须在条件位置调用**(`if f; then` / `f || x` / `f && x`),裸调用会让整个脚本退出
## File Structure
| 文件 | 职责 |
|---|---|
| `vps-xray/deploy.sh` | 配置解析与迁移、单元文件生成、`--restart` 入口。改动集中在三处:顶部参数与配置解析、新增的一组 `*_restart_*` 函数、`main()` 早退分支 |
| `vps-xray/tests/test-restart-schedule.sh` | 新建。抽取 `deploy.sh` 的函数定义做断言,覆盖归一化、迁移、校验、`.env` 改写、单元文件渲染、守卫脚本行为 |
| `vps-xray/.env.example` | 三个新字段与说明 |
| `vps-xray/uninstall.sh` | 补充清理 guard 脚本与状态文件 |
| `vps-xray/README.md` | 定时重启一节 |
---
### Task 1: 时间归一化与配置解析
**Files:**
- Create: `vps-xray/tests/test-restart-schedule.sh`
- Modify: `vps-xray/deploy.sh`(替换第 78~82 行附近的 `XRAY_DAILY_RESTART` case 块)
**Interfaces:**
- Produces: `normalize_restart_time <str>` → stdout 输出 `HH:MM`,不合法返回 1
- Produces: `resolve_restart_config` → 读环境变量 `XRAY_RESTART_EVERY_DAYS` / `XRAY_RESTART_TIME` / `XRAY_RESTART_TIMEZONE` / `XRAY_DAILY_RESTART`,设置全局 `RESTART_EVERY_DAYS`(整数)、`RESTART_TIME``HH:MM`)、`RESTART_TZ`(字符串);任一不合法则 `error` 后返回 1
- [ ] **Step 1: 写测试脚手架与失败测试**
创建 `vps-xray/tests/test-restart-schedule.sh`
```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"
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 ""
echo "通过 ${PASS},失败 ${FAIL}"
[ "$FAIL" -eq 0 ]
```
- [ ] **Step 2: 运行测试确认失败**
Run: `bash vps-xray/tests/test-restart-schedule.sh`
Expected: 大量 `FAIL`,并伴随 `normalize_restart_time: command not found` —— 因为函数尚未存在。
- [ ] **Step 3: 实现两个函数**
`vps-xray/deploy.sh` 中,把现有的这段:
```bash
# 布尔开关归一化:容忍 true/TRUE/True/yes/on/1 等写法,
# 避免用户按直觉写了 TRUE 却被静默当成关闭
case "$(printf '%s' "${XRAY_DAILY_RESTART:-false}" | tr '[:upper:]' '[:lower:]')" in
true|yes|on|1) DAILY_RESTART=1 ;;
*) DAILY_RESTART=0 ;;
esac
```
整段替换为:
```bash
# ===== 定时重启配置 =====
# 可覆盖路径,测试时指向临时目录
SYSTEMD_DIR="${SYSTEMD_DIR:-/etc/systemd/system}"
GUARD_BIN="${GUARD_BIN:-/usr/local/bin/xray-restart-guard}"
RESTART_STATE_FILE="${RESTART_STATE_FILE:-/var/lib/xray/last-restart}"
RESTART_EVERY_DAYS=0
RESTART_TIME="04:00"
RESTART_TZ="Asia/Shanghai"
# 把 4:00 / 04:00 统一成两位小时的 HH:MM,不合法返回 1。
# 归一化的意义:OnCalendar 字符串格式统一,.env 内容可预测。
normalize_restart_time() {
local t="${1:-}"
[[ "$t" =~ ^([01]?[0-9]|2[0-3]):([0-5][0-9])$ ]] || return 1
# 10# 前缀避免 08/09 被当成八进制
printf '%02d:%s' "$((10#${BASH_REMATCH[1]}))" "${BASH_REMATCH[2]}"
}
resolve_restart_config() {
local days="${XRAY_RESTART_EVERY_DAYS:-}"
# 旧字段迁移:仅当新字段完全未设置时才回退去读,
# 这样 .env 里显式写 0 能覆盖遗留的 XRAY_DAILY_RESTART=true
if [ -z "$days" ]; then
case "$(printf '%s' "${XRAY_DAILY_RESTART:-false}" | tr '[:upper:]' '[:lower:]')" in
true|yes|on|1) days=1 ;;
*) days=0 ;;
esac
fi
if ! [[ "$days" =~ ^[0-9]+$ ]]; then
error "XRAY_RESTART_EVERY_DAYS 必须是非负整数,当前为 '${days}'"
return 1
fi
RESTART_EVERY_DAYS="$days"
local t
if ! t=$(normalize_restart_time "${XRAY_RESTART_TIME:-04:00}"); then
error "XRAY_RESTART_TIME 必须是 HH:MM00:00~23:59),当前为 '${XRAY_RESTART_TIME:-}'"
return 1
fi
RESTART_TIME="$t"
RESTART_TZ="${XRAY_RESTART_TIMEZONE-Asia/Shanghai}"
if [ -z "$RESTART_TZ" ]; then
error "XRAY_RESTART_TIMEZONE 不能为空"
return 1
fi
return 0
}
resolve_restart_config || exit 1
# 过渡兼容:harden_system / start_service / save_env 仍引用 DAILY_RESTART
# 要到 Task 5 才清理。在那之前必须保证 master 上的脚本能完整跑完——
# deploy.sh 开头是 set -euo pipefail,缺这个变量会在部署中途崩溃。
DAILY_RESTART=$([ "$RESTART_EVERY_DAYS" -ne 0 ] && echo 1 || echo 0)
```
注意 `${XRAY_RESTART_TIMEZONE-Asia/Shanghai}` 用的是 `-` 而非 `:-`——显式设成空串时要报错,不能被默认值悄悄补上。
**为什么需要那行过渡兼容赋值:** 被替换掉的 case 块是 `DAILY_RESTART` 的唯一赋值处,而下游三处引用要到 Task 5 才删。`set -u` 下变量缺失会让 `harden_system` 直接 `unbound variable` 退出——那时 Xray 已装好、config.json 已写入,但服务不会启动、`.env` 不会更新。每个中间提交都必须是可部署的。
- [ ] **Step 4: 运行测试确认通过**
Run: `bash vps-xray/tests/test-restart-schedule.sh`
Expected: `通过 23,失败 0`normalize 9 项 + resolve 14 项)
Run: `bash -n vps-xray/deploy.sh`
Expected: 无输出
- [ ] **Step 5: 提交**
```bash
git add vps-xray/deploy.sh vps-xray/tests/test-restart-schedule.sh
git commit -m "feat(vps-xray): 重启计划配置解析与旧字段迁移
新增 normalize_restart_time / resolve_restart_config
XRAY_RESTART_EVERY_DAYS 缺失时才回退读 XRAY_DAILY_RESTART。
附单元测试,不需要 root 与 systemd。"
```
---
### Task 2: `.env` 字段就地更新
**Files:**
- Modify: `vps-xray/deploy.sh`(在 `resolve_restart_config` 之后新增函数)
- Modify: `vps-xray/tests/test-restart-schedule.sh`
**Interfaces:**
- Consumes: 无
- Produces: `update_env_field <file> <key> <value>` → 文件中存在 `^key=` 则替换该行,否则追加一行;文件不存在返回 1
- [ ] **Step 1: 追加失败测试**
`vps-xray/tests/test-restart-schedule.sh``echo ""` 汇总行**之前**插入:
```bash
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"
```
- [ ] **Step 2: 运行测试确认失败**
Run: `bash vps-xray/tests/test-restart-schedule.sh`
Expected: `update_env_field: command not found`,新增断言全部 FAIL
- [ ] **Step 3: 实现**
`vps-xray/deploy.sh``resolve_restart_config` 函数定义之后、`resolve_restart_config || exit 1` 调用之前,插入:
```bash
# 就地更新 .env 中的单个字段:存在则替换,不存在则追加。
# 刻意不复用 save_env——后者是 cat > 全量重写,会抹掉用户自己加的注释与字段;
# 轻量入口只该改自己那一行。
update_env_field() {
local file="$1" key="$2" value="$3" esc
[ -f "$file" ] || return 1
if grep -q "^${key}=" "$file"; then
# 用 | 作分隔符,并转义 value 中的 | & \,避免值里的 / 破坏 sed
esc=$(printf '%s' "$value" | sed 's/[|&\\]/\\&/g')
sed -i "s|^${key}=.*|${key}=${esc}|" "$file"
else
printf '%s=%s\n' "$key" "$value" >> "$file"
fi
return 0
}
```
- [ ] **Step 4: 运行测试确认通过**
Run: `bash vps-xray/tests/test-restart-schedule.sh`
Expected: `通过 34,失败 0`(新增 9 项 + 复审补的 2 项缺尾换行断言)
- [ ] **Step 5: 提交**
```bash
git add vps-xray/deploy.sh vps-xray/tests/test-restart-schedule.sh
git commit -m "feat(vps-xray): 新增 update_env_field 就地更新 .env 单个字段"
```
---
### Task 3: 渲染 timer / service / guard 三个文件
**Files:**
- Modify: `vps-xray/deploy.sh`
- Modify: `vps-xray/tests/test-restart-schedule.sh`
**Interfaces:**
- Consumes: 全局 `SYSTEMD_DIR``GUARD_BIN``RESTART_STATE_FILE`
- Produces: `render_restart_units <days> <time> <tz>` → 写出 `${SYSTEMD_DIR}/xray-restart.timer``${SYSTEMD_DIR}/xray-restart.service``${GUARD_BIN}`(可执行)
- Produces: `remove_restart_units` → 删除上述三个文件并 disable timer;原本存在返回 0,本就不存在返回 1
- [ ] **Step 1: 追加失败测试**
在汇总行之前插入:
```bash
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"
```
- [ ] **Step 2: 运行测试确认失败**
Run: `bash vps-xray/tests/test-restart-schedule.sh`
Expected: `render_restart_units: command not found`,新增断言 FAIL
- [ ] **Step 3: 实现**
`vps-xray/deploy.sh``update_env_field` 之后插入:
```bash
# 生成 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
}
```
- [ ] **Step 4: 运行测试确认通过**
Run: `bash vps-xray/tests/test-restart-schedule.sh`
Expected: `通过 50,失败 0`34 基线 + 新增 16
Run: `bash -n vps-xray/deploy.sh`
Expected: 无输出
- [ ] **Step 5: 提交**
```bash
git add vps-xray/deploy.sh vps-xray/tests/test-restart-schedule.sh
git commit -m "feat(vps-xray): 生成 timer/service/guard 三个单元文件
timer 每天唤醒,N 天间隔判断交给 guard 脚本,
绕开 OnCalendar 无法表达任意 N 天间隔的限制。"
```
---
### Task 4: 守卫脚本行为
**Files:**
- Modify: `vps-xray/tests/test-restart-schedule.sh`
**Interfaces:**
- Consumes: Task 3 的 `render_restart_units`
本任务只加测试——被测对象是 Task 3 已生成的 guard 脚本。若测试暴露缺陷,回到 Task 3 的实现修改。
- [ ] **Step 1: 追加行为测试**
在汇总行之前插入:
```bash
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"
# 每日模式:距上次 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"
```
- [ ] **Step 2: 运行测试**
Run: `bash vps-xray/tests/test-restart-schedule.sh`
Expected: `通过 61,失败 0`(50 基线 + 新增 11)。若有 FAIL,说明 Task 3 生成的 guard 逻辑有缺陷——回到 Task 3 的 `render_restart_units` 修改后重跑。
- [ ] **Step 3: 提交**
```bash
git add vps-xray/tests/test-restart-schedule.sh
git commit -m "test(vps-xray): 覆盖 guard 脚本的间隔判断、容差与状态文件容错"
```
---
### Task 5: 接线进完整部署流程
**Files:**
- Modify: `vps-xray/deploy.sh``harden_system``start_service``save_env`
**Interfaces:**
- Consumes: `resolve_restart_config``render_restart_units``remove_restart_units`
- Produces: `configure_restart_timer` → 依据全局 `RESTART_EVERY_DAYS` / `RESTART_TIME` / `RESTART_TZ` 建立或移除定时重启,并完成 `daemon-reload` 与 enable/disable
- [ ] **Step 1: 新增 configure_restart_timer**
`vps-xray/deploy.sh``remove_restart_units` 之后插入:
```bash
configure_restart_timer() {
if [ "$RESTART_EVERY_DAYS" -eq 0 ]; then
if remove_restart_units; then
warn "已移除定时重启(XRAY_RESTART_EVERY_DAYS=0"
fi
systemctl daemon-reload
log "定时重启: 关闭(崩溃恢复已由 Restart=always 秒级覆盖)"
return
fi
render_restart_units "$RESTART_EVERY_DAYS" "$RESTART_TIME" "$RESTART_TZ"
systemctl daemon-reload
systemctl enable --now xray-restart.timer >/dev/null 2>&1
log "定时重启: 每 ${RESTART_EVERY_DAYS}${RESTART_TIME} (${RESTART_TZ})"
warn "重启会切断所有活动连接;崩溃恢复本已由 Restart=always 覆盖,按需启用"
}
```
- [ ] **Step 2: 替换 harden_system 里的旧逻辑**
`harden_system()` 中,把从 `# ===== 每日重启 timer:默认关闭 =====` 注释开始、到函数结尾 `warn "该操作会切断所有活动连接,如无必要建议设 XRAY_DAILY_RESTART=false"` 为止的**全部内容**(含 `if [ "$DAILY_RESTART" -ne 1 ]` 分支、两处 `cat > /etc/systemd/system/xray-restart.*` 生成块)删除,替换为单行:
```bash
configure_restart_timer
```
保留其上方的文件描述符上限与 `xray.service.d/override.conf` 生成逻辑,不要动。
- [ ] **Step 3: 清理 start_service 中的重复 enable**
`start_service()` 中删除这三行——timer 的 enable 已由 `configure_restart_timer` 负责,且 `harden_system``start_service` 之前执行:
```bash
if [ "$DAILY_RESTART" -eq 1 ]; then
systemctl enable --now xray-restart.timer
fi
```
- [ ] **Step 4: 更新 save_env**
`save_env()` 中,把这一段:
```bash
# 每日 04:00 自动重启 Xray。默认 false。
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级),而每日硬重启会
# 切断全部活动连接。仅在确实观察到长期运行后性能退化时才改为 true。
XRAY_DAILY_RESTART=$([ "$DAILY_RESTART" -eq 1 ] && echo true || echo false)
```
替换为:
```bash
# 定时重启。0 = 关闭(默认);N = 每 N 天在 XRAY_RESTART_TIME 重启一次。
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级),而硬重启会切断
# 全部活动连接。仅在确实观察到长期运行后性能退化时才启用。
# 改这两项无需完整重新部署: bash deploy.sh --restart 7 04:00
XRAY_RESTART_EVERY_DAYS=${RESTART_EVERY_DAYS}
XRAY_RESTART_TIME=${RESTART_TIME}
XRAY_RESTART_TIMEZONE=${RESTART_TZ}
```
- [ ] **Step 5: 删除 Task 1 留下的过渡兼容赋值**
下游三处引用已在 Step 2~4 全部清理,Task 1 加的这行兼容赋值到此完成使命,删掉:
```bash
# 过渡兼容:harden_system / start_service / save_env 仍引用 DAILY_RESTART
# 要到 Task 5 才清理。在那之前必须保证 master 上的脚本能完整跑完——
# deploy.sh 开头是 set -euo pipefail,缺这个变量会在部署中途崩溃。
DAILY_RESTART=$([ "$RESTART_EVERY_DAYS" -ne 0 ] && echo 1 || echo 0)
```
留着它就是僵尸代码——没有任何读取方,却会让下一个读代码的人以为还有别处在用。
- [ ] **Step 6: 确认没有遗留引用**
Run: `grep -n "DAILY_RESTART" vps-xray/deploy.sh`
Expected: 只剩 `resolve_restart_config` 里那处向后兼容的 `${XRAY_DAILY_RESTART:-false}`,共 1 行。
Run: `bash -n vps-xray/deploy.sh`
Expected: 无输出
Run: `bash vps-xray/tests/test-restart-schedule.sh`
Expected: `通过 61,失败 0`
- [ ] **Step 7: 提交**
```bash
git add vps-xray/deploy.sh
git commit -m "refactor(vps-xray): 完整部署改用 configure_restart_timer
harden_system 与 start_service 里的旧 DAILY_RESTART 分支删除,
save_env 改写三个新字段。"
```
---
### Task 6: `--restart` 轻量入口
**Files:**
- Modify: `vps-xray/deploy.sh``usage`、参数解析、`main`,新增 `apply_restart_only`
**Interfaces:**
- Consumes: `resolve_restart_config``update_env_field``configure_restart_timer`
- Produces: 全局 `RESTART_ONLY``RESTART_ARG_DAYS``RESTART_ARG_TIME`;函数 `apply_restart_only`
- [ ] **Step 1: 更新 usage**
`usage()``--redetect` 那两行之后、`-h, --help` 之前插入:
```bash
echo " --restart [N] [HH:MM]"
echo " 只修改定时重启计划并立即生效。N=0 关闭。"
echo " 不重装 Xray、不重写配置、不重启服务,"
echo " 现有 vless 链接完全不受影响。"
echo " 例: bash deploy.sh --restart 7 04:00"
```
- [ ] **Step 2: 扩展参数解析**
在参数解析区,把:
```bash
MODE=""
MODE_EXPLICIT=0
SHOW_ONLY=0
REDETECT=0
```
改为:
```bash
MODE=""
MODE_EXPLICIT=0
SHOW_ONLY=0
REDETECT=0
RESTART_ONLY=0
RESTART_ARG_DAYS=""
RESTART_ARG_TIME=""
```
`while` 循环的 `case` 中,`--redetect) REDETECT=1; shift ;;` 之后插入:
```bash
--restart)
RESTART_ONLY=1
shift
# 后面可跟 0~2 个位置参数,按形态判断,不会误吞后续选项
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
RESTART_ARG_DAYS="$1"
shift
if [[ "${1:-}" =~ ^([01]?[0-9]|2[0-3]):[0-5][0-9]$ ]]; then
RESTART_ARG_TIME="$1"
shift
fi
fi
;;
```
`while` 循环结束后、root 检查之前插入互斥校验:
```bash
# --restart 是「只改重启计划」的轻量入口,与会改动服务的选项互斥,
# 避免用户以为只调了 timer、实际却顺带重装了 Xray
if [ "$RESTART_ONLY" -eq 1 ] \
&& { [ "$MODE_EXPLICIT" -eq 1 ] || [ "$SHOW_ONLY" -eq 1 ] || [ "$REDETECT" -eq 1 ]; }; then
error "--restart 不能与 --mode / --show / --redetect 同时使用"
exit 1
fi
```
- [ ] **Step 3: 实现 apply_restart_only**
`configure_restart_timer` 之后插入:
```bash
# ===== --restart:只改重启计划 =====
# 明确不做:装依赖、升级 Xray、生成密钥、选伪装目标、重写 config.json、
# 改防火墙与 sysctl、重启 xray。因此 vless 链接与现有连接均不受影响。
apply_restart_only() {
local env_file="$SCRIPT_DIR/.env"
if [ ! -f "$env_file" ]; then
error "未找到 ${env_file},无法确认这是一台已部署的机器"
error "请先执行完整部署: bash deploy.sh"
exit 1
fi
if ! systemctl cat xray >/dev/null 2>&1; then
error "未找到 xray.service,请先执行完整部署: bash deploy.sh"
exit 1
fi
# 命令行给了值就覆盖 .env 载入的值,没给则沿用
if [ -n "$RESTART_ARG_DAYS" ]; then
XRAY_RESTART_EVERY_DAYS="$RESTART_ARG_DAYS"
fi
if [ -n "$RESTART_ARG_TIME" ]; then
XRAY_RESTART_TIME="$RESTART_ARG_TIME"
fi
resolve_restart_config || exit 1
# 只把命令行显式给过的字段落盘,没给的不动 .env
if [ -n "$RESTART_ARG_DAYS" ]; then
update_env_field "$env_file" XRAY_RESTART_EVERY_DAYS "$RESTART_EVERY_DAYS"
fi
if [ -n "$RESTART_ARG_TIME" ]; then
update_env_field "$env_file" XRAY_RESTART_TIME "$RESTART_TIME"
fi
configure_restart_timer
echo ""
echo "=========================================================="
if [ "$RESTART_EVERY_DAYS" -eq 0 ]; then
echo " 定时重启: 已关闭"
else
echo " 定时重启: 每 ${RESTART_EVERY_DAYS}${RESTART_TIME} (${RESTART_TZ})"
fi
echo " xray 服务未重启,vless 链接与现有连接均未受影响。"
echo "=========================================================="
if [ "$RESTART_EVERY_DAYS" -ne 0 ]; then
echo ""
systemctl list-timers xray-restart.timer --no-pager 2>/dev/null || true
fi
}
```
注意 `resolve_restart_config` 在脚本顶层已经跑过一次,这里再跑一次是为了让命令行传入的值参与校验与归一化。
- [ ] **Step 4: 在 main 中早退**
`main()` 里,紧跟现有的 `--show` 分支之后插入:
```bash
if [ "$RESTART_ONLY" -eq 1 ]; then
apply_restart_only
exit 0
fi
```
- [ ] **Step 5: 修正非法参数的退出码**
现状:`usage()` 结尾是 `exit 0`,而参数解析的兜底分支是 `*) error "未知参数: $1"; usage ;;`——非法参数会以**退出码 0** 结束。`--restart 7 25:00` 中的 `25:00` 不匹配时间形态、不被 `--restart` 消费,正会落进这个分支,于是「时间写错」表现为「成功退出」。这违反本计划的全局约束。
`usage()` 的结尾:
```bash
exit 0
}
```
改为:
```bash
exit "${1:-0}"
}
```
并把参数解析的兜底分支:
```bash
*) error "未知参数: $1"; usage ;;
```
改为:
```bash
*) error "未知参数: $1"; usage 1 ;;
```
`-h|--help) usage ;;` 保持不变,仍以 0 退出。
- [ ] **Step 6: 本地校验**
Run: `bash -n vps-xray/deploy.sh`
Expected: 无输出
Run: `bash vps-xray/deploy.sh --badarg > /dev/null 2>&1; echo $?`
Expected: `1`
Run: `bash vps-xray/deploy.sh --help > /dev/null 2>&1; echo $?`
Expected: `0`
Run: `bash vps-xray/tests/test-restart-schedule.sh`
Expected: `通过 61,失败 0`
Run: `bash vps-xray/deploy.sh --help`
Expected: 帮助里出现 `--restart [N] [HH:MM]` 说明,退出码 0
- [ ] **Step 7: 提交**
```bash
git add vps-xray/deploy.sh
git commit -m "feat(vps-xray): 新增 --restart 轻量入口
只改定时重启计划并立即生效,不重装 Xray、不重写配置、不重启服务,
与 --mode/--show/--redetect 互斥。
顺带修正非法参数以退出码 0 结束的问题。"
```
---
### Task 7: 文档与卸载清理
**Files:**
- Modify: `vps-xray/.env.example`
- Modify: `vps-xray/uninstall.sh`
- Modify: `vps-xray/README.md`
- [ ] **Step 1: 更新 .env.example**
把现有这段:
```bash
# 每日 04:00 自动重启 Xray。默认 false。
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级),而每日硬重启会
# 切断全部活动连接。仅在确实观察到长期运行后性能退化时才改为 true。
# XRAY_DAILY_RESTART=false
```
替换为:
```bash
# ===== 定时重启 =====
# 0 = 关闭(默认);N = 每 N 天重启一次 Xray。
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级响应),而硬重启会切断
# 全部活动连接。仅在确实观察到长期运行后性能退化时才启用。
# XRAY_RESTART_EVERY_DAYS=0
# XRAY_RESTART_TIME=04:00
# XRAY_RESTART_TIMEZONE=Asia/Shanghai
#
# 改这几项无需完整重新部署,用轻量入口即可(不影响 vless 链接):
# bash deploy.sh --restart 7 04:00
# bash deploy.sh --restart 0
#
# 旧字段 XRAY_DAILY_RESTART 已废弃,仅在上面三项都没设时作为兼容回退:
# true → 每 1 天,其它 → 关闭。重跑部署后会自动换写成新字段。
```
- [ ] **Step 2: 更新 uninstall.sh**
把现有这段:
```bash
if [ -f /etc/systemd/system/xray-restart.timer ]; then
systemctl disable --now xray-restart.timer 2>/dev/null || true
rm -f /etc/systemd/system/xray-restart.timer \
/etc/systemd/system/xray-restart.service
```
替换为:
```bash
if [ -f /etc/systemd/system/xray-restart.timer ] \
|| [ -f /usr/local/bin/xray-restart-guard ]; then
systemctl disable --now xray-restart.timer 2>/dev/null || true
rm -f /etc/systemd/system/xray-restart.timer \
/etc/systemd/system/xray-restart.service \
/usr/local/bin/xray-restart-guard \
/var/lib/xray/last-restart
```
卸载与「关闭定时重启」不同,卸载要连状态文件一起清掉。
- [ ] **Step 3: 更新 README**
`README.md` 的「查看状态 / 日志」一节之前插入:
```markdown
### 定时重启
默认**关闭**。崩溃恢复已由 systemd 的 `Restart=always` + `RestartSec=3` 覆盖(秒级响应),而硬重启会切断全部活动连接——跨夜下载、备份、CI 都会中断。仅在确实观察到长期运行后性能退化时才启用。
需要时用轻量入口调整,**不会重装 Xray、不会重写配置、不会重启服务,现有 vless 链接完全不受影响**:
```bash
bash deploy.sh --restart 7 04:00 # 每 7 天 04:00 重启
bash deploy.sh --restart 3 # 每 3 天,时间沿用 .env
bash deploy.sh --restart 0 # 关闭
bash deploy.sh --restart # 按 .env 现有值重建 timer
```
对应 `.env` 三个字段:
| 字段 | 含义 | 默认 |
|---|---|---|
| `XRAY_RESTART_EVERY_DAYS` | 0 = 关闭;N = 每 N 天 | `0` |
| `XRAY_RESTART_TIME` | 24 小时制 `HH:MM` | `04:00` |
| `XRAY_RESTART_TIMEZONE` | 时区 | `Asia/Shanghai` |
#### 为什么不是纯 systemd timer
`OnCalendar` 表达不了任意 N 天:`*-*-1/7 04:00:00` 是「每月 1、8、15、22、29 号」,**跨月会重置**——29 号到下月 1 号只隔 2~3 天。
所以实现是 timer 每天到点唤醒,由 `/usr/local/bin/xray-restart-guard``/var/lib/xray/last-restart`,满 N 天才真正重启。间隔因此是「距上次实际重启」的真实天数,`N=3``N=7` 同样准确。
查看下次触发时间与执行记录:
```bash
systemctl list-timers xray-restart.timer
journalctl -u xray-restart.service -n 20
```
> 旧字段 `XRAY_DAILY_RESTART` 已废弃。三个新字段都未设置时它仍作为兼容回退(`true` → 每 1 天,其它 → 关闭),重跑部署后 `.env` 会自动换写成新字段。
```
- [ ] **Step 4: 校验**
Run: `bash -n vps-xray/uninstall.sh`
Expected: 无输出
Run: `grep -c "XRAY_DAILY_RESTART" vps-xray/README.md`
Expected: `1`(只剩兼容说明那处)
- [ ] **Step 5: 提交**
```bash
git add vps-xray/.env.example vps-xray/uninstall.sh vps-xray/README.md
git commit -m "docs(vps-xray): 定时重启配置说明;uninstall 清理 guard 与状态文件"
```
---
### Task 8: VPS 端到端验证
**Files:** 无改动。本任务在真实服务器上执行 spec 的验证方案。
**前置:** 目标机器已用旧版脚本部署过,`.env` 存在。先记下当前 vless 链接以便比对。
- [ ] **Step 1: 上传**
```bash
bash vps-xray/upload.sh <VPS_IP>
```
- [ ] **Step 2: 启用并确认服务未被重启**
在 VPS 上:
```bash
cd /opt/vps-xray
systemctl show xray -p ActiveEnterTimestamp # 记下改动前的值
bash deploy.sh --restart 7 04:00
systemctl show xray -p ActiveEnterTimestamp # 必须与上面完全相同
grep -E '^XRAY_RESTART_' .env
systemctl list-timers xray-restart.timer --no-pager
```
Expected`ActiveEnterTimestamp` 前后一致(证明没重启 xray);`.env``XRAY_RESTART_EVERY_DAYS=7``XRAY_RESTART_TIME=04:00``list-timers` 显示 `NEXT` 为次日 04:00。
- [ ] **Step 3: 守卫行为**
```bash
rm -f /var/lib/xray/last-restart
/usr/local/bin/xray-restart-guard # 应输出「执行重启」
/usr/local/bin/xray-restart-guard # 应输出「跳过」
cat /var/lib/xray/last-restart
```
Expected:第一次重启并写入时间戳,第二次跳过。
- [ ] **Step 4: 关闭**
```bash
bash deploy.sh --restart 0
ls -l /etc/systemd/system/xray-restart.* /usr/local/bin/xray-restart-guard 2>&1
ls -l /var/lib/xray/last-restart
```
Expected:三个单元/脚本文件均报 `No such file`;状态文件仍在。
- [ ] **Step 5: 参数校验与互斥**
```bash
bash deploy.sh --restart 7 25:00 ; echo "exit=$?"
bash deploy.sh --restart abc ; echo "exit=$?"
bash deploy.sh --restart 7 --redetect ; echo "exit=$?"
bash deploy.sh --restart 4:00 ; echo "exit=$?"
ls /etc/systemd/system/xray-restart.timer 2>&1
```
逐条预期(形态解析的既定行为,都不是缺陷):
| 命令 | 行为 | 退出码 |
|---|---|---|
| `--restart 7 25:00` | `25:00` 不匹配时间形态、不被消费,落到兜底分支报「未知参数: 25:00」并打印帮助 | `1` |
| `--restart abc` | 同上,报「未知参数: abc」 | `1` |
| `--restart 7 --redetect` | `--redetect` 不被消费,正常置位,循环后互斥校验拦下,报「--restart 不能与 --mode / --show / --redetect 同时使用」 | `1` |
| `--restart 4:00` | `4:00` 不匹配数字形态,不作 N 用;落到兜底分支报「未知参数: 4:00」。**时间必须跟在天数之后**,这是位置参数的固有约束,帮助文本已写明 `--restart [N] [HH:MM]` | `1` |
最后 `ls` 应报 `No such file`——四条命令都不该留下任何 timer 文件。
- [ ] **Step 6: 旧字段迁移**
```bash
cp .env /tmp/env.bak
sed -i '/^XRAY_RESTART_/d' .env
echo 'XRAY_DAILY_RESTART=true' >> .env
bash deploy.sh
grep -E '^XRAY_RESTART_|^XRAY_DAILY' .env
systemctl list-timers xray-restart.timer --no-pager
```
Expected`.env` 中出现 `XRAY_RESTART_EVERY_DAYS=1`,不再有 `XRAY_DAILY_RESTART`timer 已建立。
- [ ] **Step 7: 恢复并确认链接未变**
```bash
cp /tmp/env.bak .env
bash deploy.sh --restart 0
bash deploy.sh --show
```
Expected`--show` 输出的 vless 链接与 Step 1 之前记录的完全一致。
- [ ] **Step 8: 记录结果并提交**
把实测输出补进 `docs/superpowers/plans/2026-08-06-xray-restart-schedule.md` 末尾的「验证记录」小节,然后:
```bash
git add docs/superpowers/plans/2026-08-06-xray-restart-schedule.md
git commit -m "docs: 补充定时重启功能的 VPS 端到端验证记录"
```
---
## 验证记录
Task 8 执行后填写实测输出)