Files
liuzhuohua b7ca9c89e8 feat(i18n/layout): 全站中英双语 + 四轮排版修复收官 + 修 boot_id 重启检测
v1.0.10 两件大事:

1. 界面中英双语(英文界面无中文残留)
   - 新增 web/i18n.js(约 4200 条词条),零依赖零构建
   - 后端下发中文数据走 bt4() 查表翻译;独立模块(upstream/netdetail/
     realtime/update)纳入翻译并注册切语言重绘钩子
   - 后端「状态/聚合」接口只回稳定枚举+原始数值,不下发中文文案
   - 顺带修一个真实 XSS:顶层 const 不挂 window,独立脚本 esc 静默退化成零转义

2. 修掉「文字竖着排/错位」(多分辨率反复暴露)
   - 单元格 min-content 只有 1 字宽 → word-break:keep-all + 表头 nowrap
   - 依赖卡片正文被标签/按钮挤成 27px → .dep-body{flex:1 1 200px} + flex-wrap
   - 裸 flex:1(下限为0)给正文 → 一律改 flex:1 1 <px>
   - .dep-item 横向容器直接塞块级子项 → 退回块级
   - 关键陷阱:auto-fill 布局屏越宽列数越多单卡越窄,窄屏反而测不出来,
     真机审计推进到「逐视图点按钮 + 1920/2560 宽视口 + 中英双语」
   - 守卫 t-layout.py 扩到 35 条,12 处反向验证全红

3. 修真缺陷:_link_since() 重启检测被误改成 if False:(注释还写着「已修」),
   uptime 归零后相减得负数 → 恢复比对 boot_id

附带:新增 25 个检查器(t-*.py)、排版审计工具链(shot-*/ocr-*/live-*)、
版本号兜底值同步到 1.0.10、.gitignore 收纳 i18n/排版排查的一次性脚本。
2026-10-10 04:50:32 +08:00

