1187 lines
42 KiB
Markdown
1187 lines
42 KiB
Markdown
# 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 service,service 调用守卫脚本 `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:MM(00: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 执行后填写实测输出)
|