1184 lines
42 KiB
Bash
1184 lines
42 KiB
Bash
#!/usr/bin/env bash
|
||
# ==============================================================================
|
||
# deploy.sh — chongming 部署 / 回滚脚本
|
||
# 目标平台:AI Box(Sophon,ARM64 Linux + systemd)
|
||
#
|
||
# 在设备上执行,非 root 会自动 sudo 提权:
|
||
# bash deploy.sh # 全新安装 / 覆盖升级(默认保留站点数据与配置)
|
||
# bash deploy.sh --dry-run # 预演,只打印不落盘
|
||
# bash deploy.sh --no-start # 只安装不启动
|
||
# bash deploy.sh --force-config --force-data # 连站点配置与运行时数据一起还原成打包版
|
||
# bash deploy.sh --rollback # 还原最近一次备份
|
||
# bash deploy.sh --list-backups # 列出所有备份
|
||
# bash deploy.sh --help # 完整参数说明
|
||
#
|
||
# 目录约定(默认值,均可用参数覆盖):
|
||
# 打包产物 <脚本目录>/chongming 安装目录 /data/chongming
|
||
# 服务单元 /etc/systemd/system/chongming.service
|
||
# 运行用户 linaro:linaro 环境变量 /etc/chongming.env
|
||
# 备份目录 /data/chongming-deploy-backups(保留最近 3 份)
|
||
#
|
||
# 退出码:0 成功 / 1 通用错误 / 2 前置检查失败 / 3 健康检查失败
|
||
# ==============================================================================
|
||
|
||
# --- CRLF 自愈 ---------------------------------------------------------------
|
||
# 脚本在 Windows 上编辑/传输后可能带上 CR,届时 bash 会报 $'\r': command not found。
|
||
# 做法:用 tr 把自身复制一份去掉所有 CR,与原文比较,有差异才替换自身并重新执行。
|
||
# 全程只用 tr/cmp 这类按字节处理的工具,不受平台文本模式换行转换影响;无 CR 时零副作用。
|
||
# 用 `bash deploy.sh` 启动时这段也能生效(shebang 行带 CR 时无法由内核直接执行)。
|
||
if [ -z "${_CM_DEPLOY_REEXEC:-}" ]; then
|
||
_CM_SELF="$(cd "$(dirname "$0")" 2>/dev/null && pwd)/$(basename "$0")"
|
||
_CM_TMP="${_CM_SELF}.lf.$$"
|
||
if [ -f "$_CM_SELF" ] && tr -d '\r' < "$_CM_SELF" > "$_CM_TMP" 2>/dev/null && [ -s "$_CM_TMP" ]; then
|
||
if ! cmp -s "$_CM_TMP" "$_CM_SELF"; then
|
||
_CM_MODE="$(stat -c '%a' "$_CM_SELF" 2>/dev/null || true)"
|
||
if mv -f "$_CM_TMP" "$_CM_SELF" 2>/dev/null; then
|
||
if [ -n "$_CM_MODE" ]; then chmod "$_CM_MODE" "$_CM_SELF" 2>/dev/null || true; fi
|
||
printf '[deploy] 检测到 CRLF 行尾,已转换为 LF,重新执行 ...\n' >&2
|
||
_CM_DEPLOY_REEXEC=1 exec bash "$_CM_SELF" "$@"
|
||
fi
|
||
rm -f "$_CM_TMP" 2>/dev/null || true
|
||
printf '[deploy] 脚本含 CRLF 行尾且无法就地修复(目录只读?),请手动转换:\n' >&2
|
||
printf ' tr -d "\\r" < %s > /tmp/deploy.sh && bash /tmp/deploy.sh\n' "$0" >&2
|
||
exit 1
|
||
fi
|
||
fi
|
||
rm -f "$_CM_TMP" 2>/dev/null || true
|
||
fi
|
||
|
||
set -Eeuo pipefail
|
||
|
||
# ==============================================================================
|
||
# 默认值
|
||
# ==============================================================================
|
||
SELF_PATH="$(cd "$(dirname "$0")" 2>/dev/null && pwd)/$(basename "$0")"
|
||
SCRIPT_DIR="$(dirname "$SELF_PATH")"
|
||
|
||
INSTALL_DIR="${INSTALL_DIR:-/data/chongming}"
|
||
RUN_USER="${RUN_USER:-linaro}"
|
||
RUN_GROUP="${RUN_GROUP:-linaro}"
|
||
SERVICE_NAME="${SERVICE_NAME:-chongming}"
|
||
ENV_FILE="${ENV_FILE:-/etc/chongming.env}"
|
||
BACKUP_ROOT="${BACKUP_ROOT:-/data/chongming-deploy-backups}"
|
||
KEEP_BACKUPS="${KEEP_BACKUPS:-3}"
|
||
# HEALTH_PORTS="${HEALTH_PORTS:-8000,1883,7447}"
|
||
HEALTH_PORTS="${HEALTH_PORTS:-8000,1883}"
|
||
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-60}"
|
||
EXTRA_GROUPS="${EXTRA_GROUPS:-input}"
|
||
|
||
FFMPEG_BIN_DIR="/opt/sophon/sophon-ffmpeg-latest/bin"
|
||
# 单元安装目录:可用环境变量覆盖(非常规布局或测试用)
|
||
UNIT_DIR="${UNIT_DIR:-/etc/systemd/system}"
|
||
UNIT_DST="$UNIT_DIR/$SERVICE_NAME.service"
|
||
|
||
# 打包产物中必须存在的文件(缺任何一个都无法启动)
|
||
REQUIRED_FILES=(
|
||
"chongming"
|
||
"config.toml"
|
||
"static.svfs"
|
||
"config/zenoh_config.json5"
|
||
"resources/app.mbank"
|
||
"resources/plugins.mbank"
|
||
)
|
||
# 整体覆盖的目录/文件(应用资产,不承载站点状态;其余状态文件走 install_files 里的
|
||
# 设备优先策略:config/ config.toml data/ cache_data/ 已存在则不覆盖,logs/ 永不拷贝)
|
||
OVERWRITE_DIRS=("resources")
|
||
OVERWRITE_FILES=("static.svfs")
|
||
|
||
# 运行期全局
|
||
HEALTH_PORTS_ARR=()
|
||
PREV_SERVICE_STATE=""
|
||
|
||
# ==============================================================================
|
||
# 参数
|
||
# ==============================================================================
|
||
MODE="deploy" # deploy | rollback | list-backups
|
||
DRY_RUN=0
|
||
FORCE=0
|
||
FORCE_CONFIG=0
|
||
FORCE_DATA=0
|
||
DO_START=1
|
||
BUNDLE_ARG=""
|
||
UNIT_ARG=""
|
||
BUNDLE_DIR=""
|
||
UNIT_SRC=""
|
||
UNIT_FROM_EMBEDDED=0
|
||
|
||
usage() {
|
||
cat <<EOF
|
||
chongming 部署脚本 — 在 AI Box 上安装 / 升级 / 回滚 chongming 服务
|
||
|
||
用法:
|
||
bash deploy.sh [选项]
|
||
|
||
模式(默认 deploy):
|
||
--rollback 还原最近一次备份(停服 → 解包还原 → 重启 → 健康检查)
|
||
--list-backups 列出 $BACKUP_ROOT 下的备份
|
||
|
||
可选参数:
|
||
-d, --install-dir DIR 安装目录,默认 $INSTALL_DIR
|
||
-u, --user USER 运行用户,默认 $RUN_USER(不存在则创建为系统用户)
|
||
-g, --group GROUP 运行用户组,默认 $RUN_GROUP
|
||
-b, --bundle DIR 打包产物目录,默认自动探测 <脚本目录>/chongming
|
||
-s, --service-file FILE systemd 单元文件,默认自动探测脚本目录下的 chongming.service
|
||
--env-file FILE 可选环境变量文件,默认 $ENV_FILE
|
||
--backup-root DIR 备份根目录,默认 $BACKUP_ROOT
|
||
--keep-backups N 备份保留份数,默认 $KEEP_BACKUPS(更旧的自动删除;0 表示不清理)
|
||
--health-ports LIST 健康检查端口(逗号分隔),默认 $HEALTH_PORTS
|
||
--health-timeout N 健康检查超时秒数,默认 $HEALTH_TIMEOUT(0 表示跳过健康检查)
|
||
--extra-groups LIST 运行用户追加的附加组(逗号分隔),默认 $EXTRA_GROUPS
|
||
例:input(GPIO IN2 / /dev/input/event1 需要)
|
||
--force-config 用打包版覆盖 config/ 与 config.toml(默认保留设备上的站点配置)
|
||
--force-data 用打包版覆盖 data/ 与 cache_data/(默认保留设备上的运行时数据)
|
||
--force 跳过非致命前置检查(如二进制架构不匹配)
|
||
--no-start 只安装不启动,跳过启动与健康检查
|
||
--dry-run 预演:所有写操作只打印不执行
|
||
-h, --help 显示本帮助
|
||
|
||
默认行为(重要):
|
||
升级时 config/、config.toml、data/、cache_data/ 采用「设备优先」策略——设备上已有的文件
|
||
一律不覆盖,打包版里新增的文件照常补充进来;logs/ 永不从打包版拷贝,只创建目录。
|
||
|
||
退出码:
|
||
0 成功 1 通用错误 2 前置检查失败 3 健康检查失败
|
||
|
||
常用命令:
|
||
systemctl status $SERVICE_NAME # 服务状态
|
||
journalctl -u $SERVICE_NAME -f # 实时日志
|
||
tail -f $INSTALL_DIR/logs/app-*.log # 应用自身日志
|
||
|
||
示例:
|
||
bash deploy.sh # 常规升级
|
||
bash deploy.sh --dry-run # 先预演一遍
|
||
bash deploy.sh -d /opt/chongming -u root # 换目录、换运行用户
|
||
bash deploy.sh --rollback # 出问题回滚
|
||
EOF
|
||
}
|
||
|
||
parse_args() {
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
-d|--install-dir) INSTALL_DIR="${2:?$1 需要参数}"; shift 2 ;;
|
||
-d=*|--install-dir=*) INSTALL_DIR="${1#*=}"; shift ;;
|
||
-u|--user) RUN_USER="${2:?$1 需要参数}"; shift 2 ;;
|
||
-u=*|--user=*) RUN_USER="${1#*=}"; shift ;;
|
||
-g|--group) RUN_GROUP="${2:?$1 需要参数}"; shift 2 ;;
|
||
-g=*|--group=*) RUN_GROUP="${1#*=}"; shift ;;
|
||
-b|--bundle) BUNDLE_ARG="${2:?$1 需要参数}"; shift 2 ;;
|
||
-b=*|--bundle=*) BUNDLE_ARG="${1#*=}"; shift ;;
|
||
-s|--service-file) UNIT_ARG="${2:?$1 需要参数}"; shift 2 ;;
|
||
-s=*|--service-file=*) UNIT_ARG="${1#*=}"; shift ;;
|
||
--env-file) ENV_FILE="${2:?$1 需要参数}"; shift 2 ;;
|
||
--env-file=*) ENV_FILE="${1#*=}"; shift ;;
|
||
--backup-root) BACKUP_ROOT="${2:?$1 需要参数}"; shift 2 ;;
|
||
--backup-root=*) BACKUP_ROOT="${1#*=}"; shift ;;
|
||
--keep-backups) KEEP_BACKUPS="${2:?$1 需要参数}"; shift 2 ;;
|
||
--keep-backups=*) KEEP_BACKUPS="${1#*=}"; shift ;;
|
||
--health-ports) HEALTH_PORTS="${2:?$1 需要参数}"; shift 2 ;;
|
||
--health-ports=*) HEALTH_PORTS="${1#*=}"; shift ;;
|
||
--health-timeout) HEALTH_TIMEOUT="${2:?$1 需要参数}"; shift 2 ;;
|
||
--health-timeout=*) HEALTH_TIMEOUT="${1#*=}"; shift ;;
|
||
--extra-groups) EXTRA_GROUPS="${2:?$1 需要参数}"; shift 2 ;;
|
||
--extra-groups=*) EXTRA_GROUPS="${1#*=}"; shift ;;
|
||
--service-name) SERVICE_NAME="${2:?$1 需要参数}"; UNIT_DST="$UNIT_DIR/$SERVICE_NAME.service"; shift 2 ;;
|
||
--force-config) FORCE_CONFIG=1; shift ;;
|
||
--force-data) FORCE_DATA=1; shift ;;
|
||
--force) FORCE=1; shift ;;
|
||
--no-start) DO_START=0; shift ;;
|
||
--dry-run) DRY_RUN=1; shift ;;
|
||
--rollback) MODE="rollback"; shift ;;
|
||
--list-backups) MODE="list-backups"; shift ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
*) printf '[deploy] 未知参数: %s(--help 查看用法)\n' "$1" >&2; exit 1 ;;
|
||
esac
|
||
done
|
||
|
||
INSTALL_DIR="${INSTALL_DIR%/}"
|
||
BACKUP_ROOT="${BACKUP_ROOT%/}"
|
||
|
||
case "$INSTALL_DIR" in
|
||
/*) ;;
|
||
*) printf '[deploy] 安装目录必须是绝对路径: %s\n' "$INSTALL_DIR" >&2; exit 1 ;;
|
||
esac
|
||
if [ "$INSTALL_DIR" = "/" ] || [ -z "$INSTALL_DIR" ]; then
|
||
printf '[deploy] 安装目录不能是根目录\n' >&2; exit 1
|
||
fi
|
||
if [ "$UNIT_DST" = "$SERVICE_NAME" ]; then
|
||
UNIT_DST="$UNIT_DIR/$SERVICE_NAME.service"
|
||
fi
|
||
if ! [ "$HEALTH_TIMEOUT" -ge 0 ] 2>/dev/null; then
|
||
printf '[deploy] --health-timeout 需要是非负整数: %s\n' "$HEALTH_TIMEOUT" >&2; exit 1
|
||
fi
|
||
if ! [ "$KEEP_BACKUPS" -ge 0 ] 2>/dev/null; then
|
||
printf '[deploy] --keep-backups 需要是非负整数: %s\n' "$KEEP_BACKUPS" >&2; exit 1
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 输出
|
||
# ==============================================================================
|
||
if [ -t 1 ]; then
|
||
C_RST=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'
|
||
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
|
||
C_CYAN=$'\033[36m'; C_MAGENTA=$'\033[35m'
|
||
else
|
||
C_RST=''; C_BOLD=''; C_DIM=''; C_RED=''; C_GREEN=''; C_YELLOW=''; C_CYAN=''; C_MAGENTA=''
|
||
fi
|
||
|
||
info() { printf '%s[deploy]%s %s\n' "$C_CYAN" "$C_RST" "$*"; }
|
||
ok() { printf '%s[ ok ]%s %s\n' "$C_GREEN" "$C_RST" "$*"; }
|
||
warn() { printf '%s[warn]%s %s\n' "$C_YELLOW" "$C_RST" "$*" >&2; }
|
||
err() { printf '%s[fail]%s %s\n' "$C_RED" "$C_RST" "$*" >&2; }
|
||
step() { printf '\n%s==> %s%s\n' "$C_BOLD" "$*" "$C_RST"; }
|
||
die() { err "$@"; exit 1; }
|
||
|
||
# 所有写操作统一走 run(),--dry-run 时只打印
|
||
run() {
|
||
if [ "$DRY_RUN" = 1 ]; then
|
||
printf '%s[dry-run]%s %s\n' "$C_MAGENTA" "$C_RST" "$(printf '%q ' "$@")"
|
||
return 0
|
||
fi
|
||
"$@"
|
||
}
|
||
|
||
SERVICE_STOPPED=0
|
||
on_error() {
|
||
local rc="$1"
|
||
printf '\n'
|
||
err "部署中断(退出码 $rc)"
|
||
if [ "$SERVICE_STOPPED" = 1 ]; then
|
||
warn "$SERVICE_NAME 目前处于停止状态,恢复方式:"
|
||
warn " 1) 还原上一版本: sudo bash $SELF_PATH --rollback"
|
||
warn " 2) 用当前文件试启动: sudo systemctl start $SERVICE_NAME"
|
||
fi
|
||
exit "$rc"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 探测与工具函数
|
||
# ==============================================================================
|
||
|
||
# 从第 $2 个字段起取值(manifest 用)
|
||
manifest_get() {
|
||
local key="$1" file="$2"
|
||
[ -f "$file" ] || return 1
|
||
awk -F= -v k="$key" '$1==k { sub("^" k "=", ""); print; exit }' "$file"
|
||
}
|
||
|
||
detect_bundle() {
|
||
local cand
|
||
for cand in "$BUNDLE_ARG" "$SCRIPT_DIR/chongming" "$SCRIPT_DIR/build" "$SCRIPT_DIR"; do
|
||
[ -n "$cand" ] || continue
|
||
[ -d "$cand" ] || continue
|
||
if [ -f "$cand/chongming" ] && [ -f "$cand/config.toml" ]; then
|
||
(cd "$cand" && pwd)
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
detect_unit() {
|
||
local cand
|
||
for cand in "$UNIT_ARG" "$SCRIPT_DIR/$SERVICE_NAME.service" \
|
||
"$SCRIPT_DIR/deploy/$SERVICE_NAME.service" \
|
||
"$SCRIPT_DIR/../$SERVICE_NAME.service" \
|
||
"$SCRIPT_DIR/chongming.service"; do
|
||
[ -n "$cand" ] || continue
|
||
if [ -f "$cand" ]; then
|
||
( cd "$(dirname "$cand")" && printf '%s/%s\n' "$(pwd)" "$(basename "$cand")" )
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# 内嵌兜底单元:仅在找不到 chongming.service 时使用
|
||
write_embedded_unit() {
|
||
cat <<'EOF'
|
||
[Unit]
|
||
Description=Chongming Zenoh Gateway (FastAPI + Zenoh + MQTT)
|
||
Wants=network-online.target
|
||
After=network-online.target
|
||
StartLimitIntervalSec=60
|
||
StartLimitBurst=5
|
||
|
||
[Service]
|
||
Type=simple
|
||
ExecStart=/data/chongming/chongming
|
||
WorkingDirectory=/data/chongming
|
||
User=linaro
|
||
Group=linaro
|
||
UMask=0027
|
||
EnvironmentFile=-/etc/chongming.env
|
||
Environment=PYTHONUNBUFFERED=1
|
||
Environment=PATH=/opt/sophon/sophon-ffmpeg-latest/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||
KillSignal=SIGTERM
|
||
KillMode=control-group
|
||
TimeoutStopSec=30
|
||
Restart=on-failure
|
||
RestartSec=3
|
||
LimitNOFILE=65535
|
||
LimitNPROC=65535
|
||
PrivateTmp=true
|
||
NoNewPrivileges=true
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
}
|
||
|
||
# 二进制架构检查:ELF 头 e_machine,aarch64 = 0xB7
|
||
check_binary_arch() {
|
||
local f="$1" magic machine
|
||
magic="$(od -An -tx1 -j0 -N4 "$f" 2>/dev/null | tr -d ' \n')"
|
||
if [ "$magic" != "7f454c46" ]; then
|
||
warn "$f 不是 ELF 文件(magic=$magic)"
|
||
return 1
|
||
fi
|
||
machine="$(od -An -tx1 -j18 -N2 "$f" 2>/dev/null | tr -d ' \n')"
|
||
case "$machine" in
|
||
b700|00b7) return 0 ;;
|
||
*) warn "$f 的 ELF e_machine=$machine,不是 aarch64" ; return 1 ;;
|
||
esac
|
||
}
|
||
|
||
port_listening() {
|
||
local port="$1" hex found=""
|
||
[ -n "$port" ] || return 0
|
||
if command -v ss >/dev/null 2>&1; then
|
||
found="$(ss -ltnH 2>/dev/null | awk -v p=":${port}$" '$4 ~ p { print $4; exit }' || true)"
|
||
if [ -z "$found" ]; then
|
||
found="$(ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p { print $4; exit }' || true)"
|
||
fi
|
||
fi
|
||
if [ -z "$found" ] && command -v netstat >/dev/null 2>&1; then
|
||
found="$(netstat -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p { print $4; exit }' || true)"
|
||
fi
|
||
if [ -n "$found" ]; then return 0; fi
|
||
hex="$(printf '%04X' "$port")"
|
||
found="$(awk -v h="$hex" 'NR>1 && $4=="0A" { n=split($2,a,":"); if (a[n]==h) { print $2; exit } }' /proc/net/tcp 2>/dev/null || true)"
|
||
if [ -n "$found" ]; then return 0; fi
|
||
return 1
|
||
}
|
||
|
||
# 打印占用端口的进程(尽力而为,拿不到就留空)
|
||
port_owner_desc() {
|
||
local port="$1" out=""
|
||
if command -v ss >/dev/null 2>&1; then
|
||
out="$(ss -ltnp 2>/dev/null | awk -v p=":${port}$" '$4 ~ p { print $0; exit }' || true)"
|
||
elif command -v netstat >/dev/null 2>&1; then
|
||
out="$(netstat -ltnp 2>/dev/null | awk -v p=":${port}$" '$4 ~ p { print $0; exit }' || true)"
|
||
fi
|
||
printf '%s' "${out:-未知进程}"
|
||
}
|
||
|
||
unit_installed() { systemctl cat "$SERVICE_NAME.service" >/dev/null 2>&1; }
|
||
svc_active() { systemctl is-active --quiet "$SERVICE_NAME.service"; }
|
||
svc_failed() { systemctl is-failed --quiet "$SERVICE_NAME.service"; }
|
||
svc_mainpid() { systemctl show -p MainPID --value "$SERVICE_NAME.service" 2>/dev/null || true; }
|
||
|
||
snapshot_files() { # dir out_file
|
||
local dir="$1" out="$2"
|
||
if [ ! -d "$dir" ]; then
|
||
: > "$out"
|
||
return 0
|
||
fi
|
||
( cd "$dir" && find . -type f 2>/dev/null | LC_ALL=C sort ) > "$out" || : > "$out"
|
||
}
|
||
|
||
# 找到最近的已存在祖先目录(用于查磁盘空间)
|
||
nearest_existing_dir() {
|
||
local d="$1"
|
||
while [ ! -d "$d" ] && [ "$d" != "/" ] && [ -n "$d" ]; do
|
||
d="$(dirname "$d")"
|
||
done
|
||
printf '%s' "${d:-/}"
|
||
}
|
||
|
||
dir_avail_kb() {
|
||
df -Pk "$1" 2>/dev/null | awk 'NR==2 { print $4 }' || printf '0'
|
||
}
|
||
|
||
dir_size_kb() {
|
||
if [ -e "$1" ]; then du -sk "$1" 2>/dev/null | awk '{ print $1 }'; else printf '0'; fi
|
||
}
|
||
|
||
# 最大的一份备份占用(用于估算备份所需空间),拿不到估算值就返回 0
|
||
backup_estimate_kb() {
|
||
local biggest
|
||
biggest="$(du -sk "$BACKUP_ROOT"/*/ 2>/dev/null | sort -n | tail -1 | awk '{ print $1 }' || true)"
|
||
printf '%s' "${biggest:-0}"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 步骤:提权
|
||
# ==============================================================================
|
||
require_root() {
|
||
if [ "$(id -u)" -eq 0 ]; then return 0; fi
|
||
if [ "$DRY_RUN" = 1 ]; then
|
||
warn "当前非 root;--dry-run 只是预演,继续以当前身份检查"
|
||
return 0
|
||
fi
|
||
if command -v sudo >/dev/null 2>&1; then
|
||
info "需要 root 权限,正在通过 sudo 重新执行 ..."
|
||
if [ $# -gt 0 ]; then
|
||
exec sudo -E bash "$SELF_PATH" "$@"
|
||
fi
|
||
exec sudo -E bash "$SELF_PATH"
|
||
fi
|
||
die "需要 root 权限,且系统没有 sudo。请用 root 身份执行:su -c 'bash $SELF_PATH'"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 步骤:前置检查
|
||
# ==============================================================================
|
||
preflight() {
|
||
step "前置检查"
|
||
local missing=() f
|
||
|
||
command -v systemctl >/dev/null 2>&1 || { err "系统没有 systemctl,本脚本仅适用于 systemd 环境"; exit 2; }
|
||
ok "systemd 可用"
|
||
|
||
# 打包产物
|
||
if [ -z "$BUNDLE_DIR" ]; then
|
||
err "找不到打包产物目录(应包含 chongming 与 config.toml)"
|
||
err "已查找: ${BUNDLE_ARG:-<未指定>} $SCRIPT_DIR/chongming $SCRIPT_DIR/build $SCRIPT_DIR"
|
||
err "用 --bundle <目录> 显式指定"
|
||
exit 2
|
||
fi
|
||
for f in "${REQUIRED_FILES[@]}"; do
|
||
[ -e "$BUNDLE_DIR/$f" ] || missing+=("$f")
|
||
done
|
||
if [ ${#missing[@]} -gt 0 ]; then
|
||
err "打包产物缺少必需文件($BUNDLE_DIR):"
|
||
for f in "${missing[@]}"; do err " - $f"; done
|
||
exit 2
|
||
fi
|
||
ok "打包产物: $BUNDLE_DIR($(du -sh "$BUNDLE_DIR" 2>/dev/null | awk '{print $1}'))"
|
||
|
||
if [ "$UNIT_FROM_EMBEDDED" = 1 ]; then
|
||
warn "未找到 $SERVICE_NAME.service,将使用脚本内嵌的最小单元配置(缺少原注释与调优说明)"
|
||
else
|
||
ok "单元文件: $UNIT_SRC"
|
||
fi
|
||
|
||
# 二进制架构
|
||
if ! check_binary_arch "$BUNDLE_DIR/chongming"; then
|
||
if [ "$FORCE" = 1 ]; then
|
||
warn "已用 --force 忽略架构不匹配,仍继续"
|
||
else
|
||
err "打包产物的 CPU 架构与目标设备不符,请确认用的是 ARM64 构建产物(--force 可强制继续)"
|
||
exit 2
|
||
fi
|
||
else
|
||
ok "二进制架构: aarch64"
|
||
fi
|
||
|
||
# 磁盘空间:安装 + 备份
|
||
local bundle_kb avail_kb need_kb
|
||
bundle_kb="$(dir_size_kb "$BUNDLE_DIR")"
|
||
avail_kb="$(dir_avail_kb "$(nearest_existing_dir "$INSTALL_DIR")")"
|
||
need_kb=$(( bundle_kb + $(backup_estimate_kb) ))
|
||
if [ "$avail_kb" -gt 0 ]; then
|
||
if [ "$avail_kb" -lt "$need_kb" ]; then
|
||
err "磁盘空间不足($(nearest_existing_dir "$INSTALL_DIR")):可用 $((avail_kb/1024)) MB,预计需要 $((need_kb/1024)) MB"
|
||
exit 2
|
||
fi
|
||
ok "磁盘空间: 可用 $((avail_kb/1024)) MB,预计需要 $((need_kb/1024)) MB"
|
||
fi
|
||
|
||
# /tmp:Nuitka onefile 每次启动都要解包,PrivateTmp 用的是 /tmp
|
||
local tmp_avail
|
||
tmp_avail="$(dir_avail_kb /tmp)"
|
||
if [ "$tmp_avail" -gt 0 ] && [ "$tmp_avail" -lt 512000 ]; then
|
||
warn "/tmp 可用空间仅 $((tmp_avail/1024)) MB。Nuitka onefile 启动时需在 /tmp 解包,空间不足会启动失败(可考虑单元里 PrivateTmp=false)"
|
||
fi
|
||
|
||
# ffmpeg
|
||
if [ -d "$FFMPEG_BIN_DIR" ]; then
|
||
ok "sophon-ffmpeg: $FFMPEG_BIN_DIR"
|
||
else
|
||
warn "$FFMPEG_BIN_DIR 不存在,RTSP 拉流可能因找不到 ffmpeg 而失败"
|
||
fi
|
||
|
||
# 运行用户
|
||
if id -u "$RUN_USER" >/dev/null 2>&1; then
|
||
ok "运行用户: $RUN_USER:$(id -gn "$RUN_USER")"
|
||
else
|
||
info "运行用户 $RUN_USER 不存在,安装时会创建为系统用户"
|
||
fi
|
||
|
||
# 端口占用(停服前只看一眼,被外来进程占用才告警)
|
||
local p
|
||
if unit_installed; then
|
||
for p in "${HEALTH_PORTS_ARR[@]}"; do
|
||
if port_listening "$p" && ! svc_active; then
|
||
warn "端口 $p 已被占用($(port_owner_desc "$p")),且 $SERVICE_NAME 当前未运行"
|
||
fi
|
||
done
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 步骤:停服
|
||
# ==============================================================================
|
||
stop_service() {
|
||
step "停止服务"
|
||
if ! unit_installed; then
|
||
info "服务尚未安装,跳过"
|
||
return 0
|
||
fi
|
||
PREV_SERVICE_STATE="$(systemctl is-active "$SERVICE_NAME.service" 2>/dev/null || true)"
|
||
if ! svc_active; then
|
||
info "$SERVICE_NAME 当前未运行(状态: ${PREV_SERVICE_STATE:-unknown}),跳过"
|
||
return 0
|
||
fi
|
||
info "正在停止 $SERVICE_NAME(最多等待 40s,单元 TimeoutStopSec=30)..."
|
||
if [ "$DRY_RUN" = 1 ]; then
|
||
printf '%s[dry-run]%s systemctl stop %s\n' "$C_MAGENTA" "$C_RST" "$SERVICE_NAME.service"
|
||
SERVICE_STOPPED=1
|
||
return 0
|
||
fi
|
||
SERVICE_STOPPED=1
|
||
systemctl stop "$SERVICE_NAME.service" || {
|
||
err "systemctl stop 失败,请先手动确认服务状态:systemctl status $SERVICE_NAME"
|
||
exit 1
|
||
}
|
||
local waited=0
|
||
while svc_active && [ "$waited" -lt 20 ]; do
|
||
sleep 1
|
||
waited=$((waited + 1))
|
||
done
|
||
if svc_active; then
|
||
err "$SERVICE_NAME 在 20s 内未停止(PID $(svc_mainpid))"
|
||
exit 1
|
||
fi
|
||
ok "服务已停止(等待 ${waited}s)"
|
||
|
||
# 端口释放检查:内核回收监听套接字可能有短暂延迟,先给 10s 宽限;仍被占用才判定为残留
|
||
local p owner held="" grace=0
|
||
while [ "$grace" -lt 10 ]; do
|
||
held=""
|
||
for p in "${HEALTH_PORTS_ARR[@]}"; do
|
||
port_listening "$p" && held="$held $p"
|
||
done
|
||
[ -z "$held" ] && break
|
||
sleep 1
|
||
grace=$((grace + 1))
|
||
done
|
||
if [ -n "$held" ]; then
|
||
for p in $held; do
|
||
owner="$(port_owner_desc "$p")"
|
||
err "端口 $p 在服务停止 ${grace}s 后仍被占用: $owner"
|
||
done
|
||
err "可能有残留进程或其它服务在用该端口。请先处理,再重新执行部署。"
|
||
err "如需恢复现场: sudo systemctl start $SERVICE_NAME"
|
||
exit 1
|
||
fi
|
||
ok "端口已释放: ${HEALTH_PORTS//,/ }"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 步骤:备份
|
||
# ==============================================================================
|
||
BACKUP_DIR=""
|
||
|
||
do_backup() {
|
||
step "备份现有版本"
|
||
if [ ! -d "$INSTALL_DIR" ]; then
|
||
info "$INSTALL_DIR 不存在(全新安装),无需备份"
|
||
return 0
|
||
fi
|
||
|
||
BACKUP_DIR="$BACKUP_ROOT/$(date +%Y%m%d-%H%M%S)"
|
||
run mkdir -p "$BACKUP_DIR"
|
||
|
||
local members=() m
|
||
for m in chongming config.toml static.svfs config resources data cache_data; do
|
||
[ -e "$INSTALL_DIR/$m" ] && members+=("$m")
|
||
done
|
||
|
||
if [ ${#members[@]} -gt 0 ]; then
|
||
# 排除 logs 与 data/uploads:体积大且不承载站点状态
|
||
run tar -czf "$BACKUP_DIR/install.tar.gz" \
|
||
-C "$INSTALL_DIR" --exclude='logs' --exclude='data/uploads' \
|
||
"${members[@]}"
|
||
fi
|
||
|
||
if [ -f "$UNIT_DST" ]; then
|
||
run cp -f "$UNIT_DST" "$BACKUP_DIR/$SERVICE_NAME.service"
|
||
fi
|
||
if [ -f "$ENV_FILE" ]; then
|
||
run cp -f "$ENV_FILE" "$BACKUP_DIR/$(basename "$ENV_FILE")"
|
||
fi
|
||
|
||
# 清单:让 --rollback 在没带参数时也能还原到正确位置
|
||
if [ "$DRY_RUN" != 1 ]; then
|
||
cat > "$BACKUP_DIR/manifest.txt" <<EOF
|
||
TIME=$(date '+%Y-%m-%d %H:%M:%S')
|
||
INSTALL_DIR=$INSTALL_DIR
|
||
RUN_USER=$RUN_USER
|
||
RUN_GROUP=$RUN_GROUP
|
||
SERVICE_NAME=$SERVICE_NAME
|
||
SERVICE_UNIT=$UNIT_DST
|
||
ENV_FILE=$ENV_FILE
|
||
HEALTH_PORTS=$HEALTH_PORTS
|
||
BUNDLE_DIR=$BUNDLE_DIR
|
||
PREV_SERVICE_STATE=${PREV_SERVICE_STATE:-unknown}
|
||
DEPLOY_SCRIPT=$SELF_PATH
|
||
EOF
|
||
else
|
||
printf '%s[dry-run]%s 写入 %s/manifest.txt\n' "$C_MAGENTA" "$C_RST" "$BACKUP_DIR"
|
||
fi
|
||
|
||
local size
|
||
size="$(du -sh "$BACKUP_DIR" 2>/dev/null | awk '{print $1}' || true)"
|
||
ok "备份完成: $BACKUP_DIR(${size:-未知大小})"
|
||
|
||
prune_backups
|
||
}
|
||
|
||
prune_backups() {
|
||
[ "$KEEP_BACKUPS" -gt 0 ] || return 0
|
||
local olds=() d
|
||
while IFS= read -r d; do
|
||
[ -n "$d" ] && olds+=("$d")
|
||
done < <(ls -1dt "$BACKUP_ROOT"/*/ 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)) || true)
|
||
if [ ${#olds[@]} -eq 0 ]; then return 0; fi
|
||
for d in "${olds[@]}"; do
|
||
info "清理旧备份: $d"
|
||
run rm -rf "$d"
|
||
done
|
||
}
|
||
|
||
list_backups() {
|
||
if [ ! -d "$BACKUP_ROOT" ]; then
|
||
info "备份目录不存在: $BACKUP_ROOT"
|
||
return 0
|
||
fi
|
||
local d n=0 size mtime minstall
|
||
while IFS= read -r d; do
|
||
[ -n "$d" ] || continue
|
||
n=$((n + 1))
|
||
size="$(du -sh "$d" 2>/dev/null | awk '{print $1}' || true)"
|
||
printf ' %2d) %-44s %s\n' "$n" "$d" "${size:-?}"
|
||
if [ -f "$d/manifest.txt" ]; then
|
||
mtime="$(manifest_get TIME "$d/manifest.txt" || true)"
|
||
minstall="$(manifest_get INSTALL_DIR "$d/manifest.txt" || true)"
|
||
printf ' 时间=%s 安装目录=%s\n' "${mtime:-?}" "${minstall:-?}"
|
||
fi
|
||
done < <(ls -1dt "$BACKUP_ROOT"/*/ 2>/dev/null || true)
|
||
if [ "$n" -eq 0 ]; then info "暂无备份"; fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 步骤:安装文件
|
||
# ==============================================================================
|
||
CFG_ADDED=0
|
||
CFG_KEPT=0
|
||
DATA_ADDED=0
|
||
DATA_KEPT=0
|
||
|
||
# 同步站点状态目录
|
||
# merge:no-clobber,设备上已有的文件不动,打包版新增的文件补进来
|
||
# force:打包版覆盖同名文件(设备上多出来的文件不删除)
|
||
# 结果写入 LAST_ADDED / LAST_KEPT
|
||
LAST_ADDED=0
|
||
LAST_KEPT=0
|
||
sync_state_dir() { # src dst mode
|
||
local src="$1" dst="$2" mode="$3"
|
||
LAST_ADDED=0
|
||
LAST_KEPT=0
|
||
[ -d "$src" ] || return 0
|
||
if [ "$DRY_RUN" = 1 ]; then
|
||
local flag=""
|
||
[ "$mode" = "merge" ] && flag="n"
|
||
printf '%s[dry-run]%s cp -a%s %s/. %s/\n' "$C_MAGENTA" "$C_RST" "$flag" "$src" "$dst"
|
||
return 0
|
||
fi
|
||
local before after
|
||
before="$(mktemp)"
|
||
after="$(mktemp)"
|
||
snapshot_files "$dst" "$before"
|
||
mkdir -p "$dst"
|
||
if [ "$mode" = "merge" ]; then
|
||
cp -an "$src/." "$dst/"
|
||
else
|
||
cp -a "$src/." "$dst/"
|
||
fi
|
||
snapshot_files "$dst" "$after"
|
||
LAST_ADDED="$(comm -13 "$before" "$after" | wc -l | tr -d ' ')"
|
||
LAST_KEPT="$(comm -12 "$before" "$after" | wc -l | tr -d ' ')"
|
||
rm -f "$before" "$after"
|
||
return 0
|
||
}
|
||
|
||
install_files() {
|
||
step "安装文件到 $INSTALL_DIR"
|
||
|
||
run mkdir -p "$INSTALL_DIR"
|
||
|
||
# 1) 二进制:先删后拷,避免正在运行的旧文件导致 ETXTBSY
|
||
run rm -f "$INSTALL_DIR/chongming"
|
||
run cp -f "$BUNDLE_DIR/chongming" "$INSTALL_DIR/chongming"
|
||
run chmod 0755 "$INSTALL_DIR/chongming"
|
||
|
||
# 2) 应用资产:整体覆盖
|
||
local d f
|
||
for d in "${OVERWRITE_DIRS[@]}"; do
|
||
if [ -d "$BUNDLE_DIR/$d" ]; then
|
||
run rm -rf "$INSTALL_DIR/$d"
|
||
run cp -a "$BUNDLE_DIR/$d" "$INSTALL_DIR/$d"
|
||
fi
|
||
done
|
||
for f in "${OVERWRITE_FILES[@]}"; do
|
||
if [ -e "$BUNDLE_DIR/$f" ]; then
|
||
run cp -f "$BUNDLE_DIR/$f" "$INSTALL_DIR/$f"
|
||
fi
|
||
done
|
||
|
||
# 3) 站点配置:默认设备优先
|
||
local cfg_mode="merge"
|
||
if [ "$FORCE_CONFIG" = 1 ]; then
|
||
cfg_mode="force"
|
||
warn "--force-config:用打包版覆盖 config/ 与 config.toml(设备上多出来的文件不会被删除)"
|
||
fi
|
||
sync_state_dir "$BUNDLE_DIR/config" "$INSTALL_DIR/config" "$cfg_mode"
|
||
CFG_ADDED="$LAST_ADDED"
|
||
CFG_KEPT="$LAST_KEPT"
|
||
if [ "$cfg_mode" = "force" ] || [ ! -f "$INSTALL_DIR/config.toml" ]; then
|
||
if [ -e "$BUNDLE_DIR/config.toml" ]; then
|
||
run cp -f "$BUNDLE_DIR/config.toml" "$INSTALL_DIR/config.toml"
|
||
fi
|
||
else
|
||
info "保留设备上的 config.toml(站点配置);如需用打包版覆盖请加 --force-config"
|
||
fi
|
||
|
||
# 4) 运行时数据:默认设备优先
|
||
local data_mode="merge"
|
||
if [ "$FORCE_DATA" = 1 ]; then
|
||
data_mode="force"
|
||
warn "--force-data:用打包版覆盖 data/ 与 cache_data/"
|
||
fi
|
||
for d in data cache_data; do
|
||
sync_state_dir "$BUNDLE_DIR/$d" "$INSTALL_DIR/$d" "$data_mode"
|
||
DATA_ADDED=$((DATA_ADDED + LAST_ADDED))
|
||
DATA_KEPT=$((DATA_KEPT + LAST_KEPT))
|
||
done
|
||
|
||
# 5) 运行目录:只创建,不从打包版拷日志
|
||
run mkdir -p "$INSTALL_DIR/data/uploads" "$INSTALL_DIR/cache_data" "$INSTALL_DIR/logs"
|
||
|
||
ok "文件已就位"
|
||
|
||
# 6) 属主与权限
|
||
run chown -R "$RUN_USER:$RUN_GROUP" "$INSTALL_DIR"
|
||
run chmod 0755 "$INSTALL_DIR/chongming"
|
||
run chmod 0750 "$INSTALL_DIR"
|
||
for d in data cache_data logs config resources; do
|
||
if [ -d "$INSTALL_DIR/$d" ]; then
|
||
run chmod 0750 "$INSTALL_DIR/$d"
|
||
fi
|
||
done
|
||
ok "属主 $RUN_USER:$RUN_GROUP,目录权限 0750"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 步骤:运行用户与附加组
|
||
# ==============================================================================
|
||
setup_user() {
|
||
step "运行用户与权限"
|
||
if id -u "$RUN_USER" >/dev/null 2>&1; then
|
||
local cur_group
|
||
cur_group="$(id -gn "$RUN_USER")"
|
||
if [ "$cur_group" != "$RUN_GROUP" ]; then
|
||
warn "$RUN_USER 的主组是 $cur_group,与 --group 指定的 $RUN_GROUP 不同;单元文件仍按 $RUN_USER:$RUN_GROUP 启动(root 可跨组设置)"
|
||
fi
|
||
ok "运行用户已存在: $RUN_USER:$cur_group"
|
||
else
|
||
if getent group "$RUN_GROUP" >/dev/null 2>&1; then
|
||
run useradd --system --home-dir "$INSTALL_DIR" --shell /usr/sbin/nologin --gid "$RUN_GROUP" "$RUN_USER"
|
||
else
|
||
run useradd --system --home-dir "$INSTALL_DIR" --shell /usr/sbin/nologin --user-group "$RUN_USER"
|
||
fi
|
||
ok "已创建系统用户: $RUN_USER(home=$INSTALL_DIR,shell=nologin)"
|
||
fi
|
||
|
||
# 附加组(GPIO IN2 / /dev/input/event1 需要 input 组)
|
||
local groups_arr=() g
|
||
IFS=',' read -r -a groups_arr <<< "$EXTRA_GROUPS"
|
||
for g in "${groups_arr[@]}"; do
|
||
[ -n "$g" ] || continue
|
||
if ! getent group "$g" >/dev/null 2>&1; then
|
||
warn "附加组 $g 不存在,跳过"
|
||
continue
|
||
fi
|
||
if id -nG "$RUN_USER" 2>/dev/null | tr ' ' '\n' | grep -qx "$g"; then
|
||
info "$RUN_USER 已在 $g 组内"
|
||
else
|
||
run usermod -aG "$g" "$RUN_USER"
|
||
ok "已将 $RUN_USER 加入 $g 组"
|
||
fi
|
||
done
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 步骤:安装 systemd 单元
|
||
# ==============================================================================
|
||
render_unit() { # src out
|
||
local src="$1" out="$2"
|
||
{
|
||
cat <<EOF
|
||
# $SERVICE_NAME.service — 由 deploy.sh 生成(本文件的 mtime 即上次部署时间)
|
||
#
|
||
# 安装目录: $INSTALL_DIR
|
||
# 运行用户: $RUN_USER:$RUN_GROUP
|
||
# 重新部署: sudo bash $SELF_PATH
|
||
# 回滚: sudo bash $SELF_PATH --rollback
|
||
#
|
||
# 常用操作:
|
||
# systemctl status $SERVICE_NAME # 状态
|
||
# journalctl -u $SERVICE_NAME -f # 实时日志(应用自身日志另见 $INSTALL_DIR/logs/)
|
||
# systemctl restart $SERVICE_NAME # 重启(SIGTERM 优雅排空,最长等待 30s)
|
||
#
|
||
# 可选:$ENV_FILE 中写 APP_WORKERS=<N> 覆盖 worker 数。
|
||
# 注意:本文件由 deploy.sh 重写 ExecStart/WorkingDirectory/User/Group/EnvironmentFile,
|
||
# 手改这几行会在下次部署时被覆盖,请改用命令行参数。
|
||
EOF
|
||
awk -v d="$INSTALL_DIR" -v u="$RUN_USER" -v g="$RUN_GROUP" -v ef="$ENV_FILE" '
|
||
BEGIN { hdr = 1 }
|
||
{ if (hdr) { if ($0 ~ /^#/ || $0 ~ /^[[:space:]]*$/) next; hdr = 0 } }
|
||
/^[[:space:]]*ExecStart=/ { print "ExecStart=" d "/chongming"; next }
|
||
/^[[:space:]]*WorkingDirectory=/ { print "WorkingDirectory=" d; next }
|
||
/^[[:space:]]*User=/ { print "User=" u; next }
|
||
/^[[:space:]]*Group=/ { print "Group=" g; next }
|
||
/^[[:space:]]*EnvironmentFile=/ { print "EnvironmentFile=-" ef; next }
|
||
{ print }
|
||
' "$src" | sed 's/\r$//'
|
||
} > "$out"
|
||
}
|
||
|
||
install_unit() {
|
||
step "安装 systemd 单元"
|
||
local tmp template rendered_changed=0
|
||
tmp="$(mktemp)"
|
||
template="$UNIT_SRC"
|
||
|
||
if [ "$UNIT_FROM_EMBEDDED" = 1 ]; then
|
||
template="$(mktemp)"
|
||
write_embedded_unit > "$template"
|
||
fi
|
||
render_unit "$template" "$tmp"
|
||
if [ "$UNIT_FROM_EMBEDDED" = 1 ]; then rm -f "$template"; fi
|
||
|
||
if [ -f "$UNIT_DST" ] && cmp -s "$tmp" "$UNIT_DST"; then
|
||
info "单元内容无变化,跳过写入"
|
||
else
|
||
if [ -f "$UNIT_DST" ]; then
|
||
info "更新单元文件(旧文件已在备份目录中)"
|
||
else
|
||
info "写入单元文件 $UNIT_DST"
|
||
fi
|
||
if [ "$DRY_RUN" = 1 ]; then
|
||
printf '%s[dry-run]%s install -m 0644 %s %s\n' "$C_MAGENTA" "$C_RST" "$tmp" "$UNIT_DST"
|
||
else
|
||
install -m 0644 "$tmp" "$UNIT_DST"
|
||
fi
|
||
rendered_changed=1
|
||
fi
|
||
rm -f "$tmp"
|
||
|
||
# 语法自检(尽力而为,仅告警)
|
||
if [ "$DRY_RUN" != 1 ] && [ "$rendered_changed" = 1 ] && command -v systemd-analyze >/dev/null 2>&1; then
|
||
if ! systemd-analyze verify "$UNIT_DST" >/dev/null 2>&1; then
|
||
warn "systemd-analyze verify 对 $UNIT_DST 有告警,请留意(多为 EnvironmentFile/路径不存在等提示)"
|
||
fi
|
||
fi
|
||
|
||
# 可选环境变量文件
|
||
if [ -f "$ENV_FILE" ]; then
|
||
info "保留现有 $ENV_FILE"
|
||
else
|
||
if [ "$DRY_RUN" = 1 ]; then
|
||
printf '%s[dry-run]%s 创建 %s\n' "$C_MAGENTA" "$C_RST" "$ENV_FILE"
|
||
else
|
||
cat > "$ENV_FILE" <<'EOF'
|
||
# chongming 可选环境变量(由 deploy.sh 创建)
|
||
# 取消注释并修改后执行 systemctl restart chongming 生效
|
||
# APP_WORKERS=4
|
||
EOF
|
||
chmod 0644 "$ENV_FILE"
|
||
fi
|
||
ok "已创建 $ENV_FILE"
|
||
fi
|
||
|
||
run systemctl daemon-reload
|
||
ok "systemd 已重新加载"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 步骤:启动与健康检查
|
||
# ==============================================================================
|
||
start_service() {
|
||
step "启动服务"
|
||
run systemctl enable --now "$SERVICE_NAME.service"
|
||
ok "systemctl enable --now $SERVICE_NAME"
|
||
}
|
||
|
||
health_check() {
|
||
step "健康检查(超时 ${HEALTH_TIMEOUT}s,端口 ${HEALTH_PORTS})"
|
||
if [ "$DRY_RUN" = 1 ]; then
|
||
info "--dry-run:跳过健康检查"
|
||
return 0
|
||
fi
|
||
if [ "$HEALTH_TIMEOUT" -eq 0 ]; then
|
||
info "健康检查超时为 0,跳过"
|
||
return 0
|
||
fi
|
||
|
||
local deadline=$((SECONDS + HEALTH_TIMEOUT))
|
||
local last_pid="" stable=0 missing="" p
|
||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||
if svc_failed; then
|
||
err "$SERVICE_NAME 已进入 failed 状态(启动重试次数用尽)"
|
||
dump_diagnostics
|
||
exit 3
|
||
fi
|
||
if svc_active; then
|
||
missing=""
|
||
for p in "${HEALTH_PORTS_ARR[@]}"; do
|
||
port_listening "$p" || missing="$missing $p"
|
||
done
|
||
if [ -z "$missing" ]; then
|
||
local pid
|
||
pid="$(svc_mainpid)"
|
||
if [ -n "$pid" ] && [ "$pid" = "$last_pid" ]; then
|
||
stable=$((stable + 1))
|
||
else
|
||
stable=1
|
||
last_pid="$pid"
|
||
fi
|
||
if [ "$stable" -ge 2 ]; then
|
||
ok "服务运行中(PID $pid),端口全部就绪"
|
||
return 0
|
||
fi
|
||
info "服务已启动(PID ${pid:-?}),等待稳定性确认 ..."
|
||
else
|
||
info "等待端口就绪:${missing}"
|
||
fi
|
||
else
|
||
info "等待服务进入 active ..."
|
||
fi
|
||
sleep 2
|
||
done
|
||
|
||
err "健康检查超时(${HEALTH_TIMEOUT}s)"
|
||
if [ -n "$missing" ]; then
|
||
err "以下端口未监听:${missing}"
|
||
for p in $missing; do
|
||
# 端口明明没在监听却被别的东西占了是不可能的,只有监听中才值得报占用者
|
||
if port_listening "$p"; then
|
||
err " 端口 $p 正被其它进程占用: $(port_owner_desc "$p")"
|
||
fi
|
||
done
|
||
fi
|
||
dump_diagnostics
|
||
exit 3
|
||
}
|
||
|
||
dump_diagnostics() {
|
||
printf '\n'
|
||
warn "---- systemctl status $SERVICE_NAME ----"
|
||
systemctl status "$SERVICE_NAME.service" --no-pager -l 2>&1 | head -40 || true
|
||
warn "---- journalctl -u $SERVICE_NAME(最后 80 行)----"
|
||
journalctl -u "$SERVICE_NAME.service" --no-pager -n 80 2>&1 || true
|
||
warn "---- 应用日志(最后 40 行)----"
|
||
if [ -d "$INSTALL_DIR/logs" ]; then
|
||
find "$INSTALL_DIR/logs" -maxdepth 1 -name '*.log' -type f -print0 2>/dev/null \
|
||
| xargs -0 -r tail -n 40 2>/dev/null | tail -n 80 || true
|
||
fi
|
||
printf '\n'
|
||
warn "排查建议:"
|
||
warn " - 配置问题: 检查 $INSTALL_DIR/config.toml 与 $INSTALL_DIR/config/workers/*/config.toml"
|
||
warn " - 依赖缺失: ldconfig -p | grep -i zenoh;RTSP 相关先确认 $FFMPEG_BIN_DIR 存在"
|
||
warn " - 还原上一版本: sudo bash $SELF_PATH --rollback"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 模式:回滚
|
||
# ==============================================================================
|
||
do_rollback() {
|
||
step "回滚到最近一次备份"
|
||
|
||
local latest
|
||
latest="$(ls -1dt "$BACKUP_ROOT"/*/ 2>/dev/null | head -1 || true)"
|
||
if [ -z "$latest" ]; then
|
||
die "备份目录 $BACKUP_ROOT 下没有可用的备份"
|
||
fi
|
||
latest="${latest%/}"
|
||
|
||
# 未显式指定参数时,按备份清单还原
|
||
if [ -f "$latest/manifest.txt" ]; then
|
||
local m_install m_user m_group m_env m_svc
|
||
m_install="$(manifest_get INSTALL_DIR "$latest/manifest.txt" || true)"
|
||
m_user="$(manifest_get RUN_USER "$latest/manifest.txt" || true)"
|
||
m_group="$(manifest_get RUN_GROUP "$latest/manifest.txt" || true)"
|
||
m_env="$(manifest_get ENV_FILE "$latest/manifest.txt" || true)"
|
||
m_svc="$(manifest_get SERVICE_NAME "$latest/manifest.txt" || true)"
|
||
[ -n "$m_install" ] && INSTALL_DIR="$m_install"
|
||
[ -n "$m_user" ] && RUN_USER="$m_user"
|
||
[ -n "$m_group" ] && RUN_GROUP="$m_group"
|
||
[ -n "$m_env" ] && ENV_FILE="$m_env"
|
||
[ -n "$m_svc" ] && SERVICE_NAME="$m_svc"
|
||
UNIT_DST="$UNIT_DIR/$SERVICE_NAME.service"
|
||
info "按备份清单元数据还原"
|
||
fi
|
||
|
||
info "使用备份 : $latest"
|
||
info "安装目录 : $INSTALL_DIR"
|
||
info "运行用户 : $RUN_USER:$RUN_GROUP"
|
||
if [ -f "$latest/install.tar.gz" ]; then
|
||
info "内容 : $(tar -tzf "$latest/install.tar.gz" 2>/dev/null | awk -F/ '{print $1}' | sort -u | tr '\n' ' ' || true)"
|
||
else
|
||
die "备份中缺少 install.tar.gz: $latest"
|
||
fi
|
||
|
||
if [ "$DRY_RUN" = 1 ]; then
|
||
info "--dry-run:以下操作不会真正执行"
|
||
fi
|
||
|
||
if unit_installed && svc_active; then
|
||
SERVICE_STOPPED=1
|
||
run systemctl stop "$SERVICE_NAME.service"
|
||
ok "服务已停止"
|
||
fi
|
||
|
||
run mkdir -p "$INSTALL_DIR"
|
||
# 覆盖式解包:刻意不先清空目录,保证 logs/ 与 data/uploads/ 不丢
|
||
run tar -xzf "$latest/install.tar.gz" -C "$INSTALL_DIR"
|
||
ok "文件已还原(覆盖同名文件;设备上多出来的新文件不会被删除)"
|
||
|
||
if [ -f "$latest/$SERVICE_NAME.service" ]; then
|
||
if [ "$DRY_RUN" = 1 ]; then
|
||
printf '%s[dry-run]%s install -m 0644 %s %s\n' "$C_MAGENTA" "$C_RST" "$latest/$SERVICE_NAME.service" "$UNIT_DST"
|
||
else
|
||
install -m 0644 "$latest/$SERVICE_NAME.service" "$UNIT_DST"
|
||
fi
|
||
ok "单元文件已还原"
|
||
fi
|
||
if [ -f "$latest/$(basename "$ENV_FILE")" ]; then
|
||
run cp -f "$latest/$(basename "$ENV_FILE")" "$ENV_FILE"
|
||
ok "环境变量文件已还原"
|
||
fi
|
||
|
||
run chown -R "$RUN_USER:$RUN_GROUP" "$INSTALL_DIR"
|
||
run systemctl daemon-reload
|
||
run systemctl restart "$SERVICE_NAME.service"
|
||
ok "服务已用旧版本重启"
|
||
|
||
health_check
|
||
|
||
printf '\n'
|
||
ok "回滚完成:$SERVICE_NAME 已恢复到 $(basename "$latest") 时的版本"
|
||
warn "注意:logs/ 与 data/uploads/ 保持回滚前的状态未被覆盖"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 摘要
|
||
# ==============================================================================
|
||
summary() {
|
||
printf '\n%s================ 部署完成 ================%s\n' "$C_GREEN" "$C_RST"
|
||
printf ' %-12s %s\n' "安装目录" "$INSTALL_DIR"
|
||
printf ' %-12s %s\n' "运行用户" "$RUN_USER:$RUN_GROUP"
|
||
printf ' %-12s %s\n' "服务单元" "$UNIT_DST"
|
||
printf ' %-12s %s\n' "监听端口" "${HEALTH_PORTS//,/ }"
|
||
if [ -n "$BACKUP_DIR" ]; then
|
||
printf ' %-12s %s\n' "本次备份" "$BACKUP_DIR"
|
||
fi
|
||
if [ "$FORCE_CONFIG" = 0 ] && [ "$DRY_RUN" = 0 ]; then
|
||
printf ' %-12s config 新增 %s 个 / 保留现场 %s 个文件\n' "配置" "$CFG_ADDED" "$CFG_KEPT"
|
||
fi
|
||
if [ "$FORCE_DATA" = 0 ] && [ "$DRY_RUN" = 0 ]; then
|
||
printf ' %-12s data 新增 %s 个 / 保留现场 %s 个文件\n' "数据" "$DATA_ADDED" "$DATA_KEPT"
|
||
fi
|
||
printf '\n 查看状态 systemctl status %s\n' "$SERVICE_NAME"
|
||
printf ' 实时日志 journalctl -u %s -f\n' "$SERVICE_NAME"
|
||
printf ' 应用日志 tail -f %s/logs/app-*.log\n' "$INSTALL_DIR"
|
||
printf ' 回滚命令 sudo bash %s --rollback\n' "$SELF_PATH"
|
||
printf '%s==========================================%s\n' "$C_GREEN" "$C_RST"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 主流程
|
||
# ==============================================================================
|
||
main() {
|
||
parse_args "$@"
|
||
IFS=',' read -r -a HEALTH_PORTS_ARR <<< "$HEALTH_PORTS"
|
||
|
||
if [ "$MODE" = "list-backups" ]; then
|
||
require_root "$@"
|
||
list_backups
|
||
exit 0
|
||
fi
|
||
|
||
if [ "$MODE" = "rollback" ]; then
|
||
require_root "$@"
|
||
trap 'on_error $?' ERR
|
||
do_rollback
|
||
exit 0
|
||
fi
|
||
|
||
# 部署模式
|
||
require_root "$@"
|
||
trap 'on_error $?' ERR
|
||
|
||
if BUNDLE_DIR="$(detect_bundle)"; then
|
||
:
|
||
else
|
||
BUNDLE_DIR=""
|
||
fi
|
||
if UNIT_SRC="$(detect_unit)"; then
|
||
:
|
||
else
|
||
UNIT_SRC=""
|
||
UNIT_FROM_EMBEDDED=1
|
||
fi
|
||
|
||
# rollback 模式不需要打包产物,部署模式必须有
|
||
if [ -n "$BUNDLE_ARG" ] && [ -z "$BUNDLE_DIR" ]; then
|
||
err "指定的打包目录无效: $BUNDLE_ARG"
|
||
err "该目录需包含: ${REQUIRED_FILES[*]}"
|
||
exit 2
|
||
fi
|
||
|
||
if [ "$DRY_RUN" = 1 ]; then
|
||
warn "== dry-run 模式:所有写操作只打印,不落盘 =="
|
||
fi
|
||
|
||
preflight
|
||
stop_service
|
||
do_backup
|
||
install_files
|
||
setup_user
|
||
install_unit
|
||
|
||
if [ "$DO_START" = 1 ]; then
|
||
start_service
|
||
health_check
|
||
SERVICE_STOPPED=0
|
||
summary
|
||
else
|
||
printf '\n'
|
||
ok "安装完成(--no-start:未启动服务)"
|
||
info "启动命令: sudo systemctl start $SERVICE_NAME"
|
||
fi
|
||
|
||
if [ "$DRY_RUN" = 1 ]; then
|
||
printf '\n'
|
||
warn "== dry-run 结束:以上命令均未执行 =="
|
||
fi
|
||
}
|
||
|
||
main "$@"
|