fix(vps-xray): 修复定时重启的四类静默失效并补齐安全不变式测试
Critical-1 timer 时区:[Timer] 段没有 TimeZone= 这个 key,systemd 只会记一条 "Unknown key name" 然后按系统本地时区解析 OnCalendar。VPS 出厂多为 UTC, 用户设「04:00 Asia/Shanghai」会在北京时间 12:00 触发且毫无提示。 改为把时区写进日历表达式本身:OnCalendar=*-*-* HH:MM:00 <TZ>(systemd >= 242)。 Critical-2 前导零:^[0-9]+$ 放行 08/09/010,该值原样写进 guard 的 DAYS=, 而 guard 里是算术展开——010 被当八进制解析成 8 天,08/09 直接报 "value too great for base",使间隔判断求值为假,落进重启分支变成每天重启。 校验通过后统一 $((10#$days)) 归一化。 Important-3 状态文件写入失败:mkdir 与写时间戳都不检查返回值,/var 写满或 只读时写入失败却照样重启,「先写时间戳」的保护完全失效,退化成每天硬重启且 无限持续。改为 fail-safe:写不进去就报错并跳过本轮。 update_env_field 的三处写入同样补上返回值检查,调用方改在条件位置。 Important-4 未来时间戳:RTC 错乱或 NTP 大幅向后校正时会写下未来的时间戳, 时钟校回后 now-last 恒为负,此后永远跳过且无自愈路径。与「内容非法按 0 处理」 同策略,读到 last > now 时按 0 处理。 Important-5 静默中止:configure_restart_timer 被 harden_system、main 一路裸调用, set -e 下 systemctl enable 失败会让部署在「8/8 加固」之后无声退出—— start_service / save_env / print_result 全不执行,config.json 已换、防火墙已改, 但 xray 没重启、.env 没更新、链接没打印。三处 systemctl 全部改到条件位置, enable 不再吞 stderr(规格本就要求靠 systemctl 报错暴露非法时区)。 Important-6 NEXT 语义:list-timers 的 NEXT 是 timer 的「下次唤醒」,每天一次, 不是下次重启时间。新增 print_next_restart,--restart 输出里直接算出并显示 「下次实际重启时间」,README 同步说明差异。 Important-7 安全不变式测试:configure_restart_timer 与 apply_restart_only 原本 零覆盖,「不重启 xray、不碰 vless 链接」这个最重要的承诺恰恰没有回归保护。 补上启用/关闭两个方向的断言,以及 .env 逐字段 diff 与 systemctl 调用日志断言。 Minor-9 README 补充取舍:完整部署的 systemctl restart xray 不更新状态文件。 Minor-11 save_env 注释「这两项」→「这几项」。 测试 61 → 101 项,全部通过。
This commit is contained in:
+10
-1
@@ -226,13 +226,22 @@ bash deploy.sh --restart # 按 .env 现有值重建 timer
|
||||
|
||||
所以实现是 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
|
||||
```
|
||||
|
||||
> ⚠️ `list-timers` 的 `NEXT` 是 **timer 下次「唤醒」的时间**,不是下次重启的时间。timer 每天唤醒一次,NEXT 因此恒为次日的 `XRAY_RESTART_TIME`;是否真的重启由守卫按间隔判断。设了「每 7 天」且 5 天前刚重启过时,NEXT 显示明天凌晨,但实际还要再等 2 天。
|
||||
>
|
||||
> 想直接看「下次实际重启时间」,跑一次不带参数的 `bash deploy.sh --restart`(不改任何配置、不重启服务),输出里会算好打印出来。
|
||||
|
||||
#### 已知取舍
|
||||
|
||||
- **间隔锚点是「上次由守卫执行的重启」,不是任何一次 xray 重启。** 完整部署(`bash deploy.sh`)里的 `systemctl restart xray` 不会更新 `/var/lib/xray/last-restart`——设了每 7 天、距上次守卫重启已 6.9 天时跑一次完整部署,次日守卫仍会判定满 7 天而再重启一次。介意的话可在部署后手动执行 `date +%s > /var/lib/xray/last-restart` 把锚点对齐。
|
||||
- **重启失败时时间戳仍已更新**,本轮被跳过,下一轮照常——这是为了避免陷入每天硬重启的循环。但若时间戳**写不进去**(磁盘满、只读挂载),守卫会跳过本轮重启并在日志里报错,宁可不重启也不退化成每日重启。
|
||||
|
||||
> 旧字段 `XRAY_DAILY_RESTART` 已废弃。三个新字段都未设置时它仍作为兼容回退(`true` → 每 1 天,其它 → 关闭),重跑部署后 `.env` 会自动换写成新字段。
|
||||
|
||||
### 查看状态 / 日志
|
||||
|
||||
+97
-16
@@ -138,7 +138,11 @@ resolve_restart_config() {
|
||||
error "XRAY_RESTART_EVERY_DAYS 必须是非负整数,当前为 '${days}'"
|
||||
return 1
|
||||
fi
|
||||
RESTART_EVERY_DAYS="$days"
|
||||
# 10# 前缀去掉前导零:这个值会原样写进 guard 的 DAYS=,
|
||||
# 而 guard 里 $(( DAYS * 86400 - 300 )) 是算术展开——
|
||||
# 010 会被当八进制解析成 8 天,08 / 09 直接报 "value too great for base"
|
||||
# 从而让间隔判断求值为假,退化成每天重启。
|
||||
RESTART_EVERY_DAYS=$((10#$days))
|
||||
|
||||
local t
|
||||
if ! t=$(normalize_restart_time "${XRAY_RESTART_TIME:-04:00}"); then
|
||||
@@ -163,16 +167,18 @@ update_env_field() {
|
||||
local file="$1" key="$2" value="$3" esc
|
||||
[ -f "$file" ] || return 1
|
||||
|
||||
# 每一步写入都检查返回值:磁盘满或只读时静默失败会让用户以为改动已落盘,
|
||||
# 下次完整部署又被 .env 里的旧值覆盖回去
|
||||
if grep -q "^${key}=" "$file"; then
|
||||
# 用 | 作分隔符,并转义 value 中的 | & \,避免值里的 / 破坏 sed
|
||||
esc=$(printf '%s' "$value" | sed 's/[|&\\]/\\&/g')
|
||||
sed -i "s|^${key}=.*|${key}=${esc}|" "$file"
|
||||
sed -i "s|^${key}=.*|${key}=${esc}|" "$file" || return 1
|
||||
else
|
||||
# 文件末尾可能缺换行(用户手工编辑过),直接追加会把新字段黏到上一行
|
||||
if [ -n "$(tail -c 1 "$file")" ]; then
|
||||
printf '\n' >> "$file"
|
||||
printf '\n' >> "$file" || return 1
|
||||
fi
|
||||
printf '%s=%s\n' "$key" "$value" >> "$file"
|
||||
printf '%s=%s\n' "$key" "$value" >> "$file" || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
@@ -199,6 +205,10 @@ if [ -f "\$STATE" ]; then
|
||||
last=\$(cat "\$STATE" 2>/dev/null || echo 0)
|
||||
# 内容被写坏时按 0 处理,即允许立即重启
|
||||
[[ "\$last" =~ ^[0-9]+\$ ]] || last=0
|
||||
# 时间戳落在未来(RTC 错乱或 NTP 大幅向后校正时写下的)同样按 0 处理。
|
||||
# 否则时钟校回之后 now - last 恒为负,间隔判断恒成立,此后永远跳过,
|
||||
# 而状态文件只在真正重启时才被覆盖——没有任何自愈路径。
|
||||
[ "\$last" -gt "\$now" ] && last=0
|
||||
fi
|
||||
|
||||
# 300 秒容差:systemd timer 默认 AccuracySec=1min,第 N 次触发的实际间隔
|
||||
@@ -208,11 +218,20 @@ if [ "\$last" -gt 0 ] && [ \$(( now - last )) -lt \$(( DAYS * 86400 - 300 )) ];
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "\$(dirname "\$STATE")"
|
||||
# 必须先写时间戳:restart 会中断本脚本所在的 systemd 事务,
|
||||
# 顺序反了会丢记录,退化成每天都重启。
|
||||
# 代价是重启失败时本轮仍被记为已执行——这比陷入每日重启循环可接受。
|
||||
echo "\$now" > "\$STATE"
|
||||
#
|
||||
# 写不进去(/var 满了、只读挂载)时必须跳过本轮:照常重启会让下一轮
|
||||
# 依旧读不到时间戳,从而变成每天硬重启且无限持续——保护完全失效。
|
||||
if ! mkdir -p "\$(dirname "\$STATE")" 2>/dev/null; then
|
||||
echo "无法创建 \$(dirname "\$STATE"),跳过本轮重启以免退化成每日重启" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "\$now" > "\$STATE" 2>/dev/null; then
|
||||
echo "无法写入 \$STATE,跳过本轮重启以免退化成每日重启" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "距上次重启已满 \${DAYS} 天,执行重启"
|
||||
exec systemctl restart xray
|
||||
EOF
|
||||
@@ -234,8 +253,11 @@ EOF
|
||||
Description=Wake xray-restart daily at ${time} ${tz}; the guard enforces the ${days}-day interval
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* ${time}:00
|
||||
TimeZone=${tz}
|
||||
# 时区必须写在日历表达式里(systemd >= 242 支持)。
|
||||
# [Timer] 段没有 TimeZone= 这个 key——写了只会被忽略并留下一行
|
||||
# "Unknown key name 'TimeZone'",然后按系统本地时区解析。
|
||||
# VPS 出厂多为 UTC,那样「04:00 Asia/Shanghai」会在北京时间 12:00 触发。
|
||||
OnCalendar=*-*-* ${time}:00 ${tz}
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
@@ -249,7 +271,11 @@ 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
|
||||
# 失败不阻断删除(单元可能本就没被 enable 过),但要留一句可见的提示,
|
||||
# 而不是把结果彻底吞掉
|
||||
if ! systemctl disable --now xray-restart.timer >/dev/null 2>&1; then
|
||||
warn "禁用 xray-restart.timer 未成功(可能本就未启用),继续删除单元文件"
|
||||
fi
|
||||
rm -f "${SYSTEMD_DIR}/xray-restart.timer" \
|
||||
"${SYSTEMD_DIR}/xray-restart.service" \
|
||||
"$GUARD_BIN"
|
||||
@@ -258,21 +284,66 @@ remove_restart_units() {
|
||||
|
||||
# 依据 RESTART_EVERY_DAYS / RESTART_TIME / RESTART_TZ 建立或移除定时重启,
|
||||
# 并完成 daemon-reload 与 enable/disable。完整部署与 --restart 共用。
|
||||
# 本函数被 harden_system 裸调用、harden_system 又被 main 裸调用,
|
||||
# 全程不在条件位置。deploy.sh 开头是 set -euo pipefail,所以这里任何一条
|
||||
# systemctl 返回非 0 都会让整份部署在「8/8 加固」之后无声退出——
|
||||
# start_service / save_env / print_result 全都不执行。
|
||||
# 因此下面每条 systemctl 都放在条件位置,失败只告警不中断。
|
||||
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
|
||||
if ! systemctl daemon-reload; then
|
||||
warn "systemctl daemon-reload 失败,已删除的单元可能仍残留在 systemd 内存中"
|
||||
fi
|
||||
log "定时重启: 关闭(崩溃恢复已由 Restart=always 秒级覆盖)"
|
||||
return
|
||||
return 0
|
||||
fi
|
||||
|
||||
render_restart_units "$RESTART_EVERY_DAYS" "$RESTART_TIME" "$RESTART_TZ"
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now xray-restart.timer >/dev/null 2>&1
|
||||
if ! systemctl daemon-reload; then
|
||||
warn "systemctl daemon-reload 失败,新的 timer 可能不会立即生效"
|
||||
fi
|
||||
# 不吞 stderr:规格明确「不预校验时区字符串,靠 systemctl 报错」,
|
||||
# 把报错藏起来就等于既不校验也不提示
|
||||
if ! systemctl enable --now xray-restart.timer; then
|
||||
error "定时重启 timer 启用失败,请检查 XRAY_RESTART_TIMEZONE 是否为合法时区"
|
||||
error "部署的其余部分不受影响,可用 systemd-analyze verify ${SYSTEMD_DIR}/xray-restart.timer 排查"
|
||||
return 0
|
||||
fi
|
||||
log "定时重启: 每 ${RESTART_EVERY_DAYS} 天 ${RESTART_TIME} (${RESTART_TZ})"
|
||||
warn "重启会切断所有活动连接;崩溃恢复本已由 Restart=always 覆盖,按需启用"
|
||||
return 0
|
||||
}
|
||||
|
||||
# list-timers 里的 NEXT 是 timer 的「下次唤醒」——每天一次,恒为次日的
|
||||
# ${RESTART_TIME};真正是否重启由 guard 按间隔判断。设了「每 7 天」且 5 天前
|
||||
# 刚重启过的用户,看到 NEXT 是明天凌晨会以为明天就重启,实际还差 2 天。
|
||||
# 所以这里直接把「下次实际重启」算出来一并打印。
|
||||
print_next_restart() {
|
||||
local last next now
|
||||
now=$(date +%s)
|
||||
|
||||
if [ ! -f "$RESTART_STATE_FILE" ]; then
|
||||
echo " 下次实际重启: 下次唤醒时即会重启(尚无重启记录)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
last=$(cat "$RESTART_STATE_FILE" 2>/dev/null || echo "")
|
||||
if ! [[ "$last" =~ ^[0-9]+$ ]] || [ "$last" -gt "$now" ]; then
|
||||
echo " 下次实际重启: 下次唤醒时即会重启(状态文件内容不可用,按无记录处理)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
next=$(( last + RESTART_EVERY_DAYS * 86400 ))
|
||||
echo " 上次实际重启: $(date -d "@${last}" '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || echo "$last")"
|
||||
if [ "$next" -le "$now" ]; then
|
||||
echo " 下次实际重启: 下次唤醒时即会重启(距上次已满 ${RESTART_EVERY_DAYS} 天)"
|
||||
else
|
||||
echo " 下次实际重启: $(date -d "@${next}" '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || echo "$next") 之后的首次唤醒"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===== --restart:只改重启计划 =====
|
||||
@@ -302,10 +373,16 @@ apply_restart_only() {
|
||||
|
||||
# 只把命令行显式给过的字段落盘,没给的不动 .env
|
||||
if [ -n "$RESTART_ARG_DAYS" ]; then
|
||||
update_env_field "$env_file" XRAY_RESTART_EVERY_DAYS "$RESTART_EVERY_DAYS"
|
||||
if ! update_env_field "$env_file" XRAY_RESTART_EVERY_DAYS "$RESTART_EVERY_DAYS"; then
|
||||
error "写入 ${env_file} 失败,未做任何改动"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ -n "$RESTART_ARG_TIME" ]; then
|
||||
update_env_field "$env_file" XRAY_RESTART_TIME "$RESTART_TIME"
|
||||
if ! update_env_field "$env_file" XRAY_RESTART_TIME "$RESTART_TIME"; then
|
||||
error "写入 ${env_file} 失败,未做任何改动"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
configure_restart_timer
|
||||
@@ -316,12 +393,16 @@ apply_restart_only() {
|
||||
echo " 定时重启: 已关闭"
|
||||
else
|
||||
echo " 定时重启: 每 ${RESTART_EVERY_DAYS} 天 ${RESTART_TIME} (${RESTART_TZ})"
|
||||
print_next_restart
|
||||
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
|
||||
echo ""
|
||||
echo "注意:上表的 NEXT 是 timer 的下次「唤醒」时间(每天一次),"
|
||||
echo " 不是下次重启时间。是否真的重启由守卫脚本按上面的间隔判断。"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -972,7 +1053,7 @@ SERVER_IP=${SERVER_IP:-}
|
||||
# 定时重启。0 = 关闭(默认);N = 每 N 天在 XRAY_RESTART_TIME 重启一次。
|
||||
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级),而硬重启会切断
|
||||
# 全部活动连接。仅在确实观察到长期运行后性能退化时才启用。
|
||||
# 改这两项无需完整重新部署: bash deploy.sh --restart 7 04:00
|
||||
# 改这几项无需完整重新部署: bash deploy.sh --restart 7 04:00
|
||||
XRAY_RESTART_EVERY_DAYS=${RESTART_EVERY_DAYS}
|
||||
XRAY_RESTART_TIME=${RESTART_TIME}
|
||||
XRAY_RESTART_TIMEZONE=${RESTART_TZ}
|
||||
|
||||
@@ -89,6 +89,20 @@ 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"
|
||||
@@ -169,10 +183,12 @@ is "timer 文件存在" "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo
|
||||
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"
|
||||
# 时区必须写在日历表达式里:[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" \
|
||||
@@ -186,8 +202,8 @@ is "guard 用 PATH 查找 systemctl 而非绝对路径" \
|
||||
|
||||
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"
|
||||
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"
|
||||
@@ -246,6 +262,37 @@ 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"
|
||||
@@ -272,6 +319,150 @@ 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 ]
|
||||
|
||||
Reference in New Issue
Block a user