Compare commits
13
Commits
938df26cbb
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9b19b404a | ||
|
|
3fd2c89020 | ||
|
|
0dc80ff144 | ||
|
|
34f698e5fb | ||
|
|
9f1e293d4c | ||
|
|
682b79852d | ||
|
|
22d1aa6a93 | ||
|
|
354ac8a58c | ||
|
|
d19d337bf2 | ||
|
|
dfcaf7e4d0 | ||
|
|
39b0aee2c9 | ||
|
|
920774d7d7 | ||
|
|
29236e8e1c |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,182 @@
|
|||||||
|
# Xray 定时重启:可配置的「每 N 天 + 固定时间点」
|
||||||
|
|
||||||
|
日期:2026-08-06
|
||||||
|
涉及文件:`vps-xray/deploy.sh`、`vps-xray/.env.example`、`vps-xray/uninstall.sh`、`vps-xray/README.md`
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
当前 `deploy.sh` 只有一个布尔开关 `XRAY_DAILY_RESTART`,为真时创建 `OnCalendar=*-*-* 04:00:00` 的每日重启 timer,为假时删除该 timer。默认关闭,理由是崩溃恢复已由 `Restart=always` + `RestartSec=3` 覆盖(秒级),而每日硬重启会切断全部活动连接。
|
||||||
|
|
||||||
|
这个判断本身没变,但「一天一次」粒度太粗:想要「每 7 天凌晨 4 点重启一次」这种低频维护性重启时,只能在「每天重启」和「完全不重启」之间二选一。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
1. 支持任意 N 天间隔 + 指定时间点,如「每 7 天 04:00」「每 3 天 05:30」
|
||||||
|
2. 保持默认关闭,已部署机器重跑脚本时行为不变
|
||||||
|
3. 提供一个轻量命令行入口,只改重启计划,不触碰 Xray 与现有 vless 链接
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
- 不做「每月/每季度」等日历语义(真有需要时用户可直接编辑 timer)
|
||||||
|
- 不做多时间点(如一天重启两次)
|
||||||
|
- 不改动 `Restart=always` 这一层崩溃恢复逻辑
|
||||||
|
|
||||||
|
## 关键约束:systemd 表达不了任意 N 天
|
||||||
|
|
||||||
|
`OnCalendar` 的 `*-*-1/7 04:00:00` 是「每月的 1、8、15、22、29 号」,**跨月会重置**——29 号到下月 1 号只隔 2~3 天。因此不能直接用 `OnCalendar` 表达「每 N 天」。
|
||||||
|
|
||||||
|
`OnUnitActiveSec=7d` 是真实间隔,但无法钉住具体时间点,且会随每次触发缓慢漂移。
|
||||||
|
|
||||||
|
本设计采用第三种:**timer 每天到点唤醒,由 service 里的守卫脚本决定这次要不要真的重启。**
|
||||||
|
|
||||||
|
## 配置模型
|
||||||
|
|
||||||
|
`.env` 三个字段:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
XRAY_RESTART_EVERY_DAYS=0 # 0 或缺失 = 关闭;N = 每 N 天重启一次
|
||||||
|
XRAY_RESTART_TIME=04:00 # 24 小时制 HH:MM,默认 04:00
|
||||||
|
XRAY_RESTART_TIMEZONE=Asia/Shanghai # 默认保持现状
|
||||||
|
```
|
||||||
|
|
||||||
|
首次部署默认 `0`(关闭),与当前行为一致。
|
||||||
|
|
||||||
|
### 旧字段迁移
|
||||||
|
|
||||||
|
只在 `XRAY_RESTART_EVERY_DAYS` **未设置**时才回退去读 `XRAY_DAILY_RESTART`:
|
||||||
|
|
||||||
|
| 旧值 | 迁移结果 |
|
||||||
|
|---|---|
|
||||||
|
| `true` / `yes` / `on` / `1`(大小写不敏感) | `XRAY_RESTART_EVERY_DAYS=1` |
|
||||||
|
| 其它或缺失 | `XRAY_RESTART_EVERY_DAYS=0` |
|
||||||
|
|
||||||
|
`save_env` 之后只写新字段,不再写 `XRAY_DAILY_RESTART`,`.env` 随重跑自动换代。两个字段同时存在时以新字段为准。
|
||||||
|
|
||||||
|
现存四台服务器的 `.env` 都是 `XRAY_DAILY_RESTART=false`,迁移后为 `0`,行为不变。
|
||||||
|
|
||||||
|
## 实现架构
|
||||||
|
|
||||||
|
```
|
||||||
|
xray-restart.timer OnCalendar=*-*-* <HH:MM>:00 每天到点唤醒
|
||||||
|
TimeZone=<tz>
|
||||||
|
Persistent=true
|
||||||
|
↓
|
||||||
|
xray-restart.service Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/xray-restart-guard
|
||||||
|
↓
|
||||||
|
xray-restart-guard 读 /var/lib/xray/last-restart
|
||||||
|
距上次不足 N 天 → 打日志后 exit 0
|
||||||
|
满 N 天 → 写入新时间戳 → systemctl restart xray
|
||||||
|
```
|
||||||
|
|
||||||
|
「每 N 天」因此是**距上次实际重启**的真实间隔,不受跨月重置影响,N=3 与 N=7 同样准确。
|
||||||
|
|
||||||
|
### 守卫脚本的两个关键细节
|
||||||
|
|
||||||
|
**300 秒容差。** systemd timer 默认 `AccuracySec=1min`,第 N 次触发的实际间隔可能是 `N×86400 - 30s`。严格比较会判定「不满 N 天」从而顺延整整一天,且误差会持续累积。阈值取 `N×86400 - 300` 规避。容差远小于一天,不会造成重复触发。
|
||||||
|
|
||||||
|
**先写时间戳,再重启。** `systemctl restart xray` 会中断 guard 所在的 systemd 事务。顺序反了会丢失时间戳记录,导致每天都重启。代价是:重启失败时时间戳仍已更新,本轮被跳过——这比陷入每日重启循环可接受。
|
||||||
|
|
||||||
|
### 状态文件
|
||||||
|
|
||||||
|
`/var/lib/xray/last-restart`,内容为一个 Unix 时间戳。guard 以 root 运行,读写无权限问题。内容非法(非纯数字)时按 `0` 处理,即立即允许重启。
|
||||||
|
|
||||||
|
## 参数校验
|
||||||
|
|
||||||
|
在脚本早期(`.env` 载入后)完成,不合法直接报错退出,**不静默回退到默认值**——避免用户以为设了每 7 天、实际在每天重启。
|
||||||
|
|
||||||
|
| 字段 | 规则 |
|
||||||
|
|---|---|
|
||||||
|
| `XRAY_RESTART_EVERY_DAYS` | `^[0-9]+$` |
|
||||||
|
| `XRAY_RESTART_TIME` | `^([01]?[0-9]\|2[0-3]):[0-5][0-9]$`,校验通过后**补零归一化**为 `HH:MM` |
|
||||||
|
| `XRAY_RESTART_TIMEZONE` | 非空即可,交由 systemd 校验并在启动失败时报错 |
|
||||||
|
|
||||||
|
时间同时接受 `4:00` 与 `04:00`,但写入 `.env` 和 timer 前一律归一化成两位小时(`04:00`),保证 `OnCalendar` 字符串格式统一、`.env` 内容可预测。
|
||||||
|
|
||||||
|
## `--restart` 轻量入口
|
||||||
|
|
||||||
|
### 用法
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参数解析
|
||||||
|
|
||||||
|
`--restart` 后可跟 0~2 个位置参数,按形态判断,不会误吞后续选项:
|
||||||
|
|
||||||
|
- 下一个 token 匹配 `^[0-9]+$` → 取作 N
|
||||||
|
- 再下一个匹配 `^([01]?[0-9]|2[0-3]):[0-5][0-9]$` → 取作 HH:MM
|
||||||
|
- 不匹配则不消费,沿用 `.env` 现值
|
||||||
|
|
||||||
|
`--restart` 与 `--mode` / `--show` / `--redetect` 互斥,同时给出直接报错。
|
||||||
|
|
||||||
|
### 执行路径
|
||||||
|
|
||||||
|
在 `main()` 中早退,与 `--show` 同一位置:
|
||||||
|
|
||||||
|
1. 校验 N 与 HH:MM,时间归一化为 `HH:MM`
|
||||||
|
2. 就地更新 `.env`:命令行给了 N 就写 `XRAY_RESTART_EVERY_DAYS`,给了时间才写 `XRAY_RESTART_TIME`;两者都没给则不改 `.env`,只按现值重建 timer。字段不存在时追加(`TIME` 追加时用默认值 `04:00`)
|
||||||
|
3. 调用 `configure_restart_timer()`
|
||||||
|
4. `systemctl daemon-reload`,按需 enable / disable timer
|
||||||
|
5. 打印结果与 `systemctl list-timers xray-restart.timer`
|
||||||
|
|
||||||
|
### 明确不做的事
|
||||||
|
|
||||||
|
不装依赖、不升级 Xray、不生成密钥、不选伪装目标、不重写 `config.json`、不动防火墙与 sysctl、**不重启 xray**。因此 vless 链接与现有连接均不受影响。这些约束会以注释形式写在函数头部。
|
||||||
|
|
||||||
|
### 前置检查
|
||||||
|
|
||||||
|
- `.env` 不存在 → 报错退出(无法确认这是已部署的机器)
|
||||||
|
- xray 主单元不存在 → 报错退出(避免在未部署的机器上留下孤儿 timer)
|
||||||
|
|
||||||
|
## 代码组织
|
||||||
|
|
||||||
|
**timer / service / guard 的创建逻辑抽成独立函数 `configure_restart_timer()`**,由 `harden_system()` 与 `--restart` 路径共用。两条路径各写一份实现迟早会漂移成不一致。
|
||||||
|
|
||||||
|
`.env` 更新走两条不同路径,这是有意的:
|
||||||
|
|
||||||
|
- 完整部署走既有的 `save_env()`(`cat >` 全量重写)
|
||||||
|
- `--restart` 走 `sed` 就地替换 —— 轻量入口只应改自己那两行,不该抹掉用户在 `.env` 里加的注释和自定义字段
|
||||||
|
|
||||||
|
## 关闭与清理
|
||||||
|
|
||||||
|
`XRAY_RESTART_EVERY_DAYS=0` 时:
|
||||||
|
|
||||||
|
- `systemctl disable --now xray-restart.timer`
|
||||||
|
- 删除 `xray-restart.timer`、`xray-restart.service`、`/usr/local/bin/xray-restart-guard`
|
||||||
|
- **保留** `/var/lib/xray/last-restart`
|
||||||
|
|
||||||
|
保留状态文件的理由:以后重新启用时,若距上次已超过 N 天则首次触发即重启,符合直觉;若刚重启过则不会立刻再来一次。
|
||||||
|
|
||||||
|
`uninstall.sh` 需补上 guard 脚本与状态文件的清理(当前只删了 timer 和 service)。
|
||||||
|
|
||||||
|
## 受影响文件
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|---|---|
|
||||||
|
| `vps-xray/deploy.sh` | 参数解析、配置校验与迁移、`configure_restart_timer()`、`--restart` 路径、`save_env`、`usage` |
|
||||||
|
| `vps-xray/.env.example` | 三个新字段与说明 |
|
||||||
|
| `vps-xray/uninstall.sh` | 清理 guard 脚本与状态文件 |
|
||||||
|
| `vps-xray/README.md` | 定时重启一节 |
|
||||||
|
|
||||||
|
## 验证方案
|
||||||
|
|
||||||
|
在真实 VPS 上执行,不接受仅靠阅读代码判断:
|
||||||
|
|
||||||
|
1. **语法** —— `bash -n deploy.sh`
|
||||||
|
2. **启用** —— `bash deploy.sh --restart 7 04:00`,确认:`.env` 两字段已更新;`systemctl list-timers xray-restart.timer` 有下次触发时间;xray 主服务的 `ActiveEnterTimestamp` **未变**(证明没重启)
|
||||||
|
3. **守卫生效** —— 手动连跑 `xray-restart-guard` 两次,第一次应重启并写入时间戳,第二次应打印「跳过」且 xray 未重启
|
||||||
|
4. **关闭** —— `bash deploy.sh --restart 0`,确认 timer / service / guard 三个文件均被删除,状态文件仍在
|
||||||
|
5. **迁移** —— 构造一份只含 `XRAY_DAILY_RESTART=true` 的 `.env`,跑完整部署,确认得到 `XRAY_RESTART_EVERY_DAYS=1` 且 timer 已建立
|
||||||
|
6. **校验** —— `bash deploy.sh --restart 7 25:00` 与 `--restart abc` 均应报错退出,且不留下任何单元文件
|
||||||
|
7. **互斥** —— `bash deploy.sh --restart 7 --redetect` 应报错退出
|
||||||
|
|
||||||
|
## 已知取舍
|
||||||
|
|
||||||
|
- **间隔锚点是「上次实际重启时间」,不是固定日历日期。** 若某次因机器关机错过了触发点,下次开机后的第一个触发点会补上(`Persistent=true`),此后间隔重新从那一刻起算,会缓慢偏移触发日期。这是「真实间隔」语义的必然结果,也是用户选择的语义。
|
||||||
|
- **重启失败时时间戳仍已更新**,本轮被跳过,下一轮照常。理由见上文「先写时间戳」。
|
||||||
|
- **时区错误要到 timer 启动时才暴露**。脚本不预校验时区字符串,靠 `systemctl` 报错——自行维护时区白名单成本高且易过期。
|
||||||
+14
-5
@@ -21,11 +21,20 @@
|
|||||||
# REALITY_DEST=www.apple.com
|
# REALITY_DEST=www.apple.com
|
||||||
# REALITY_SNI=www.apple.com
|
# REALITY_SNI=www.apple.com
|
||||||
|
|
||||||
# 每日 04:00 自动重启 Xray(默认 false)
|
# ===== 定时重启 =====
|
||||||
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级响应),
|
# 0 = 关闭(默认);N = 每 N 天重启一次 Xray。
|
||||||
# 而每日硬重启会切断全部活动连接(跨夜下载、备份、CI 都会中断)。
|
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级响应),而硬重启会切断
|
||||||
# 仅在确实观察到长期运行后性能退化时才改为 true。
|
# 全部活动连接。仅在确实观察到长期运行后性能退化时才启用。
|
||||||
# XRAY_DAILY_RESTART=false
|
# 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 天,其它 → 关闭。重跑部署后会自动换写成新字段。
|
||||||
|
|
||||||
# 备份目录
|
# 备份目录
|
||||||
# BACKUP_DIR=/var/backups/xray
|
# BACKUP_DIR=/var/backups/xray
|
||||||
|
|||||||
+105
-16
@@ -193,6 +193,111 @@ xray tls ping www.apple.com # 只看「Pinging with SNI」那一段
|
|||||||
|
|
||||||
脚本的候选列表因此只保留 Apple 系域名。
|
脚本的候选列表因此只保留 Apple 系域名。
|
||||||
|
|
||||||
|
### 定时重启
|
||||||
|
|
||||||
|
默认**关闭**。原因是:
|
||||||
|
|
||||||
|
- 崩溃恢复已由 systemd 的 `Restart=always` + `RestartSec=3` 覆盖(秒级响应),不必等到凌晨
|
||||||
|
- Xray 无已知需要定期重启的缺陷(内存、句柄上限均已配置)
|
||||||
|
- 而硬重启会切断全部活动连接——跨夜下载、备份、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` |
|
||||||
|
|
||||||
|
#### 查看当前配置与重启记录
|
||||||
|
|
||||||
|
**一条命令看全** —— 不带参数的 `--restart` 只按 `.env` 现值重建 timer,不改配置、不重启服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deploy.sh --restart
|
||||||
|
```
|
||||||
|
|
||||||
|
输出里包含当前间隔与时间、**上次实际重启**、**下次实际重启**。
|
||||||
|
|
||||||
|
分项查看:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 配置值
|
||||||
|
grep '^XRAY_RESTART_' /opt/vps-xray/.env
|
||||||
|
|
||||||
|
# 2. timer 是否启用、下次唤醒时间
|
||||||
|
systemctl list-timers xray-restart.timer
|
||||||
|
|
||||||
|
# 3. 确认时区真的生效(最容易出错的一项)
|
||||||
|
systemctl show xray-restart.timer -p TimersCalendar
|
||||||
|
|
||||||
|
# 4. 上次实际重启的时间戳
|
||||||
|
cat /var/lib/xray/last-restart # Unix 时间戳
|
||||||
|
date -d "@$(cat /var/lib/xray/last-restart)" # 可读格式
|
||||||
|
|
||||||
|
# 5. 守卫每次唤醒后的判断记录(跳过还是执行)
|
||||||
|
journalctl -u xray-restart.service --no-pager -n 50
|
||||||
|
|
||||||
|
# 6. xray 进程最近一次启动的时刻
|
||||||
|
systemctl show xray -p ActiveEnterTimestamp --value
|
||||||
|
```
|
||||||
|
|
||||||
|
第 3 条的输出应形如:
|
||||||
|
|
||||||
|
```
|
||||||
|
{ OnCalendar=*-*-* 04:00:00 Asia/Shanghai ; next_elapse=Fri 2026-08-07 20:00:00 UTC }
|
||||||
|
```
|
||||||
|
|
||||||
|
**里面必须看得到时区。** 若只有 `OnCalendar=*-*-* 04:00:00` 而没有时区,说明时区没生效,systemd 会按服务器的系统时区解析——多数 VPS 是 UTC,那样设的「04:00 北京时间」实际会在**中午 12 点**触发,偏移 8 小时且没有任何提示。
|
||||||
|
|
||||||
|
> 早期实现把时区写成 `[Timer]` 段的 `TimeZone=`,而 `systemd.timer` 根本没有这个 key——写了只会在 journal 里留一行 `Unknown key name 'TimeZone' in section 'Timer', ignoring.` 然后按本地时区解析。排查时可以顺手确认一下:
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> journalctl -b | grep -i "unknown key name"
|
||||||
|
> systemd-analyze verify /etc/systemd/system/xray-restart.timer
|
||||||
|
> ```
|
||||||
|
|
||||||
|
第 5 条的日志长这样:
|
||||||
|
|
||||||
|
```
|
||||||
|
xray-restart-guard[1234]: 距上次重启不足 7 天(已过 3 天),跳过
|
||||||
|
xray-restart-guard[5678]: 距上次重启已满 7 天,执行重启
|
||||||
|
```
|
||||||
|
|
||||||
|
出现下面这行说明状态文件写不进去(磁盘满或只读挂载),守卫会主动跳过本轮——宁可不重启,也不让保护失效后退化成每天硬重启:
|
||||||
|
|
||||||
|
```
|
||||||
|
xray-restart-guard[...]: 无法写入 /var/lib/xray/last-restart,跳过本轮重启以免退化成每日重启
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ `list-timers` 的 `NEXT` 是 **timer 下次「唤醒」的时间,不是下次重启的时间**。timer 每天唤醒一次,NEXT 因此恒为次日的 `XRAY_RESTART_TIME`;是否真的重启由守卫按间隔判断。设了「每 7 天」且 5 天前刚重启过时,NEXT 显示明天凌晨,实际还要再等 2 天。想看真实的下次重启时间,用上面那条不带参数的 `bash deploy.sh --restart`。
|
||||||
|
|
||||||
|
#### 为什么不是纯 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` 同样准确。
|
||||||
|
|
||||||
|
具体怎么查看配置与执行记录,见上面的「查看当前配置与重启记录」。
|
||||||
|
|
||||||
|
#### 已知取舍
|
||||||
|
|
||||||
|
- **间隔锚点是「上次由守卫执行的重启」,不是任何一次 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` 会自动换写成新字段。
|
||||||
|
|
||||||
### 查看状态 / 日志
|
### 查看状态 / 日志
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -245,22 +350,6 @@ bash <(curl -fsSL https://raw.githubusercontent.com/XTLS/Xray-install/main/insta
|
|||||||
systemctl restart xray
|
systemctl restart xray
|
||||||
```
|
```
|
||||||
|
|
||||||
### 每日自动重启(默认关闭)
|
|
||||||
|
|
||||||
早期版本会安装一个每天 04:00 重启 Xray 的 systemd timer,现已**默认关闭**:
|
|
||||||
|
|
||||||
- 崩溃恢复已由 `Restart=always` + `RestartSec=3` 覆盖,响应是秒级,不必等到凌晨
|
|
||||||
- Xray 无已知需要定期重启的缺陷(内存、句柄上限均已配置)
|
|
||||||
- 而每日硬重启会**切断全部活动连接**,跨夜下载、备份、CI 都会中断
|
|
||||||
|
|
||||||
即"为假想问题付出真实代价"。部署时会自动移除旧版本遗留的 timer。
|
|
||||||
|
|
||||||
确有需要(例如实测到长期运行后性能退化)时在 `.env` 中开启:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
XRAY_DAILY_RESTART=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### 重启 / 停止
|
### 重启 / 停止
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+348
-56
@@ -31,8 +31,13 @@ usage() {
|
|||||||
echo " --show 只读取 .env 打印连接信息和分享链接,不改动任何服务"
|
echo " --show 只读取 .env 打印连接信息和分享链接,不改动任何服务"
|
||||||
echo " --redetect 强制重新挑选 Reality 伪装目标(会改变 SNI,"
|
echo " --redetect 强制重新挑选 Reality 伪装目标(会改变 SNI,"
|
||||||
echo " 所有客户端链接都必须同步更新)"
|
echo " 所有客户端链接都必须同步更新)"
|
||||||
|
echo " --restart [N] [HH:MM]"
|
||||||
|
echo " 只修改定时重启计划并立即生效。N=0 关闭。"
|
||||||
|
echo " 不重装 Xray、不重写配置、不重启服务,"
|
||||||
|
echo " 现有 vless 链接完全不受影响。"
|
||||||
|
echo " 例: bash deploy.sh --restart 7 04:00"
|
||||||
echo " -h, --help 显示帮助"
|
echo " -h, --help 显示帮助"
|
||||||
exit 0
|
exit "${1:-0}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ===== 解析参数 =====
|
# ===== 解析参数 =====
|
||||||
@@ -40,16 +45,40 @@ MODE=""
|
|||||||
MODE_EXPLICIT=0
|
MODE_EXPLICIT=0
|
||||||
SHOW_ONLY=0
|
SHOW_ONLY=0
|
||||||
REDETECT=0
|
REDETECT=0
|
||||||
|
RESTART_ONLY=0
|
||||||
|
RESTART_ARG_DAYS=""
|
||||||
|
RESTART_ARG_TIME=""
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--mode) MODE="${2:-}"; MODE_EXPLICIT=1; shift 2 ;;
|
--mode) MODE="${2:-}"; MODE_EXPLICIT=1; shift 2 ;;
|
||||||
--show) SHOW_ONLY=1; shift ;;
|
--show) SHOW_ONLY=1; shift ;;
|
||||||
--redetect) REDETECT=1; shift ;;
|
--redetect) REDETECT=1; shift ;;
|
||||||
|
--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
|
||||||
|
;;
|
||||||
-h|--help) usage ;;
|
-h|--help) usage ;;
|
||||||
*) error "未知参数: $1"; usage ;;
|
*) error "未知参数: $1"; usage 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# --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
|
||||||
|
|
||||||
# ===== 检查 root =====
|
# ===== 检查 root =====
|
||||||
# .env 权限为 600,读取它同样需要 root
|
# .env 权限为 600,读取它同样需要 root
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
@@ -74,12 +103,310 @@ if [[ "$MODE" != "reality" && "$MODE" != "fast" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 布尔开关归一化:容忍 true/TRUE/True/yes/on/1 等写法,
|
# ===== 定时重启配置 =====
|
||||||
# 避免用户按直觉写了 TRUE 却被静默当成关闭
|
# 可覆盖路径,测试时指向临时目录
|
||||||
case "$(printf '%s' "${XRAY_DAILY_RESTART:-false}" | tr '[:upper:]' '[:lower:]')" in
|
SYSTEMD_DIR="${SYSTEMD_DIR:-/etc/systemd/system}"
|
||||||
true|yes|on|1) DAILY_RESTART=1 ;;
|
GUARD_BIN="${GUARD_BIN:-/usr/local/bin/xray-restart-guard}"
|
||||||
*) DAILY_RESTART=0 ;;
|
RESTART_STATE_FILE="${RESTART_STATE_FILE:-/var/lib/xray/last-restart}"
|
||||||
esac
|
|
||||||
|
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
|
||||||
|
# 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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# 就地更新 .env 中的单个字段:存在则替换,不存在则追加。
|
||||||
|
# 刻意不复用 save_env——后者是 cat > 全量重写,会抹掉用户自己加的注释与字段;
|
||||||
|
# 轻量入口只该改自己那一行。
|
||||||
|
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" || return 1
|
||||||
|
else
|
||||||
|
# 文件末尾可能缺换行(用户手工编辑过),直接追加会把新字段黏到上一行
|
||||||
|
if [ -n "$(tail -c 1 "$file")" ]; then
|
||||||
|
printf '\n' >> "$file" || return 1
|
||||||
|
fi
|
||||||
|
printf '%s=%s\n' "$key" "$value" >> "$file" || return 1
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
# 时间戳落在未来(RTC 错乱或 NTP 大幅向后校正时写下的)同样按 0 处理。
|
||||||
|
# 否则时钟校回之后 now - last 恒为负,间隔判断恒成立,此后永远跳过,
|
||||||
|
# 而状态文件只在真正重启时才被覆盖——没有任何自愈路径。
|
||||||
|
[ "\$last" -gt "\$now" ] && 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
|
||||||
|
|
||||||
|
# 必须先写时间戳:restart 会中断本脚本所在的 systemd 事务,
|
||||||
|
# 顺序反了会丢记录,退化成每天都重启。
|
||||||
|
# 代价是重启失败时本轮仍被记为已执行——这比陷入每日重启循环可接受。
|
||||||
|
#
|
||||||
|
# 写不进去(/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
|
||||||
|
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]
|
||||||
|
# 时区必须写在日历表达式里(systemd >= 242 支持)。
|
||||||
|
# [Timer] 段没有 TimeZone= 这个 key——写了只会被忽略并留下一行
|
||||||
|
# "Unknown key name 'TimeZone'",然后按系统本地时区解析。
|
||||||
|
# VPS 出厂多为 UTC,那样「04:00 Asia/Shanghai」会在北京时间 12:00 触发。
|
||||||
|
OnCalendar=*-*-* ${time}:00 ${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
|
||||||
|
# 失败不阻断删除(单元可能本就没被 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"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 依据 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
|
||||||
|
if ! systemctl daemon-reload; then
|
||||||
|
warn "systemctl daemon-reload 失败,已删除的单元可能仍残留在 systemd 内存中"
|
||||||
|
fi
|
||||||
|
log "定时重启: 关闭(崩溃恢复已由 Restart=always 秒级覆盖)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
render_restart_units "$RESTART_EVERY_DAYS" "$RESTART_TIME" "$RESTART_TZ"
|
||||||
|
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:只改重启计划 =====
|
||||||
|
# 明确不做:装依赖、升级 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
|
||||||
|
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
|
||||||
|
if ! update_env_field "$env_file" XRAY_RESTART_TIME "$RESTART_TIME"; then
|
||||||
|
error "写入 ${env_file} 失败,未做任何改动"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
configure_restart_timer
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================================="
|
||||||
|
if [ "$RESTART_EVERY_DAYS" -eq 0 ]; then
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_restart_config || exit 1
|
||||||
|
|
||||||
XRAY_PORT="${XRAY_PORT:-443}"
|
XRAY_PORT="${XRAY_PORT:-443}"
|
||||||
|
|
||||||
@@ -691,47 +1018,7 @@ Restart=always
|
|||||||
RestartSec=3
|
RestartSec=3
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# ===== 每日重启 timer:默认关闭 =====
|
configure_restart_timer
|
||||||
# 崩溃恢复已由上面的 Restart=always + RestartSec=3 覆盖(秒级响应),
|
|
||||||
# 而每日硬重启会切断全部活动连接(跨夜下载、备份、CI 都会中断),
|
|
||||||
# 属于为假想问题付出真实代价。仅在确实观察到长期运行后性能退化时才开启。
|
|
||||||
if [ "$DAILY_RESTART" -ne 1 ]; then
|
|
||||||
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
|
|
||||||
warn "已移除旧部署遗留的每日重启 timer"
|
|
||||||
warn "如需保留,请在 .env 中设置 XRAY_DAILY_RESTART=true 后重新部署"
|
|
||||||
fi
|
|
||||||
log "每日自动重启: 关闭(崩溃由 Restart=always 秒级恢复)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 定时重启 timer(每日凌晨 4 点)
|
|
||||||
cat > /etc/systemd/system/xray-restart.service << 'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=Daily restart of Xray
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/bin/systemctl restart xray
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > /etc/systemd/system/xray-restart.timer << 'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=Daily restart Xray at 04:00 CST
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=*-*-* 04:00:00
|
|
||||||
TimeZone=Asia/Shanghai
|
|
||||||
Persistent=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
EOF
|
|
||||||
warn "每日自动重启: 已启用(04:00 Asia/Shanghai)"
|
|
||||||
warn "该操作会切断所有活动连接,如无必要建议设 XRAY_DAILY_RESTART=false"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ===== 7. 启动服务 =====
|
# ===== 7. 启动服务 =====
|
||||||
@@ -739,9 +1026,6 @@ start_service() {
|
|||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl restart xray
|
systemctl restart xray
|
||||||
systemctl enable xray
|
systemctl enable xray
|
||||||
if [ "$DAILY_RESTART" -eq 1 ]; then
|
|
||||||
systemctl enable --now xray-restart.timer
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
if systemctl is-active --quiet xray; then
|
if systemctl is-active --quiet xray; then
|
||||||
@@ -766,10 +1050,13 @@ XRAY_UUID=${XRAY_UUID}
|
|||||||
# NAT / 端口转发的机器请填写服务商给你的实际入站 IP
|
# NAT / 端口转发的机器请填写服务商给你的实际入站 IP
|
||||||
SERVER_IP=${SERVER_IP:-}
|
SERVER_IP=${SERVER_IP:-}
|
||||||
|
|
||||||
# 每日 04:00 自动重启 Xray。默认 false。
|
# 定时重启。0 = 关闭(默认);N = 每 N 天在 XRAY_RESTART_TIME 重启一次。
|
||||||
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级),而每日硬重启会
|
# 崩溃恢复已由 systemd 的 Restart=always 覆盖(秒级),而硬重启会切断
|
||||||
# 切断全部活动连接。仅在确实观察到长期运行后性能退化时才改为 true。
|
# 全部活动连接。仅在确实观察到长期运行后性能退化时才启用。
|
||||||
XRAY_DAILY_RESTART=$([ "$DAILY_RESTART" -eq 1 ] && echo true || echo false)
|
# 改这几项无需完整重新部署: bash deploy.sh --restart 7 04:00
|
||||||
|
XRAY_RESTART_EVERY_DAYS=${RESTART_EVERY_DAYS}
|
||||||
|
XRAY_RESTART_TIME=${RESTART_TIME}
|
||||||
|
XRAY_RESTART_TIMEZONE=${RESTART_TZ}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if [ "$MODE" = "reality" ]; then
|
if [ "$MODE" = "reality" ]; then
|
||||||
@@ -906,6 +1193,11 @@ main() {
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$RESTART_ONLY" -eq 1 ]; then
|
||||||
|
apply_restart_only
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
log "部署模式: ${MODE}"
|
log "部署模式: ${MODE}"
|
||||||
[ "$MODE" = "reality" ] && log "VLESS-Reality(推荐,抗封锁)" || log "VLESS TCP(极速,无 TLS)"
|
[ "$MODE" = "reality" ] && log "VLESS-Reality(推荐,抗封锁)" || log "VLESS TCP(极速,无 TLS)"
|
||||||
|
|||||||
@@ -0,0 +1,468 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy.sh 中重启计划相关逻辑的单元测试
|
||||||
|
#
|
||||||
|
# 用法: bash vps-xray/tests/test-restart-schedule.sh
|
||||||
|
#
|
||||||
|
# 不需要 root,不接触真实 systemd:所有写入路径都被重定向到临时目录,
|
||||||
|
# systemctl 用 PATH 打桩拦截。
|
||||||
|
#
|
||||||
|
# 加载方式说明:deploy.sh 结尾会直接调用 main,且顶层有 root 检查与参数解析,
|
||||||
|
# 不能整份 source。这里只抽取「多行函数定义」(形如 `name() {` 开头、`}` 结尾)。
|
||||||
|
# 单行函数(log/warn/error/step)抽不到,正好由下面的桩覆盖。
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEPLOY="${TEST_DIR}/../deploy.sh"
|
||||||
|
|
||||||
|
if [ ! -f "$DEPLOY" ]; then
|
||||||
|
echo "找不到 $DEPLOY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
eval "$(sed -n '/^[a-z_][a-z_]*() {$/,/^}$/p' "$DEPLOY")"
|
||||||
|
|
||||||
|
# 日志桩:error 保留输出便于断言,其余静音
|
||||||
|
log() { :; }
|
||||||
|
warn() { :; }
|
||||||
|
step() { :; }
|
||||||
|
error() { echo "ERROR: $*" >&2; }
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
ok() { PASS=$((PASS + 1)); echo " ok - $1"; }
|
||||||
|
bad() { FAIL=$((FAIL + 1)); echo " FAIL - $1"; echo " 期望: $2"; echo " 实际: $3"; }
|
||||||
|
# is <描述> <实际值> <期望值>
|
||||||
|
is() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1" "$3" "$2"; fi; }
|
||||||
|
|
||||||
|
echo "== normalize_restart_time =="
|
||||||
|
is "04:00 原样通过" "$(normalize_restart_time 04:00)" "04:00"
|
||||||
|
is "4:00 补零" "$(normalize_restart_time 4:00)" "04:00"
|
||||||
|
is "08:30 不被当八进制" "$(normalize_restart_time 08:30)" "08:30"
|
||||||
|
is "23:59 边界" "$(normalize_restart_time 23:59)" "23:59"
|
||||||
|
is "00:00 边界" "$(normalize_restart_time 00:00)" "00:00"
|
||||||
|
|
||||||
|
normalize_restart_time "24:00" >/dev/null 2>&1
|
||||||
|
is "24:00 被拒" "$?" "1"
|
||||||
|
normalize_restart_time "04:60" >/dev/null 2>&1
|
||||||
|
is "04:60 被拒" "$?" "1"
|
||||||
|
normalize_restart_time "abc" >/dev/null 2>&1
|
||||||
|
is "abc 被拒" "$?" "1"
|
||||||
|
normalize_restart_time "" >/dev/null 2>&1
|
||||||
|
is "空串被拒" "$?" "1"
|
||||||
|
|
||||||
|
echo "== resolve_restart_config =="
|
||||||
|
|
||||||
|
reset_env() {
|
||||||
|
unset XRAY_RESTART_EVERY_DAYS XRAY_RESTART_TIME XRAY_RESTART_TIMEZONE XRAY_DAILY_RESTART
|
||||||
|
RESTART_EVERY_DAYS=""; RESTART_TIME=""; RESTART_TZ=""
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_env
|
||||||
|
resolve_restart_config
|
||||||
|
is "全空时天数默认 0" "$RESTART_EVERY_DAYS" "0"
|
||||||
|
is "全空时时间默认" "$RESTART_TIME" "04:00"
|
||||||
|
is "全空时时区默认" "$RESTART_TZ" "Asia/Shanghai"
|
||||||
|
|
||||||
|
reset_env; XRAY_RESTART_EVERY_DAYS=7; XRAY_RESTART_TIME=5:30
|
||||||
|
resolve_restart_config
|
||||||
|
is "读取新字段天数" "$RESTART_EVERY_DAYS" "7"
|
||||||
|
is "读取新字段时间并补零" "$RESTART_TIME" "05:30"
|
||||||
|
|
||||||
|
reset_env; XRAY_DAILY_RESTART=true
|
||||||
|
resolve_restart_config
|
||||||
|
is "迁移 true → 1" "$RESTART_EVERY_DAYS" "1"
|
||||||
|
|
||||||
|
reset_env; XRAY_DAILY_RESTART=TRUE
|
||||||
|
resolve_restart_config
|
||||||
|
is "迁移 TRUE → 1" "$RESTART_EVERY_DAYS" "1"
|
||||||
|
|
||||||
|
reset_env; XRAY_DAILY_RESTART=false
|
||||||
|
resolve_restart_config
|
||||||
|
is "迁移 false → 0" "$RESTART_EVERY_DAYS" "0"
|
||||||
|
|
||||||
|
reset_env; XRAY_DAILY_RESTART=true; XRAY_RESTART_EVERY_DAYS=3
|
||||||
|
resolve_restart_config
|
||||||
|
is "新字段优先于旧字段" "$RESTART_EVERY_DAYS" "3"
|
||||||
|
|
||||||
|
reset_env; XRAY_DAILY_RESTART=true; XRAY_RESTART_EVERY_DAYS=0
|
||||||
|
resolve_restart_config
|
||||||
|
is "新字段显式 0 覆盖旧 true" "$RESTART_EVERY_DAYS" "0"
|
||||||
|
|
||||||
|
# 前导零必须按十进制归一化:这个值会原样写进 guard 的 DAYS=,
|
||||||
|
# 而 guard 里是算术展开——010 会变成 8 天,08/09 直接报错导致每天重启
|
||||||
|
reset_env; XRAY_RESTART_EVERY_DAYS=08
|
||||||
|
resolve_restart_config
|
||||||
|
is "08 归一化为 8(不当八进制)" "$RESTART_EVERY_DAYS" "8"
|
||||||
|
|
||||||
|
reset_env; XRAY_RESTART_EVERY_DAYS=09
|
||||||
|
resolve_restart_config
|
||||||
|
is "09 归一化为 9(不当八进制)" "$RESTART_EVERY_DAYS" "9"
|
||||||
|
|
||||||
|
reset_env; XRAY_RESTART_EVERY_DAYS=010
|
||||||
|
resolve_restart_config
|
||||||
|
is "010 归一化为 10(不当八进制)" "$RESTART_EVERY_DAYS" "10"
|
||||||
|
|
||||||
|
reset_env; XRAY_RESTART_EVERY_DAYS=abc
|
||||||
|
resolve_restart_config 2>/dev/null
|
||||||
|
is "天数非数字报错" "$?" "1"
|
||||||
|
|
||||||
|
reset_env; XRAY_RESTART_EVERY_DAYS=-1
|
||||||
|
resolve_restart_config 2>/dev/null
|
||||||
|
is "天数负数报错" "$?" "1"
|
||||||
|
|
||||||
|
reset_env; XRAY_RESTART_TIME=25:00
|
||||||
|
resolve_restart_config 2>/dev/null
|
||||||
|
is "时间非法报错" "$?" "1"
|
||||||
|
|
||||||
|
reset_env; XRAY_RESTART_TIMEZONE=""
|
||||||
|
resolve_restart_config 2>/dev/null
|
||||||
|
is "时区空串报错" "$?" "1"
|
||||||
|
|
||||||
|
echo "== update_env_field =="
|
||||||
|
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
cat > "${TMP}/.env" << 'ENVEOF'
|
||||||
|
# 这是用户自己加的注释
|
||||||
|
XRAY_PORT=443
|
||||||
|
XRAY_RESTART_EVERY_DAYS=0
|
||||||
|
# 尾部注释
|
||||||
|
ENVEOF
|
||||||
|
|
||||||
|
update_env_field "${TMP}/.env" XRAY_RESTART_EVERY_DAYS 7
|
||||||
|
is "替换已有字段" "$(grep '^XRAY_RESTART_EVERY_DAYS=' "${TMP}/.env")" "XRAY_RESTART_EVERY_DAYS=7"
|
||||||
|
is "保留用户注释" "$(grep -c '这是用户自己加的注释' "${TMP}/.env")" "1"
|
||||||
|
is "保留尾部注释" "$(grep -c '尾部注释' "${TMP}/.env")" "1"
|
||||||
|
is "不影响其它字段" "$(grep '^XRAY_PORT=' "${TMP}/.env")" "XRAY_PORT=443"
|
||||||
|
|
||||||
|
update_env_field "${TMP}/.env" XRAY_RESTART_TIME 05:30
|
||||||
|
is "追加缺失字段" "$(grep '^XRAY_RESTART_TIME=' "${TMP}/.env")" "XRAY_RESTART_TIME=05:30"
|
||||||
|
|
||||||
|
update_env_field "${TMP}/.env" XRAY_RESTART_TIMEZONE Asia/Shanghai
|
||||||
|
is "含斜杠的值不被 sed 分隔符干扰" \
|
||||||
|
"$(grep '^XRAY_RESTART_TIMEZONE=' "${TMP}/.env")" "XRAY_RESTART_TIMEZONE=Asia/Shanghai"
|
||||||
|
|
||||||
|
update_env_field "${TMP}/.env" XRAY_RESTART_TIMEZONE Europe/Berlin
|
||||||
|
is "含斜杠的值可被再次替换" \
|
||||||
|
"$(grep '^XRAY_RESTART_TIMEZONE=' "${TMP}/.env")" "XRAY_RESTART_TIMEZONE=Europe/Berlin"
|
||||||
|
|
||||||
|
is "字段总行数正确" "$(grep -c '^XRAY_RESTART_TIMEZONE=' "${TMP}/.env")" "1"
|
||||||
|
|
||||||
|
update_env_field "${TMP}/nonexistent" FOO bar 2>/dev/null
|
||||||
|
is "文件不存在返回 1" "$?" "1"
|
||||||
|
|
||||||
|
printf 'KEY=old' > "${TMP}/.env2"
|
||||||
|
update_env_field "${TMP}/.env2" NEWKEY val
|
||||||
|
is "缺尾换行时不黏连" "$(grep -c '^NEWKEY=val$' "${TMP}/.env2")" "1"
|
||||||
|
is "缺尾换行时原字段完好" "$(grep -c '^KEY=old$' "${TMP}/.env2")" "1"
|
||||||
|
|
||||||
|
echo "== render_restart_units =="
|
||||||
|
|
||||||
|
SYSTEMD_DIR="${TMP}/systemd"
|
||||||
|
GUARD_BIN="${TMP}/bin/xray-restart-guard"
|
||||||
|
RESTART_STATE_FILE="${TMP}/state/last-restart"
|
||||||
|
mkdir -p "${TMP}/systemd" "${TMP}/bin" "${TMP}/state"
|
||||||
|
|
||||||
|
# systemctl 打桩:记录被调用的参数,避免碰真实 systemd
|
||||||
|
STUB="${TMP}/stub"
|
||||||
|
mkdir -p "$STUB"
|
||||||
|
cat > "${STUB}/systemctl" << 'STUBEOF'
|
||||||
|
#!/bin/sh
|
||||||
|
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
|
||||||
|
STUBEOF
|
||||||
|
chmod 755 "${STUB}/systemctl"
|
||||||
|
export SYSTEMCTL_LOG="${TMP}/systemctl.log"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
export PATH="${STUB}:${PATH}"
|
||||||
|
|
||||||
|
render_restart_units 7 04:00 Asia/Shanghai
|
||||||
|
|
||||||
|
is "timer 文件存在" "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes)" "yes"
|
||||||
|
is "service 文件存在" "$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes)" "yes"
|
||||||
|
is "guard 可执行" "$([ -x "$GUARD_BIN" ] && echo yes)" "yes"
|
||||||
|
|
||||||
|
# 时区必须写在日历表达式里:[Timer] 段没有 TimeZone= 这个 key,
|
||||||
|
# 写了会被 systemd 忽略并按系统本地时区(VPS 多为 UTC)解析,静默偏移 8 小时
|
||||||
|
is "OnCalendar 每天触发且带时区" \
|
||||||
|
"$(grep -c '^OnCalendar=\*-\*-\* 04:00:00 Asia/Shanghai$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
|
||||||
|
is "不再使用非法的 TimeZone= 指令" \
|
||||||
|
"$(grep -c '^TimeZone=' "${SYSTEMD_DIR}/xray-restart.timer")" "0"
|
||||||
|
is "Persistent 开启" \
|
||||||
|
"$(grep -c '^Persistent=true$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
|
||||||
|
is "service 指向 guard" \
|
||||||
|
"$(grep -c "^ExecStart=${GUARD_BIN}\$" "${SYSTEMD_DIR}/xray-restart.service")" "1"
|
||||||
|
is "guard 里天数正确" \
|
||||||
|
"$(grep -c '^DAYS=7$' "$GUARD_BIN")" "1"
|
||||||
|
is "guard 里状态文件路径正确" \
|
||||||
|
"$(grep -c "^STATE=\"${RESTART_STATE_FILE}\"\$" "$GUARD_BIN")" "1"
|
||||||
|
is "guard 用 PATH 查找 systemctl 而非绝对路径" \
|
||||||
|
"$(grep -c '^exec systemctl restart xray$' "$GUARD_BIN")" "1"
|
||||||
|
|
||||||
|
render_restart_units 3 23:05 Europe/Berlin
|
||||||
|
is "重复渲染覆盖天数" "$(grep -c '^DAYS=3$' "$GUARD_BIN")" "1"
|
||||||
|
is "重复渲染覆盖时间与时区" \
|
||||||
|
"$(grep -c '^OnCalendar=\*-\*-\* 23:05:00 Europe/Berlin$' "${SYSTEMD_DIR}/xray-restart.timer")" "1"
|
||||||
|
|
||||||
|
remove_restart_units
|
||||||
|
is "remove 后 timer 消失" "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "no"
|
||||||
|
is "remove 后 service 消失" "$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes || echo no)" "no"
|
||||||
|
is "remove 后 guard 消失" "$([ -f "$GUARD_BIN" ] && echo yes || echo no)" "no"
|
||||||
|
|
||||||
|
remove_restart_units
|
||||||
|
is "重复 remove 返回 1" "$?" "1"
|
||||||
|
|
||||||
|
echo "== guard 脚本行为 =="
|
||||||
|
|
||||||
|
render_restart_units 7 04:00 Asia/Shanghai
|
||||||
|
rm -f "$RESTART_STATE_FILE"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
|
||||||
|
# 无状态文件 → 首次必须执行
|
||||||
|
out=$("$GUARD_BIN" 2>&1)
|
||||||
|
is "首次执行重启" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "1"
|
||||||
|
is "首次写入时间戳" "$([ -f "$RESTART_STATE_FILE" ] && echo yes)" "yes"
|
||||||
|
|
||||||
|
# 刚重启过 → 必须跳过
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
out=$("$GUARD_BIN" 2>&1)
|
||||||
|
is "刚重启过则跳过" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||||
|
is "跳过时有提示" "$(echo "$out" | grep -c '跳过')" "1"
|
||||||
|
|
||||||
|
# 距上次 6 天 → 仍未满 7 天,跳过
|
||||||
|
echo "$(( $(date +%s) - 6 * 86400 ))" > "$RESTART_STATE_FILE"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
"$GUARD_BIN" > /dev/null 2>&1
|
||||||
|
is "6 天不足 7 天则跳过" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||||
|
|
||||||
|
# 距上次 7 天差 100 秒 → 300 秒容差内,必须执行
|
||||||
|
echo "$(( $(date +%s) - 7 * 86400 + 100 ))" > "$RESTART_STATE_FILE"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
"$GUARD_BIN" > /dev/null 2>&1
|
||||||
|
is "容差内视为已满 7 天" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "1"
|
||||||
|
|
||||||
|
# 距上次 7 天差 600 秒 → 超出容差,跳过
|
||||||
|
echo "$(( $(date +%s) - 7 * 86400 + 600 ))" > "$RESTART_STATE_FILE"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
"$GUARD_BIN" > /dev/null 2>&1
|
||||||
|
is "超出容差仍判不足" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||||
|
|
||||||
|
# 状态文件内容被写坏 → 按 0 处理,立即执行
|
||||||
|
echo "garbage" > "$RESTART_STATE_FILE"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
"$GUARD_BIN" > /dev/null 2>&1
|
||||||
|
is "状态文件非法时立即执行" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "1"
|
||||||
|
is "非法内容被覆盖为数字" \
|
||||||
|
"$(grep -cE '^[0-9]+$' "$RESTART_STATE_FILE")" "1"
|
||||||
|
|
||||||
|
echo "garbage" > "$RESTART_STATE_FILE"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
guard_err=$("$GUARD_BIN" 2>&1 >/dev/null)
|
||||||
|
is "状态文件非法时不产生算术求值报错" \
|
||||||
|
"$(printf '%s' "$guard_err" | grep -c 'integer expression')" "0"
|
||||||
|
|
||||||
|
# 时间戳落在未来(RTC 错乱 / NTP 大幅向后校正)→ 必须按 0 处理立即执行,
|
||||||
|
# 否则时钟校回后 now-last 恒为负,此后永远跳过且无自愈路径
|
||||||
|
echo "$(( $(date +%s) + 30 * 86400 ))" > "$RESTART_STATE_FILE"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
"$GUARD_BIN" > /dev/null 2>&1
|
||||||
|
is "未来时间戳不导致永久跳过" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "1"
|
||||||
|
is "未来时间戳被覆盖为当前时间" \
|
||||||
|
"$([ "$(cat "$RESTART_STATE_FILE")" -le "$(date +%s)" ] && echo yes || echo no)" "yes"
|
||||||
|
|
||||||
|
# 状态文件写不进去 → 必须跳过本轮。照常重启会让下一轮依旧读不到时间戳,
|
||||||
|
# 退化成每天硬重启且无限持续
|
||||||
|
guard_saved_state="$RESTART_STATE_FILE"
|
||||||
|
|
||||||
|
# (a) 目录建不出来:拿一个普通文件占住父目录的位置
|
||||||
|
: > "${TMP}/blocked"
|
||||||
|
RESTART_STATE_FILE="${TMP}/blocked/last-restart"
|
||||||
|
render_restart_units 7 04:00 Asia/Shanghai
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
"$GUARD_BIN" > /dev/null 2>&1
|
||||||
|
is "状态目录建不出来时跳过重启" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||||
|
|
||||||
|
# (b) 目录能建但文件写不进去:让状态文件路径本身是个目录
|
||||||
|
mkdir -p "${TMP}/asdir/last-restart"
|
||||||
|
RESTART_STATE_FILE="${TMP}/asdir/last-restart"
|
||||||
|
render_restart_units 7 04:00 Asia/Shanghai
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
"$GUARD_BIN" > /dev/null 2>&1
|
||||||
|
is "时间戳写不进去时跳过重启" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||||
|
|
||||||
|
RESTART_STATE_FILE="$guard_saved_state"
|
||||||
|
|
||||||
|
# 每日模式:距上次 1 天即执行
|
||||||
|
render_restart_units 1 04:00 Asia/Shanghai
|
||||||
|
echo "$(( $(date +%s) - 86400 ))" > "$RESTART_STATE_FILE"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
"$GUARD_BIN" > /dev/null 2>&1
|
||||||
|
is "N=1 时满一天即执行" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "1"
|
||||||
|
|
||||||
|
# 时间戳先于重启写入:用一个必定失败的 systemctl 桩验证
|
||||||
|
cat > "${STUB}/systemctl" << 'STUBEOF'
|
||||||
|
#!/bin/sh
|
||||||
|
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
|
||||||
|
exit 1
|
||||||
|
STUBEOF
|
||||||
|
chmod 755 "${STUB}/systemctl"
|
||||||
|
rm -f "$RESTART_STATE_FILE"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
"$GUARD_BIN" > /dev/null 2>&1
|
||||||
|
is "重启失败时时间戳仍已写入" "$([ -s "$RESTART_STATE_FILE" ] && echo yes)" "yes"
|
||||||
|
|
||||||
|
# 恢复正常桩,避免影响后续
|
||||||
|
cat > "${STUB}/systemctl" << 'STUBEOF'
|
||||||
|
#!/bin/sh
|
||||||
|
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
|
||||||
|
STUBEOF
|
||||||
|
chmod 755 "${STUB}/systemctl"
|
||||||
|
|
||||||
|
echo "== configure_restart_timer =="
|
||||||
|
|
||||||
|
# 启用方向
|
||||||
|
RESTART_EVERY_DAYS=7
|
||||||
|
RESTART_TIME="04:00"
|
||||||
|
RESTART_TZ="Asia/Shanghai"
|
||||||
|
rm -f "${SYSTEMD_DIR}/xray-restart.timer" "${SYSTEMD_DIR}/xray-restart.service" "$GUARD_BIN"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
configure_restart_timer > /dev/null 2>&1
|
||||||
|
is "启用时渲染出 timer" \
|
||||||
|
"$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "yes"
|
||||||
|
is "启用时渲染出 guard" "$([ -x "$GUARD_BIN" ] && echo yes || echo no)" "yes"
|
||||||
|
is "启用时执行 daemon-reload" \
|
||||||
|
"$(grep -c '^systemctl daemon-reload$' "$SYSTEMCTL_LOG")" "1"
|
||||||
|
is "启用时执行 enable --now" \
|
||||||
|
"$(grep -c '^systemctl enable --now xray-restart.timer$' "$SYSTEMCTL_LOG")" "1"
|
||||||
|
is "启用时不重启 xray" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||||
|
|
||||||
|
# enable 失败时不得中断调用方:configure_restart_timer 在部署主干上被裸调用
|
||||||
|
cat > "${STUB}/systemctl" << 'STUBEOF'
|
||||||
|
#!/bin/sh
|
||||||
|
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
|
||||||
|
case "$1" in
|
||||||
|
enable) exit 1 ;;
|
||||||
|
esac
|
||||||
|
STUBEOF
|
||||||
|
chmod 755 "${STUB}/systemctl"
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
configure_restart_timer > /dev/null 2>&1
|
||||||
|
is "enable 失败时仍返回 0(不中断部署主干)" "$?" "0"
|
||||||
|
cat > "${STUB}/systemctl" << 'STUBEOF'
|
||||||
|
#!/bin/sh
|
||||||
|
echo "systemctl $*" >> "${SYSTEMCTL_LOG}"
|
||||||
|
STUBEOF
|
||||||
|
chmod 755 "${STUB}/systemctl"
|
||||||
|
|
||||||
|
# 关闭方向
|
||||||
|
echo "12345" > "$RESTART_STATE_FILE"
|
||||||
|
RESTART_EVERY_DAYS=0
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
configure_restart_timer > /dev/null 2>&1
|
||||||
|
is "关闭时删除 timer" \
|
||||||
|
"$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "no"
|
||||||
|
is "关闭时删除 service" \
|
||||||
|
"$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes || echo no)" "no"
|
||||||
|
is "关闭时删除 guard" "$([ -f "$GUARD_BIN" ] && echo yes || echo no)" "no"
|
||||||
|
is "关闭时保留状态文件" "$(cat "$RESTART_STATE_FILE")" "12345"
|
||||||
|
is "关闭时执行 disable --now" \
|
||||||
|
"$(grep -c '^systemctl disable --now xray-restart.timer$' "$SYSTEMCTL_LOG")" "1"
|
||||||
|
is "关闭时不重启 xray" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||||
|
|
||||||
|
echo "== apply_restart_only 安全不变式 =="
|
||||||
|
|
||||||
|
# 这一节守护本特性最核心的承诺:--restart 不重启 xray、不碰 vless 链接。
|
||||||
|
# 缺了它,谁在 apply_restart_only 里加一行 systemctl restart xray 都不会被发现。
|
||||||
|
APP_DIR="${TMP}/app"
|
||||||
|
mkdir -p "$APP_DIR"
|
||||||
|
SCRIPT_DIR="$APP_DIR"
|
||||||
|
|
||||||
|
make_app_env() {
|
||||||
|
cat > "${APP_DIR}/.env" << 'ENVEOF'
|
||||||
|
# Xray 部署配置(由 deploy.sh 自动生成)
|
||||||
|
|
||||||
|
XRAY_MODE=reality
|
||||||
|
XRAY_PORT=443
|
||||||
|
XRAY_UUID=11111111-2222-3333-4444-555555555555
|
||||||
|
# 这是用户自己加的注释,不许动
|
||||||
|
SERVER_IP=
|
||||||
|
XRAY_RESTART_EVERY_DAYS=0
|
||||||
|
XRAY_RESTART_TIME=04:00
|
||||||
|
XRAY_RESTART_TIMEZONE=Asia/Shanghai
|
||||||
|
XRAY_PUBLIC_KEY=pubkeyzzz
|
||||||
|
XRAY_SHORT_ID=abcdef0123456789
|
||||||
|
REALITY_SNI=www.apple.com
|
||||||
|
ENVEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 启用:--restart 7 5:30 ---
|
||||||
|
reset_env
|
||||||
|
make_app_env
|
||||||
|
cp "${APP_DIR}/.env" "${TMP}/env.before"
|
||||||
|
RESTART_ARG_DAYS=7
|
||||||
|
RESTART_ARG_TIME=5:30
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
apply_restart_only > /dev/null 2>&1
|
||||||
|
|
||||||
|
is "apply_restart_only 绝不重启 xray" \
|
||||||
|
"$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||||
|
is "天数写回 .env" "$(grep '^XRAY_RESTART_EVERY_DAYS=' "${APP_DIR}/.env")" "XRAY_RESTART_EVERY_DAYS=7"
|
||||||
|
is "时间归一化后写回" "$(grep '^XRAY_RESTART_TIME=' "${APP_DIR}/.env")" "XRAY_RESTART_TIME=05:30"
|
||||||
|
is ".env 只有这两行发生变化" \
|
||||||
|
"$(diff "${TMP}/env.before" "${APP_DIR}/.env" | grep -c '^[<>]')" "4"
|
||||||
|
is "用户注释原样保留" "$(grep -c '这是用户自己加的注释,不许动' "${APP_DIR}/.env")" "1"
|
||||||
|
is "UUID 未被改动" "$(grep -c '^XRAY_UUID=11111111-2222-3333-4444-555555555555$' "${APP_DIR}/.env")" "1"
|
||||||
|
is "SNI 未被改动" "$(grep -c '^REALITY_SNI=www\.apple\.com$' "${APP_DIR}/.env")" "1"
|
||||||
|
is "PublicKey 未被改动" "$(grep -c '^XRAY_PUBLIC_KEY=pubkeyzzz$' "${APP_DIR}/.env")" "1"
|
||||||
|
is "启用后 timer 已就位" \
|
||||||
|
"$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "yes"
|
||||||
|
|
||||||
|
# --- 不带位置参数:.env 必须完全不变 ---
|
||||||
|
reset_env
|
||||||
|
make_app_env
|
||||||
|
XRAY_RESTART_EVERY_DAYS=7
|
||||||
|
XRAY_RESTART_TIME=04:00
|
||||||
|
cp "${APP_DIR}/.env" "${TMP}/env.before2"
|
||||||
|
RESTART_ARG_DAYS=""
|
||||||
|
RESTART_ARG_TIME=""
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
apply_restart_only > /dev/null 2>&1
|
||||||
|
is "不带位置参数时 .env 一字不改" \
|
||||||
|
"$(diff "${TMP}/env.before2" "${APP_DIR}/.env" > /dev/null && echo same || echo differ)" "same"
|
||||||
|
is "不带位置参数时也不重启 xray" \
|
||||||
|
"$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||||
|
|
||||||
|
# --- 关闭:--restart 0 ---
|
||||||
|
reset_env
|
||||||
|
echo "12345" > "$RESTART_STATE_FILE"
|
||||||
|
RESTART_ARG_DAYS=0
|
||||||
|
RESTART_ARG_TIME=""
|
||||||
|
: > "$SYSTEMCTL_LOG"
|
||||||
|
apply_restart_only > /dev/null 2>&1
|
||||||
|
is "关闭后 timer 被删" "$([ -f "${SYSTEMD_DIR}/xray-restart.timer" ] && echo yes || echo no)" "no"
|
||||||
|
is "关闭后 service 被删" "$([ -f "${SYSTEMD_DIR}/xray-restart.service" ] && echo yes || echo no)" "no"
|
||||||
|
is "关闭后 guard 被删" "$([ -f "$GUARD_BIN" ] && echo yes || echo no)" "no"
|
||||||
|
is "关闭后状态文件保留" "$(cat "$RESTART_STATE_FILE")" "12345"
|
||||||
|
is "关闭时天数写回 .env" "$(grep '^XRAY_RESTART_EVERY_DAYS=' "${APP_DIR}/.env")" "XRAY_RESTART_EVERY_DAYS=0"
|
||||||
|
is "关闭时也不重启 xray" "$(grep -c '^systemctl restart xray$' "$SYSTEMCTL_LOG")" "0"
|
||||||
|
|
||||||
|
echo "== print_next_restart =="
|
||||||
|
|
||||||
|
RESTART_EVERY_DAYS=7
|
||||||
|
rm -f "$RESTART_STATE_FILE"
|
||||||
|
is "无状态文件时提示下次唤醒即重启" \
|
||||||
|
"$(print_next_restart | grep -c '下次唤醒时即会重启')" "1"
|
||||||
|
|
||||||
|
now_ts=$(date +%s)
|
||||||
|
echo "$(( now_ts - 6 * 86400 ))" > "$RESTART_STATE_FILE"
|
||||||
|
is "未满 N 天时给出的是真实重启日期而非明天的唤醒时间" \
|
||||||
|
"$(print_next_restart | grep -c "下次实际重启: $(date -d "@$(( now_ts + 86400 ))" '+%Y-%m-%d')")" "1"
|
||||||
|
|
||||||
|
echo "$(( $(date +%s) - 9 * 86400 ))" > "$RESTART_STATE_FILE"
|
||||||
|
is "已满 N 天时提示下次唤醒即重启" \
|
||||||
|
"$(print_next_restart | grep -c '下次唤醒时即会重启')" "1"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "通过 ${PASS},失败 ${FAIL}"
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# 定时重启功能的真实环境验收脚本
|
||||||
|
#
|
||||||
|
# 用法(在 VPS 上,root):
|
||||||
|
# cd /opt/vps-xray && bash tests/verify-restart-on-vps.sh
|
||||||
|
#
|
||||||
|
# 为什么必须在真机上跑:本地的 62→101 项单元测试都是把生成的文件写进临时目录
|
||||||
|
# 再比对内容——它们能验证「我们写出了预期的字节」,但验证不了「systemd 认不认
|
||||||
|
# 这些字节」。TimeZone= 那个缺陷就是这么漏过去的:文件里确实有那一行,断言通过,
|
||||||
|
# 而 systemd 加载时只会记一条 Unknown key name 然后按系统本地时区解析。
|
||||||
|
#
|
||||||
|
# 本脚本只做安全操作:不升级 Xray、不重写 config.json、不重启 xray。
|
||||||
|
# 会临时改动 .env 的重启字段与三个 systemd 单元文件,结束时恢复原状。
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
ok() { PASS=$((PASS+1)); echo " ✅ $1"; }
|
||||||
|
bad() { FAIL=$((FAIL+1)); echo " ❌ $1"; [ -n "${2:-}" ] && echo " 实际: $2"; }
|
||||||
|
sec() { echo ""; echo "===== $* ====="; }
|
||||||
|
|
||||||
|
[ "$(id -u)" -eq 0 ] || { echo "需要 root"; exit 1; }
|
||||||
|
[ -f .env ] || { echo "找不到 .env,这台机器还没部署过"; exit 1; }
|
||||||
|
|
||||||
|
ENV_BAK="$(mktemp)"; cp .env "$ENV_BAK"
|
||||||
|
restore() {
|
||||||
|
echo ""
|
||||||
|
echo "===== 恢复原状 ====="
|
||||||
|
cp "$ENV_BAK" .env && echo " .env 已还原"
|
||||||
|
rm -f "$ENV_BAK"
|
||||||
|
bash deploy.sh --restart >/dev/null 2>&1 && echo " timer 已按还原后的 .env 重建" \
|
||||||
|
|| echo " timer 重建失败,请手工检查 systemctl list-timers xray-restart.timer"
|
||||||
|
}
|
||||||
|
trap restore EXIT
|
||||||
|
|
||||||
|
sec "0. 环境"
|
||||||
|
systemctl --version | head -1
|
||||||
|
timedatectl 2>/dev/null | grep -i "time zone" || cat /etc/timezone 2>/dev/null
|
||||||
|
|
||||||
|
sec "1. Critical-1:OnCalendar 尾随时区是否被 systemd 真正接受"
|
||||||
|
# 这是全套验证里最关键的一条。旧实现用 [Timer] 段的 TimeZone=,
|
||||||
|
# 而 systemd.timer 根本没有这个 key——写了会被忽略并按系统本地时区解析。
|
||||||
|
if out=$(systemd-analyze calendar "*-*-* 04:00:00 Asia/Shanghai" 2>&1); then
|
||||||
|
ok "systemd 接受带时区的日历表达式"
|
||||||
|
echo "$out" | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
bad "systemd 不接受带时区的日历表达式(该 systemd 版本可能低于 242)" "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sec "2. 启用:--restart 7 04:00"
|
||||||
|
BEFORE_TS=$(systemctl show xray -p ActiveEnterTimestamp --value)
|
||||||
|
bash deploy.sh --restart 7 04:00
|
||||||
|
AFTER_TS=$(systemctl show xray -p ActiveEnterTimestamp --value)
|
||||||
|
|
||||||
|
[ "$BEFORE_TS" = "$AFTER_TS" ] \
|
||||||
|
&& ok "xray 未被重启(ActiveEnterTimestamp 前后一致)" \
|
||||||
|
|| bad "xray 被重启了!--restart 不该碰服务" "before=$BEFORE_TS after=$AFTER_TS"
|
||||||
|
|
||||||
|
grep -q '^XRAY_RESTART_EVERY_DAYS=7$' .env && ok ".env 天数已写入" || bad ".env 天数未写入" "$(grep '^XRAY_RESTART_EVERY_DAYS=' .env)"
|
||||||
|
grep -q '^XRAY_RESTART_TIME=04:00$' .env && ok ".env 时间已写入" || bad ".env 时间未写入" "$(grep '^XRAY_RESTART_TIME=' .env)"
|
||||||
|
|
||||||
|
sec "3. 生成的 timer 是否被 systemd 正确加载"
|
||||||
|
systemd-analyze verify /etc/systemd/system/xray-restart.timer 2>&1 | sed 's/^/ /'
|
||||||
|
if journalctl -b --no-pager 2>/dev/null | grep -q "Unknown key name.*xray-restart"; then
|
||||||
|
bad "journal 里有 Unknown key name 告警,说明 timer 里仍有非法指令"
|
||||||
|
else
|
||||||
|
ok "journal 无 Unknown key name 告警"
|
||||||
|
fi
|
||||||
|
|
||||||
|
CAL=$(systemctl show xray-restart.timer -p TimersCalendar --value 2>/dev/null)
|
||||||
|
echo " TimersCalendar: $CAL"
|
||||||
|
case "$CAL" in
|
||||||
|
*Asia/Shanghai*) ok "解析后的日历规格里含时区" ;;
|
||||||
|
*) bad "解析后的日历规格里没有时区——时区没生效" "$CAL" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
grep -q '^TimeZone=' /etc/systemd/system/xray-restart.timer \
|
||||||
|
&& bad "timer 里仍有非法的 TimeZone= 指令" \
|
||||||
|
|| ok "timer 里无 TimeZone= 指令"
|
||||||
|
|
||||||
|
sec "4. 守卫脚本行为"
|
||||||
|
rm -f /var/lib/xray/last-restart
|
||||||
|
echo " -- 首次(无状态文件)应执行重启 --"
|
||||||
|
/usr/local/bin/xray-restart-guard 2>&1 | sed 's/^/ /'
|
||||||
|
[ -s /var/lib/xray/last-restart ] && ok "时间戳已写入" || bad "时间戳未写入"
|
||||||
|
|
||||||
|
echo " -- 第二次(刚重启过)应跳过 --"
|
||||||
|
G2=$(/usr/local/bin/xray-restart-guard 2>&1); echo "$G2" | sed 's/^/ /'
|
||||||
|
echo "$G2" | grep -q "跳过" && ok "正确跳过" || bad "未跳过" "$G2"
|
||||||
|
|
||||||
|
echo " -- Important-4:时间戳落在未来时应视为 0 并执行重启 --"
|
||||||
|
TS_BEFORE_FUTURE=$(systemctl show xray -p ActiveEnterTimestamp --value)
|
||||||
|
echo "$(( $(date +%s) + 86400*365 ))" > /var/lib/xray/last-restart
|
||||||
|
G3=$(/usr/local/bin/xray-restart-guard 2>&1); echo "$G3" | sed 's/^/ /'
|
||||||
|
if echo "$G3" | grep -q "跳过"; then
|
||||||
|
bad "未来时间戳导致永久跳过——保护未生效"
|
||||||
|
else
|
||||||
|
ok "未来时间戳被正确当成 0"
|
||||||
|
fi
|
||||||
|
echo " (这一步会真的重启一次 xray,是预期内的)"
|
||||||
|
sleep 2
|
||||||
|
systemctl is-active --quiet xray && ok "重启后 xray 仍然存活" || bad "xray 未能重新启动!"
|
||||||
|
|
||||||
|
sec "5. Important-6:下次实际重启时间是否被正确显示"
|
||||||
|
echo "$(( $(date +%s) - 3*86400 ))" > /var/lib/xray/last-restart
|
||||||
|
OUT=$(bash deploy.sh --restart 2>&1); echo "$OUT" | sed 's/^/ /'
|
||||||
|
echo " (N=7、距上次 3 天,所以真实的下次重启应在 4 天后,而非次日)"
|
||||||
|
|
||||||
|
sec "6. 关闭:--restart 0"
|
||||||
|
bash deploy.sh --restart 0 >/dev/null 2>&1
|
||||||
|
for f in /etc/systemd/system/xray-restart.timer /etc/systemd/system/xray-restart.service /usr/local/bin/xray-restart-guard; do
|
||||||
|
[ -f "$f" ] && bad "未删除: $f" || ok "已删除: $(basename "$f")"
|
||||||
|
done
|
||||||
|
[ -f /var/lib/xray/last-restart ] && ok "状态文件按设计保留" || bad "状态文件被误删(关闭≠卸载)"
|
||||||
|
|
||||||
|
sec "7. Critical-2:前导零天数"
|
||||||
|
bash deploy.sh --restart 09 >/dev/null 2>&1
|
||||||
|
D=$(grep '^XRAY_RESTART_EVERY_DAYS=' .env | cut -d= -f2)
|
||||||
|
[ "$D" = "9" ] && ok "09 被归一化成 9(未被当八进制)" || bad "09 未被正确归一化" "$D"
|
||||||
|
if [ -f /usr/local/bin/xray-restart-guard ]; then
|
||||||
|
grep -q '^DAYS=9$' /usr/local/bin/xray-restart-guard \
|
||||||
|
&& ok "guard 里 DAYS=9" || bad "guard 里天数不对" "$(grep '^DAYS=' /usr/local/bin/xray-restart-guard)"
|
||||||
|
OUT=$(/usr/local/bin/xray-restart-guard 2>&1)
|
||||||
|
echo "$OUT" | grep -qi "value too great\|error token" \
|
||||||
|
&& bad "guard 仍有八进制算术错误" "$OUT" \
|
||||||
|
|| ok "guard 无八进制算术错误"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sec "8. 参数校验与互斥"
|
||||||
|
bash deploy.sh --restart 7 25:00 >/dev/null 2>&1; [ $? -eq 1 ] && ok "非法时间退出码 1" || bad "非法时间退出码不是 1"
|
||||||
|
bash deploy.sh --badarg >/dev/null 2>&1; [ $? -eq 1 ] && ok "未知参数退出码 1" || bad "未知参数退出码不是 1"
|
||||||
|
bash deploy.sh --help >/dev/null 2>&1; [ $? -eq 0 ] && ok "--help 退出码 0" || bad "--help 退出码不是 0"
|
||||||
|
# 注意:不能写成 `bash deploy.sh ... | grep -q ...`。本脚本开了 pipefail,
|
||||||
|
# 而 deploy.sh 在这里本就该以退出码 1 结束,管道会因此被判为失败——
|
||||||
|
# 于是断言恒为「未生效」,与被测行为无关。必须先取输出再匹配。
|
||||||
|
MX_OUT=$(bash deploy.sh --restart 7 --redetect 2>&1 || true)
|
||||||
|
echo "$MX_OUT" | grep -q "不能与" && ok "互斥校验生效" || bad "互斥校验未生效" "$MX_OUT"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo " 通过 ${PASS},失败 ${FAIL}"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
echo "还需要你手动做一步(本脚本刻意不做,因为它会重启服务、断掉所有连接):"
|
||||||
|
echo ""
|
||||||
|
echo " # 旧字段迁移验证 —— 建议挑一台当前没在用的机器"
|
||||||
|
echo " cp .env /tmp/env.bak"
|
||||||
|
echo " sed -i '/^XRAY_RESTART_/d' .env"
|
||||||
|
echo " echo 'XRAY_DAILY_RESTART=true' >> .env"
|
||||||
|
echo " bash deploy.sh # 完整部署,会重启 xray"
|
||||||
|
echo " grep -E '^XRAY_RESTART_|^XRAY_DAILY' .env"
|
||||||
|
echo " # 预期:出现 XRAY_RESTART_EVERY_DAYS=1,且 XRAY_DAILY_RESTART 已消失"
|
||||||
|
echo " cp /tmp/env.bak .env && bash deploy.sh --restart"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
@@ -84,11 +84,14 @@ fi
|
|||||||
systemctl disable xray 2>/dev/null || true
|
systemctl disable xray 2>/dev/null || true
|
||||||
|
|
||||||
# 每日重启 timer(旧版部署可能残留)
|
# 每日重启 timer(旧版部署可能残留)
|
||||||
if [ -f /etc/systemd/system/xray-restart.timer ]; then
|
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
|
systemctl disable --now xray-restart.timer 2>/dev/null || true
|
||||||
rm -f /etc/systemd/system/xray-restart.timer \
|
rm -f /etc/systemd/system/xray-restart.timer \
|
||||||
/etc/systemd/system/xray-restart.service
|
/etc/systemd/system/xray-restart.service \
|
||||||
log "已移除每日重启 timer"
|
/usr/local/bin/xray-restart-guard \
|
||||||
|
/var/lib/xray/last-restart
|
||||||
|
log "已移除定时重启 timer 与状态文件"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ===== 2. 卸载 Xray =====
|
# ===== 2. 卸载 Xray =====
|
||||||
|
|||||||
Reference in New Issue
Block a user