#!/bin/bash ############################################################################### # PlateRecApp 一键部署启动脚本 (fix24 v41) # # 功能: # deploy 编译 + 部署 + 重启服务(默认动作) # build 仅编译 # restart 重启 systemd 服务 # start 启动服务 # stop 停止服务 # status 查看服务状态 + 端口 + 进程 + 磁盘 # logs 跟踪运行日志 (PlateRecApp.log) # journal 跟踪 systemd journal # backup 手动备份当前部署 # rollback 回滚到上一次备份 # deps 检查/安装编译与运行依赖 # sudoers 配置 frpc 管理所需 sudoers 权限 # # 用法: # bash deploy.sh # 默认 = deploy(编译+部署+重启) # bash deploy.sh build # bash deploy.sh restart # bash deploy.sh logs # sudo bash deploy.sh deps # 安装系统依赖需要 root # # 可用环境变量覆盖默认值: # APP_DIR 部署目录 (默认 /opt/openAI/project/003.PlateRecAPP) # RUN_USER 运行用户 (默认 stevenroc) # SVC_NAME systemd 服务名 (默认 PlateRecApp) ############################################################################### set -euo pipefail # ============================ 配置区(可环境变量覆盖) ============================ APP_DIR="${APP_DIR:-/opt/openAI/project/003.PlateRecAPP}" RUN_USER="${RUN_USER:-stevenroc}" SVC_NAME="${SVC_NAME:-PlateRecApp}" SERVICE_FILE="/etc/systemd/system/${SVC_NAME}.service" # 源码目录 = 本脚本所在目录的上一级(脚本位于 /scripts/deploy.sh) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SRC_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" BUILD_DIR="${SRC_DIR}/build" BIN_NAME="PlateRecApp" LOG_FILE="${APP_DIR}/PlateRecApp.log" BACKUP_ROOT="${APP_DIR}/backup" HEALTH_PORT="8080" # HTTPS 服务端口(与 config.ini web_server_url 一致) HTTP_REDIRECT_PORT="8079" # HTTP→HTTPS 重定向端口 # 颜色输出 RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m' info() { echo -e "${GREEN}[✓]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } err() { echo -e "${RED}[✗]${NC} $*" >&2; } step() { echo -e "${CYAN}▶ $*${NC}"; } die() { err "$*"; exit 1; } # root 检测 is_root() { [ "$(id -u)" -eq 0 ]; } # 以 RUN_USER 身份执行命令(root 时降权,非 root 且用户匹配则直接执行) as_run_user() { if is_root; then if id "$RUN_USER" &>/dev/null; then sudo -u "$RUN_USER" "$@" else warn "用户 $RUN_USER 不存在,以当前用户执行: $*" "$@" fi else "$@" fi } # ============================ 依赖检查 ============================ deps_check() { step "检查编译/运行依赖..." local missing=() # 编译工具 for cmd in cmake make g++ pkg-config; do command -v "$cmd" &>/dev/null || missing+=("$cmd") done # 运行工具 for cmd in curl ss; do command -v "$cmd" &>/dev/null || missing+=("$cmd") done # 开发库(pkg-config / 头文件探测) pkg-config --exists opencv4 2>/dev/null || missing+=("libopencv-dev") pkg-config --exists openssl 2>/dev/null || missing+=("libssl-dev") pkg-config --exists sqlite3 2>/dev/null || missing+=("libsqlite3-dev") pkg-config --exists freetype2 2>/dev/null || missing+=("libfreetype-dev") pkg-config --exists libcurl 2>/dev/null || missing+=("libcurl4-openssl-dev") [ -f /usr/include/cjson/cJSON.h ] || [ -f /usr/local/include/cjson/cJSON.h ] || missing+=("libcjson-dev") # HyperLPR3 SDK(源码树内) [ -d "${SRC_DIR}/hyperlpr3/lib" ] || warn "未找到 ${SRC_DIR}/hyperlpr3/lib —— 编译需要 HyperLPR3 SDK,请确认 SDK 已部署到源码目录" if [ ${#missing[@]} -gt 0 ]; then warn "缺少依赖: ${missing[*]}" echo " 安装命令(Debian/Ubuntu/RPi OS):" echo " sudo apt update && sudo apt install -y cmake build-essential pkg-config \\" echo " libopencv-dev libssl-dev libsqlite3-dev libfreetype-dev \\" echo " libcurl4-openssl-dev libcjson-dev curl iproute2" return 1 fi info "依赖检查通过" return 0 } deps_install() { is_root || die "安装依赖需要 root,请用: sudo bash $0 deps" step "安装编译与运行依赖..." apt update apt install -y cmake build-essential pkg-config \ libopencv-dev libssl-dev libsqlite3-dev libfreetype-dev \ libcurl4-openssl-dev libcjson-dev curl iproute2 info "依赖安装完成" # nginx stream 模块(v2 架构需要,不强制) if ! nginx -V 2>&1 | grep -q stream; then warn "nginx stream 模块未安装(v2 公网架构需要): sudo apt install -y libnginx-mod-stream" fi } # ============================ 编译 ============================ do_build() { step "编译 PlateRecApp (源码: ${SRC_DIR})" deps_check || die "依赖缺失,请先运行: sudo bash $0 deps" mkdir -p "${BUILD_DIR}" cd "${BUILD_DIR}" cmake -DCMAKE_BUILD_TYPE=Release "${SRC_DIR}" make -j"$(nproc)" [ -x "${BUILD_DIR}/${BIN_NAME}" ] || die "编译失败:未生成 ${BUILD_DIR}/${BIN_NAME}" info "编译成功: ${BUILD_DIR}/${BIN_NAME} ($(du -h "${BUILD_DIR}/${BIN_NAME}" | cut -f1))" } # ============================ 部署 ============================ ensure_dirs() { step "创建部署目录结构: ${APP_DIR}" mkdir -p "${APP_DIR}"/{assets/web,hyperlpr3,PlateJPG,data,scripts,ssl,backup} } backup_current() { if [ ! -f "${APP_DIR}/${BIN_NAME}" ]; then info "首次部署(${APP_DIR}/${BIN_NAME} 不存在),跳过备份" return 0 fi local ts; ts="$(date +%Y%m%d_%H%M%S)" local bk="${BACKUP_ROOT}/${ts}" step "备份当前版本 -> ${bk}" mkdir -p "${bk}" cp -a "${APP_DIR}/${BIN_NAME}" "${bk}/" 2>/dev/null || true cp -a "${APP_DIR}/config.ini" "${bk}/" 2>/dev/null || true cp -a "${APP_DIR}/config.txt" "${bk}/" 2>/dev/null || true [ -d "${APP_DIR}/assets" ] && cp -a "${APP_DIR}/assets" "${bk}/" 2>/dev/null || true [ -d "${APP_DIR}/hyperlpr3" ] && cp -a "${APP_DIR}/hyperlpr3" "${bk}/" 2>/dev/null || true # 记录最新备份路径 echo "${bk}" > "${BACKUP_ROOT}/.latest" # 只保留最近 5 个备份 ls -1dt "${BACKUP_ROOT}"/20* 2>/dev/null | tail -n +6 | xargs -r rm -rf info "备份完成(保留最近 5 个版本)" } do_deploy() { do_build is_root || die "部署需要 root(写入 ${APP_DIR} / 安装 systemd 服务),请用: sudo bash $0 deploy" ensure_dirs backup_current step "复制程序文件..." install -m 0755 "${BUILD_DIR}/${BIN_NAME}" "${APP_DIR}/${BIN_NAME}" # Web 前端资源(全量同步,保证 help.html 等更新生效) if [ -d "${SRC_DIR}/assets/web" ]; then cp -a "${SRC_DIR}/assets/web/." "${APP_DIR}/assets/web/" info "Web 资源已更新 ($(ls -1 "${SRC_DIR}/assets/web" | wc -l) 个文件)" fi # HyperLPR3 SDK(模型 + 动态库),缺失时给出警告但不阻断 if [ -d "${SRC_DIR}/hyperlpr3" ]; then cp -an "${SRC_DIR}/hyperlpr3/." "${APP_DIR}/hyperlpr3/" 2>/dev/null || true info "HyperLPR3 SDK 已同步(-n 不覆盖已有模型文件)" else warn "源码树无 hyperlpr3/ 目录,沿用部署目录现有 SDK" fi # 运维脚本 cp -a "${SCRIPT_DIR}/." "${APP_DIR}/scripts/" 2>/dev/null || true # config.ini:首次部署从模板生成;已存在则永不覆盖(保留现场配置) if [ ! -f "${APP_DIR}/config.ini" ]; then if [ -f "${SRC_DIR}/config.ini" ]; then cp -a "${SRC_DIR}/config.ini" "${APP_DIR}/config.ini" warn "已生成初始 config.ini,请按现场环境修改(RTSP/上报地址/飞书等)后重启" else warn "源码包中无 config.ini 模板,请手动放置到 ${APP_DIR}/config.ini" fi else info "config.ini 已存在,保留现场配置不覆盖" fi # 动态库搜索路径:hyperlpr3/lib 加入 rpath 链接缓存(部署目录内) if [ -d "${APP_DIR}/hyperlpr3/lib" ]; then echo "${APP_DIR}/hyperlpr3/lib" > /etc/ld.so.conf.d/platerecapp.conf ldconfig info "已注册动态库路径: ${APP_DIR}/hyperlpr3/lib" fi # 目录属主 if id "$RUN_USER" &>/dev/null; then chown -R "${RUN_USER}:${RUN_USER}" "${APP_DIR}" info "目录属主已设置为 ${RUN_USER}" else warn "用户 ${RUN_USER} 不存在,请先创建: sudo useradd -r -s /bin/bash ${RUN_USER}" fi install_service do_restart sleep 3 do_status info "部署完成 ✅ 访问: https://<设备IP>:${HEALTH_PORT}/" } # ============================ systemd 服务 ============================ install_service() { step "安装 systemd 服务: ${SERVICE_FILE}" cat > "${SERVICE_FILE}" </dev/null || true info "服务已安装并设置开机自启" } do_start() { step "启动 ${SVC_NAME}"; systemctl start "${SVC_NAME}"; sleep 2; do_status; } do_stop() { step "停止 ${SVC_NAME}"; systemctl stop "${SVC_NAME}"; info "已停止"; } do_restart() { step "重启 ${SVC_NAME}"; systemctl restart "${SVC_NAME}"; sleep 2; info "已重启"; } do_status() { step "服务状态" systemctl status "${SVC_NAME}" --no-pager -l 2>/dev/null || warn "服务未运行或未安装" echo step "端口监听" ss -tlnp 2>/dev/null | grep -E ":${HEALTH_PORT}|:${HTTP_REDIRECT_PORT}" || warn "未检测到 ${HEALTH_PORT}/${HTTP_REDIRECT_PORT} 端口监听(服务可能仍在初始化,启动有 10s sleep)" echo step "进程" pgrep -af "${BIN_NAME}" | grep -v grep || warn "无 ${BIN_NAME} 进程" echo step "磁盘占用 (${APP_DIR})" if [ -d "${APP_DIR}" ]; then du -sh "${APP_DIR}/PlateJPG" 2>/dev/null || true du -sh "${APP_DIR}" 2>/dev/null || true fi } do_logs() { step "跟踪日志: ${LOG_FILE} (Ctrl+C 退出)"; touch "${LOG_FILE}" 2>/dev/null; tail -n 100 -f "${LOG_FILE}"; } do_journal() { step "journalctl -u ${SVC_NAME} (Ctrl+C 退出)"; journalctl -u "${SVC_NAME}" -f --no-pager; } # ============================ 备份/回滚 ============================ do_backup() { is_root || die "备份需要 root" ensure_dirs backup_current } do_rollback() { is_root || die "回滚需要 root" local bk bk="$(cat "${BACKUP_ROOT}/.latest" 2>/dev/null || true)" [ -n "$bk" ] && [ -d "$bk" ] || die "没有可用备份(${BACKUP_ROOT})" step "回滚到: ${bk}" systemctl stop "${SVC_NAME}" 2>/dev/null || true [ -f "${bk}/${BIN_NAME}" ] && cp -a "${bk}/${BIN_NAME}" "${APP_DIR}/${BIN_NAME}" [ -f "${bk}/config.ini" ] && cp -a "${bk}/config.ini" "${APP_DIR}/config.ini" [ -f "${bk}/config.txt" ] && cp -a "${bk}/config.txt" "${APP_DIR}/config.txt" [ -d "${bk}/assets" ] && rm -rf "${APP_DIR}/assets" && cp -a "${bk}/assets" "${APP_DIR}/assets" [ -d "${bk}/hyperlpr3" ] && rm -rf "${APP_DIR}/hyperlpr3" && cp -a "${bk}/hyperlpr3" "${APP_DIR}/hyperlpr3" chown -R "${RUN_USER}:${RUN_USER}" "${APP_DIR}" 2>/dev/null || true # 回滚后移除 .latest 指向,防止重复回滚同版本 rm -f "${BACKUP_ROOT}/.latest" systemctl start "${SVC_NAME}" sleep 3 do_status info "回滚完成" } # ============================ sudoers ============================ do_sudoers() { is_root || die "配置 sudoers 需要 root: sudo bash $0 sudoers" step "配置 frpc 管理 sudoers 权限" bash "${SCRIPT_DIR}/setup_frpc_sudoers.sh" "${RUN_USER}" } # ============================ 入口 ============================ usage() { sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//' exit 0 } case "${1:-deploy}" in deploy) do_deploy ;; build) do_build ;; start) do_start ;; stop) do_stop ;; restart) do_restart; do_status ;; status) do_status ;; logs) do_logs ;; journal) do_journal ;; backup) do_backup ;; rollback) do_rollback ;; deps) deps_install ;; sudoers) do_sudoers ;; -h|--help|help) usage ;; *) die "未知命令: $1(运行 bash $0 help 查看用法)" ;; esac