497 lines
23 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# Debian13 主路由系统 —— 部署脚本
# 将后端、前端、systemd 服务、sudoers 白名单安装到位。
# 不会启动任何网络服务,不会修改网络配置。
set -e
# 源码目录可用 DROUTER_SRC 覆盖(一键脚本 drouter-ctl.sh 会传入实际路径)
SRC="${DROUTER_SRC:-/tmp/drouter-src}"
OPT=/opt/drouter
UNIT_WEB=drouter-web
echo "=== 1. 创建目录结构 ==="
mkdir -p $OPT/backend $OPT/web $OPT/data $OPT/snapshots $OPT/certs $OPT/bin $OPT/docs
mkdir -p /etc/drouter/generated
mkdir -p /etc/nftables.d
mkdir -p /var/log/drouter
mkdir -p /etc/dnsmasq.d
mkdir -p /var/lib/drouter/trash # 文件管理器"回收站"
mkdir -p $OPT/dpi # DPI 识别规则库与源码(#25)
mkdir -p /etc/ppp/ip-up.d /etc/ppp/ip-down.d # PPPoE 多拨策略路由钩子(#23)
# helper 私有临时目录:/run 为 tmpfs(重启自清),且不归低权用户所有,
# 避开 /tmp、/var/tmp 的 sticky 位与 fs.protected_regular=2 写保护。
mkdir -p /run/drouter-helper
chmod 700 /run/drouter-helper
sha_of() { [ -f "$1" ] && sha1sum < "$1" | cut -d' ' -f1 || echo "-"; }
# shelld 是常驻守护,代码在内存里,不重启就是旧行为。
# 判断依据必须是「本次源码指纹」vs「上次真正重启过的指纹」(记在 mark 文件里),
# 不能拿 $SRC 和 $OPT 比 —— install 之后两边同源,cmp 恒等,永远不重启。
# 也不能拿 install 前的 $OPT 比 —— 若上一轮已经把新文件拷进去了但没重启成功,
# 这一轮两边又相等,漏网。
SHELLD_NEW=$(sha_of "$SRC/backend/drouter-shelld.py")
SHELLD_MARK=/var/lib/drouter/shelld.deployed.sha
echo "=== 2. 安装后端 ==="
install -m 0644 $SRC/backend/render.py $OPT/backend/render.py
install -m 0644 $SRC/backend/drouter-helper.py $OPT/backend/drouter-helper.py
install -m 0644 $SRC/backend/drouter-web.py $OPT/backend/drouter-web.py
install -m 0644 $SRC/backend/theme.py $OPT/backend/theme.py
# drouter-logd:统一日志采集守护。helper 写出的 drouter-logd.service 直接
# ExecStart 这个文件,漏装会导致日志定时器起不来(此前确实漏了,务必保留)。
install -m 0644 $SRC/backend/drouter-logd.py $OPT/backend/drouter-logd.py
install -m 0644 $SRC/backend/drouter-snapshotd.py $OPT/backend/drouter-snapshotd.py
install -m 0644 $SRC/backend/drouter-rescue.py $OPT/backend/drouter-rescue.py
# drouter-shelld:Web 真·PTY 终端的会话守护。它必须由常驻进程持有 PTY
# (helper 每次都是跑完就退出的新进程,PTY fd 跨不了请求),漏装则终端连不上。
install -m 0644 $SRC/backend/drouter-shelld.py $OPT/backend/drouter-shelld.py
install -m 0644 $SRC/backend/drouter-helpd.py $OPT/backend/drouter-helpd.py
# 三个定时任务守护(1.0.7)。它们的 systemd 单元由 helper 在用户保存设置时
# 现写,ExecStart 指向的就是下面这三个文件 —— 漏装 = 定时器起来就报 203/EXEC。
install -m 0644 $SRC/backend/drouter-backupd.py $OPT/backend/drouter-backupd.py
install -m 0644 $SRC/backend/drouter-alertd.py $OPT/backend/drouter-alertd.py
install -m 0644 $SRC/backend/drouter-quotad.py $OPT/backend/drouter-quotad.py
# DDNS 定时更新守护(1.0.8)。drouter-ddns.service/.timer 不在这里预置:
# 周期取自用户填的「检测间隔」,由 helper 的 _write_ddns_timer() 运行时生成。
install -m 0644 $SRC/backend/drouter-ddnsd.py $OPT/backend/drouter-ddnsd.py
# 版本检测/自更新(1.0.10)。它不是守护进程,是被 drouter-web.py 按路径
# importlib 加载的模块 —— 文件名带连字符,不是合法 Python 标识符,
# 所以 web.py 里用 spec_from_file_location 加载,**不要**改成普通 import。
# 这里必须装:漏了的话 web.py 每次调 /api/update/* 都会抛异常。
install -m 0644 $SRC/backend/drouter-update.py $OPT/backend/drouter-update.py
# 主题之家(#12):自定义主题目录 + 生效主题标记
mkdir -p /etc/drouter/themes
# 只在「还没有生效主题」时播种默认值。
# 这里原来是无条件 `echo default > active-theme` —— 每次部署都把用户自己
# 挑的主题打回默认,用户只会看到「部署完样式莫名其妙变回去了」。
# 容器版 docker-init.sh 一直是「首次为空时才写」,两处行为保持一致。
if [ ! -f /etc/drouter/active-theme ]; then
echo "default" > /etc/drouter/active-theme
fi
chmod 644 /etc/drouter/active-theme
# 记录的生效主题如果指向一个已不存在的主题(自定义主题目录被删/id 写错),
# 界面会一直挂着一个「不存在」的生效主题,且只能手改文件才出得来。这里兜底回退。
python3 - <<'PY' || true
import importlib.util
spec = importlib.util.spec_from_file_location(
'drouter_helper', '/opt/drouter/backend/drouter-helper.py')
h = importlib.util.module_from_spec(spec)
spec.loader.exec_module(h)
tid = h._theme_read_active()
if h._theme_find(tid) is None:
h._theme_write_active('default')
print('生效主题 %r 已不存在,已回退 default' % tid)
PY
echo "=== 3. 安装前端 ==="
install -m 0644 $SRC/web/index.html $OPT/web/index.html
install -m 0644 $SRC/web/app.css $OPT/web/app.css
install -m 0644 $SRC/web/app.js $OPT/web/app.js
# ⚠️ 新增的前端模块(1.0.10)必须在这里逐个登记 —— 这一段是**逐个 install**,
# 不是通配符,漏登记 = 真机上文件不存在 = 页面 JS 404。
# 2026-10-04 首次部署就踩了:drouter-update.py 装上了,
# 但 update.js / update.css / upstream.js 三个都没装,概览页直接坏掉。
# 加新的 web/*.js|css 时**必须同步这里**,t-audit 的部署判据会检查。
for wf in update.js update.css upstream.js netdetail.js realtime.js i18n.js; do
[ -f "$SRC/web/$wf" ] || { echo "✘ 源码缺少 web/$wf"; exit 1; }
install -m 0644 "$SRC/web/$wf" "$OPT/web/$wf"
done
if [ -f "$SRC/web/logo.svg" ]; then
install -m 0644 $SRC/web/logo.svg $OPT/web/logo.svg
fi
# 版本号(1.0.9 新增)。页面底部的版本号与 /api/openapi 的 version 字段
# 都从 /opt/drouter/VERSION 读。缺了不报错,只是永远显示后端的兜底值 ——
# 部署了新版却显示旧版本号,比什么都不做更让人困惑。
# devtools/sync.sh 负责把 packaging/VERSION 打进源码包;源码包里没有就跳过。
if [ -f "$SRC/packaging/VERSION" ]; then
install -m 0644 $SRC/packaging/VERSION $OPT/VERSION
echo " 版本号 $(cat $OPT/VERSION)"
fi
echo "=== 3.5 安装运维脚本(自检/回归用)==="
if [ -d "$SRC/scripts" ]; then
install -d -m 0755 $OPT/scripts
for s in "$SRC"/scripts/*.sh; do
[ -f "$s" ] || continue
install -m 0755 "$s" "$OPT/scripts/$(basename "$s")"
done
echo "已安装脚本:$(ls $OPT/scripts | tr '\n' ' ')"
fi
echo "=== 3.6 安装文档(systemd unit 的 Documentation= 指向这里)==="
if [ -d "$SRC/docs" ]; then
for d in "$SRC"/docs/*; do
[ -f "$d" ] || continue
install -m 0644 "$d" "$OPT/docs/$(basename "$d")"
done
echo "已安装文档:$(ls $OPT/docs | tr '\n' ' ')"
fi
echo "=== 4. 创建专用低权用户 drouter ==="
if ! id drouter >/dev/null 2>&1; then
useradd -r -s /usr/sbin/nologin -d $OPT -M drouter
echo "已创建系统用户 drouter"
else
echo "用户 drouter 已存在"
fi
echo "=== 5. 权限设置 ==="
chown -R drouter:drouter $OPT/data $OPT/snapshots $OPT/certs
chmod 750 $OPT/data $OPT/snapshots $OPT/certs
chmod +x $OPT/backend/drouter-helper.py
chmod +x $OPT/backend/drouter-snapshotd.py
chmod +x $OPT/backend/drouter-rescue.py
chown root:root $OPT/backend/drouter-helper.py
chown -R root:root $OPT/backend
chown -R root:root $OPT/web
chmod 755 /var/log/drouter
chmod 755 /etc/drouter/generated
chown -R drouter:drouter /var/lib/drouter 2>/dev/null || true
chmod -R 750 /var/lib/drouter 2>/dev/null || true
echo "=== 6. sudoers 白名单(仅放行 helper,且限定参数形态)==="
cat > /etc/sudoers.d/drouter <<'SUDO'
# drouter:后端仅可通过该脚本以 root 执行白名单操作
# 参数被严格限制为 action + JSON 字符串
drouter ALL=(root) NOPASSWD: /usr/bin/python3 /opt/drouter/backend/drouter-helper.py *
SUDO
chmod 0440 /etc/sudoers.d/drouter
visudo -c -f /etc/sudoers.d/drouter >/dev/null && echo "sudoers 语法检查通过"
echo "=== 7. 日志轮转配置 ==="
cat > /etc/logrotate.d/drouter <<'LR'
/var/log/drouter/*.log /var/log/drouter/*.jsonl {
daily
rotate 7
missingok
notifempty
compress
delaycompress
copytruncate
create 0644 root root
}
LR
echo "=== 8. systemd 服务单元 ==="
cat > /etc/systemd/system/drouter-web.service <<'UNIT'
[Unit]
Description=drouter 主路由管理后台
Documentation=file:/opt/drouter/docs/
After=network-online.target
Wants=network-online.target
Before=shutdown.target
[Service]
Type=simple
User=drouter
Group=drouter
WorkingDirectory=/opt/drouter
# systemd 默认 PATH 不含 /usr/sbin,而 nft/dnsmasq/radvd 都在那儿。
# 后端虽已自行归一化 PATH,这里再显式声明一次,避免任何遗漏路径静默失败。
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/bin/python3 /opt/drouter/backend/drouter-web.py
Restart=on-failure
RestartSec=5
# 资源限制:适配 2 核 / 3.9G 小内存环境
MemoryMax=160M
MemoryHigh=120M
CPUQuota=40%
Nice=5
IOSchedulingClass=best-effort
IOSchedulingPriority=5
# 安全加固
# 说明:后端以低权用户 drouter 运行,写 /etc 由 root helper 完成(白名单 action)。
# 注意:这里必须 ProtectSystem=false —— 「依赖自检 / 一键安装」需要通过 apt/dpkg
# 往 /usr/bin、/var/cache/apt、/var/lib/dpkg 写入;若把 /usr 挂为只读,
# apt 会报 "只读文件系统" 而安装失败。安全边界由 helper 的 action 白名单保证:
# 非白名单动作一律拒绝,且所有参数逐个强校验。
NoNewPrivileges=false
PrivateTmp=false
ProtectSystem=false
ProtectHome=read-only
ReadWritePaths=/opt/drouter/data /opt/drouter/snapshots /opt/drouter/certs /var/log/drouter
# 额外保护:这些路径不给写(即使 helper 被滥用也改不到)
InaccessiblePaths=/boot
StandardOutput=journal
StandardError=journal
SyslogIdentifier=drouter-web
[Install]
WantedBy=multi-user.target
UNIT
echo "=== 8.5 自动快照定时器 + 紧急救援通道服务单元 ==="
# 自动快照:默认「开启」,每 6 小时一次。真实参数由界面写入
# /etc/drouter/snapshot.conf,helper 保存策略时会重建本单元。
cat > /etc/systemd/system/drouter-snapshot.service <<'SVC'
[Unit]
Description=drouter 自动配置快照
After=network.target
[Service]
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Type=oneshot
User=root
ExecStart=/usr/bin/python3 /opt/drouter/backend/drouter-snapshotd.py
# oneshot 打完包就退出,但 tar 大目录时也会有内存尖峰,给个上限兜底
MemoryMax=300M
[Install]
WantedBy=multi-user.target
SVC
cat > /etc/systemd/system/drouter-snapshot.timer <<'TMR'
[Unit]
Description=drouter 自动快照定时器
[Timer]
OnBootSec=10min
OnUnitActiveSec=6h
Persistent=true
Unit=drouter-snapshot.service
[Install]
WantedBy=timers.target
TMR
# 紧急救援通道:默认「关闭」。只有用户在界面上勾选开启后,
# helper 才会创建虚拟网卡别名并 enable/start 本服务。
cat > /etc/systemd/system/drouter-rescue.service <<'RSC'
[Unit]
Description=drouter 紧急救援通道(独立于 WAN/LAN 的应急恢复入口)
After=network.target
Wants=network.target
[Service]
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Type=simple
User=root
ExecStart=/usr/bin/python3 /opt/drouter/backend/drouter-rescue.py
Restart=always
RestartSec=5
# 救援通道是「最后的入口」:本身只是个小 HTTP 服务,几十 MB 足够。
# 给它也装上内存闸 —— 真到内存泄漏那天,不能让它把本就紧张的机器拖死
MemoryMax=150M
StandardOutput=journal
StandardError=journal
NoNewPrivileges=false
ProtectSystem=false
[Install]
WantedBy=multi-user.target
RSC
# 救援通道默认关闭:不 enable、不 start(避免影响现有局域网)
echo "救援通道单元已就位(默认关闭,需在界面显式开启)"
# 常驻执行守护(#7 性能优化):把「每个请求 spawn 一次 python3+sudo」的
# 236ms 固定开销去掉。以 root 运行,但只监听 Unix socket(0660 root:drouter),
# 不占任何 TCP 端口;drouter-web 连不上它时会自动回退到 subprocess,所以它
# 挂掉只会变慢、不会让面板不可用。
cat > /etc/systemd/system/drouter-helpd.service <<'HPD'
[Unit]
Description=drouter 特权动作常驻执行守护
Documentation=file:/opt/drouter/docs/
After=network.target
[Service]
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Type=simple
User=root
Group=root
# 刻意不用 RuntimeDirectory:见 drouter-shelld.service 里的说明。
# /run/drouter 由守护自己 mkdir 并 chmod 0755(drouter-web 要能进目录连
# helper.sock,而 socket 自身是 0660 root:drouter,权限并没有放宽)。
ExecStart=/usr/bin/python3 /opt/drouter/backend/drouter-helpd.py
Restart=on-failure
RestartSec=2
# 常驻 import 了整套 helper,比其它守护占得多;上限防内存泄漏拖垮路由器
MemoryMax=300M
# 故意不设 CPUQuota:它要跑格式化大硬盘这类子进程,限 CPU 会把这些操作拖慢
StandardOutput=journal
StandardError=journal
SyslogIdentifier=drouter-helpd
ProtectHome=read-only
PrivateTmp=false
NoNewPrivileges=false
[Install]
WantedBy=multi-user.target
HPD
echo "常驻执行守护单元已就位"
# Web 终端守护(#4 重做):真 PTY 会话必须由常驻进程持有。
# 说明为什么不用 socket 激活:helper 在连不上时会自动 systemctl start 拉起,
# 但那要等几秒;预启动能让用户点「连接」时立刻进入 shell。
# 它以 root 运行(要开 root shell),但只监听 0600 的 Unix socket,不占任何端口。
cat > /etc/systemd/system/drouter-shelld.service <<'SHD'
[Unit]
Description=drouter Web 终端 PTY 会话守护
Documentation=file:/opt/drouter/docs/
After=network.target
[Service]
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Type=simple
User=root
Group=root
# 刻意不用 RuntimeDirectory=drouter:systemd 会在「最后一个声明它的单元停止」
# 时把整个目录删掉重建。两个守护共用 /run/drouter,于是 restart helpd 的瞬间
# systemd 重建目录,把 shelld 的 shell.sock 一起删了 —— shelld 依旧 active,
# socket 却没了,用户点终端只看到「守护未运行」。改成两个守护各自 mkdir。
ExecStart=/usr/bin/python3 /opt/drouter/backend/drouter-shelld.py
Restart=on-failure
RestartSec=3
# 只是转字节流,用不了多少内存;上限是防异常输出把内存吃光
MemoryMax=80M
CPUQuota=15%
StandardOutput=journal
StandardError=journal
SyslogIdentifier=drouter-shelld
# 安全加固:不碰系统目录、不给新特权(它本身就以 root 跑,靠 socket 0600 收口)
ProtectHome=read-only
PrivateTmp=false
NoNewPrivileges=false
[Install]
WantedBy=multi-user.target
SHD
echo "Web 终端守护单元已就位"
echo "=== 9. 生成 isp-dns 占位文件(dnsmasq resolv-file 引用)==="
# 只在缺失时补占位。这个文件是 pppd 拨号成功后经
# /opt/drouter/scripts/sync-isp-dns.sh 写入的真实运营商 DNS;无条件覆盖会把
# 已经拿到的 DNS 擦掉,在那之后 dnsmasq 的上游解析一直空着,直到下一次拨号
# 才会恢复(表现为「重新部署后网页解析变慢」)。
# 容器版 docker-init.sh 同样是「缺失才写」。
# 脚本本体不再用 heredoc 写进 $OPT/bin/:第 84 行的 install 已经把
# scripts/*.sh 装到 $OPT/scripts/,而 deb / 镜像走的也是同一条规则 ——
# 早先只有 deploy.sh 部署的机器才有这个文件。
if [ ! -f /etc/drouter/generated/isp-dns.conf ]; then
cat > /etc/drouter/generated/isp-dns.conf <<'ISP'
# 由 drouter 自动维护:运营商 PPPoE 下发的 DNS
# 拨号成功后由 pppd 的 usepeerdns 写入 /etc/ppp/resolv.conf,
# 可执行 /opt/drouter/scripts/sync-isp-dns.sh 将其同步到此处。
ISP
fi
# 存量占位文件里可能还留着旧路径(bin/),只换注释行、不碰 nameserver:
# 用户已经拿到的运营商 DNS 不能被部署动作擦掉。
if [ -f /etc/drouter/generated/isp-dns.conf ] \
&& grep -qs 'bin/sync-isp-dns.sh' /etc/drouter/generated/isp-dns.conf; then
sed -i 's#bin/sync-isp-dns\.sh#scripts/sync-isp-dns.sh#' \
/etc/drouter/generated/isp-dns.conf
fi
# 存量机器迁移:/etc/drouter/rescue.conf 旧版本写成了 0600 属主 root,
# 以 drouter 身份运行的快照读不到它,一个文件就让整个 /etc/drouter 目录
# 进不了快照(界面却显示「已创建快照」)。这里把它放宽成 0640 属组 drouter。
if [ -f /etc/drouter/rescue.conf ]; then
chmod 0640 /etc/drouter/rescue.conf 2>/dev/null || true
chgrp drouter /etc/drouter/rescue.conf 2>/dev/null || true
fi
echo "=== 10. 校验 ==="
# 逐个编译校验:缺文件直接中止部署(避免上线后才发现少装了一个守护进程)
for f in render.py drouter-helper.py drouter-web.py drouter-logd.py \
drouter-snapshotd.py drouter-rescue.py drouter-shelld.py \
drouter-helpd.py drouter-backupd.py drouter-alertd.py \
drouter-quotad.py drouter-ddnsd.py drouter-update.py \
theme.py; do
p="$OPT/backend/$f"
if [ ! -f "$p" ]; then echo "✘ 缺少文件: $p"; exit 1; fi
# ⚠️ 用 compile() 不是 ast.parse():后者只做语法分析,抓不到
# 「重复关键字参数」这类语义错误(1.0.10 实测:静态全绿、import 才炸)
python3 -c 'import sys; compile(open(sys.argv[1],encoding="utf-8").read(),sys.argv[1],"exec")' "$p" \
&& echo "语法OK: $p" || { echo "✘ 语法错误: $p"; exit 1; }
done
echo "--- 目录 ---"
ls -la $OPT/
echo "--- 服务文件 ---"
systemctl daemon-reload
systemctl is-enabled drouter-web 2>&1 || true
echo "--- 自动快照定时器(默认开启)---"
systemctl enable --now drouter-snapshot.timer 2>&1 | tail -2 || true
systemctl is-active drouter-snapshot.timer 2>&1 || true
echo "--- 紧急救援通道(默认关闭)---"
systemctl is-active drouter-rescue 2>&1 || true
echo "--- 磁盘与日志清理定时器(默认开启)---"
# 单元与入口脚本由 helper 的 cleanup save 动作生成(默认参数即推荐值)。
# 关键点:**只在新机器上才传 enabled=true**。
# _clean_save_op 是合并式保存,无条件传 enabled=true 会把用户在界面上明确
# 关掉的自动清理重新打开 —— 而清理是本项目唯一会真删文件的模块,
# 静默恢复一个「自动删文件」的策略是所有副作用里最不该发生的。
if [ ! -f /etc/drouter/cleanup.conf ]; then
/usr/bin/python3 $OPT/backend/drouter-helper.py cleanup '{"op":"save","enabled":true}' \
>/dev/null 2>&1 && echo "清理策略已按推荐值初始化" || echo "⚠ 清理策略初始化失败(界面保存一次即可恢复)"
else
# 已有策略:只重建单元文件,enabled / trigger / items 全部沿用用户设置
/usr/bin/python3 $OPT/backend/drouter-helper.py cleanup '{"op":"save"}' \
>/dev/null 2>&1 && echo "清理策略沿用已有设置(未改动用户开关)" || echo "⚠ 清理单元重建失败(界面保存一次即可恢复)"
fi
systemctl is-active drouter-cleanup.timer 2>&1 || true
echo "--- 常驻执行守护 ---"
# 必须 restart 而不是 enable --now:守护把 helper 代码常驻在内存里,
# 只 enable --now 的话它还在跑上一版代码,改的东西根本不生效。
systemctl enable drouter-helpd 2>&1 | tail -1 || true
systemctl restart drouter-helpd 2>&1 | tail -2 || true
sleep 1
systemctl is-active drouter-helpd 2>&1 || echo "⚠ 常驻执行守护未运行(面板会自动回退到慢路径)"
echo "--- Web 终端守护 ---"
# 重启 shelld 会断掉正在用的终端会话,所以默认只 ensure 起来。
# 但两种情况必须重启:① socket 文件没了(多半是被 helpd 重启连带删掉的);
# ② 代码确实变了 —— 不重启的话跑的还是旧代码。
mkdir -p /var/lib/drouter
SHELLD_SEEN=$(cat "$SHELLD_MARK" 2>/dev/null || echo "-")
if [ ! -S /run/drouter/shell.sock ] || [ "$SHELLD_NEW" != "$SHELLD_SEEN" ]; then
echo "终端守护代码有更新($SHELLD_SEEN → $SHELLD_NEW),重启以生效"
systemctl restart drouter-shelld 2>&1 | tail -2 || true
# 只有真的起来了才更新 mark,否则下一轮还会重试重启
if systemctl is-active --quiet drouter-shelld; then
echo "$SHELLD_NEW" > "$SHELLD_MARK"
else
echo "⚠ 终端守护未能启动,mark 不更新(下一轮部署会再试)"
fi
else
systemctl enable --now drouter-shelld 2>&1 | tail -1 || true
echo "$SHELLD_NEW" > "$SHELLD_MARK"
fi
systemctl is-active drouter-shelld 2>&1 || true
# socket 必须真的存在:systemd 说 active 不代表 socket 已经在盘上
# (restart 是异步的,守护起来后还要自己 mkdir + bind,得给它几秒)。
for _i in 1 2 3 4 5 6 7 8 9 10; do
[ -S /run/drouter/shell.sock ] && break
sleep 1
done
[ -S /run/drouter/shell.sock ] && echo "终端 socket 就位" \
|| echo "⚠ 终端 socket 缺失,Web 终端将不可用"
echo "--- 网络与 VNC 复核 ---"
ip -4 -o addr show ens18 2>/dev/null || echo "(ens18 不存在,跳过)"
ss -lnt | grep 5900 || echo "5900 异常"
# 重启管理后台,让新代码真正生效。
# 必须放在语法校验之后:一旦新代码有语法错误,宁可继续跑旧进程也不能把自己关在门外。
echo "--- 重启管理后台 ---"
systemctl restart $UNIT_WEB 2>&1 | tail -3 || echo "(重启失败,请手动检查)"
sleep 2
systemctl is-active $UNIT_WEB || echo "⚠ drouter-web 未在运行"
# 统一日志采集定时器:默认配置是 enabled=true,但 service/timer 单元只有
# 在界面里点过一次「保存日志设置」才会被 helper 写出来。全新部署后防火墙日志
# 页面会一直是空的,表现为「日志功能没做」。这里补一次同步,让默认开关真正生效。
echo "--- 同步日志采集定时器 ---"
python3 - <<'PY' || echo "(日志定时器同步失败,可在界面「日志设置」里保存一次重建)"
# 文件名带连字符,不能用普通 import,走 importlib 按路径加载
import importlib.util
spec = importlib.util.spec_from_file_location(
'drouter_helper', '/opt/drouter/backend/drouter-helper.py')
h = importlib.util.module_from_spec(spec)
spec.loader.exec_module(h)
r = h.act_logd_sync({'interval_sec': 60})
print(r.get('msg_cn') or r)
PY
systemctl is-active drouter-logd.timer 2>&1 || true
echo "DEPLOY_DONE"