Files
ccdi/deploy/ccdi_function.sh
2026-05-08 13:32:07 +08:00

505 lines
13 KiB
Bash
Executable File
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/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
# ==================== 生产配置区:按服务器实际路径修改 ====================
# JDK 安装目录。留空时使用服务器已有 JAVA_HOME仍为空时使用 PATH 中的 java。
BACKEND_JAVA_HOME=""
# 后端 Jar 所在目录。生产目录结构为脚本在外层Jar 位于 backend/ruoyi-admin.jar。
APP_HOME="${SCRIPT_DIR}/backend"
# 后端 Jar 文件名。
JAR_NAME="ruoyi-admin.jar"
# Spring Profile。
SPRING_PROFILES_ACTIVE="uat"
# JVM 参数。
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
# 额外应用启动参数,例如:--server.port=8080
APP_ARGS=""
# 停止进程等待秒数。
STOP_WAIT_SECONDS=30
# ==================== 以下为脚本逻辑,一般不需要修改 ====================
if [ "${APP_HOME#/}" = "${APP_HOME}" ]; then
APP_HOME="${SCRIPT_DIR}/${APP_HOME}"
fi
APP_HOME="${APP_HOME%/}"
JAR_PATH="${APP_HOME}/${JAR_NAME}"
FRONTEND_DIR="${SCRIPT_DIR}/frontend"
LOG_DIR="${APP_HOME}/logs"
CONSOLE_LOG="${LOG_DIR}/backend-console.log"
PID_FILE="${LOG_DIR}/backend-java.pid"
APP_MARKER="-Dccdi.backend.prod.home=${APP_HOME}"
JAVA_CMD="java"
RELATIVE_JAR_PATH=""
TIMESTAMP=$(date '+%Y%m%d%H%M%S')
BACKUP_ROOT="${SCRIPT_DIR}/backups"
BACKUP_DIR="${BACKUP_ROOT}/${TIMESTAMP}"
WORK_ROOT="${SCRIPT_DIR}/.deploy-work"
WORK_DIR="${WORK_ROOT}/release-${TIMESTAMP}"
RELEASE_ZIP=""
RELEASE_JAR=""
RELEASE_DIST_ZIP=""
FRONTEND_SOURCE_DIR=""
case "${APP_HOME}" in
"${SCRIPT_DIR}"/*)
RELATIVE_JAR_PATH="${APP_HOME#${SCRIPT_DIR}/}/${JAR_NAME}"
;;
esac
timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
log_info() {
printf '[%s] %s\n' "$(timestamp)" "$1"
}
log_error() {
printf '[%s] %s\n' "$(timestamp)" "$1" >&2
}
usage() {
cat <<'EOF'
用法: ./ccdi_function.sh [deploy|restart|stop] [上线压缩包路径]
命令:
deploy 备份 backend/ 与 frontend/,部署上线包,然后重启后端并持续输出日志
restart 停止旧后端进程,启动 backend/ruoyi-admin.jar并持续输出日志
stop 停止当前脚本目录对应的 backend/ruoyi-admin.jar 进程
上线包要求:
未传上线压缩包路径时,脚本会自动使用当前脚本目录下唯一的 .zip 文件。
上线压缩包根层必须包含 ruoyi-admin.jar 和 dist.zip。
dist.zip 解压后必须包含 dist/index.html。
生产目录示例:
ccdi_function.sh
backend/
frontend/
ccdi_YYYYMMDD.zip
说明:
restart 和 deploy 启动成功后会持续输出 backend/logs/backend-console.log。
按 Ctrl+C 仅退出日志查看,不会停止已启动的后端进程。
EOF
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少命令: $1"
exit 1
fi
}
resolve_path() {
input_path="$1"
case "${input_path}" in
/*)
printf '%s\n' "${input_path}"
;;
*)
input_dir=$(dirname "${input_path}")
input_base=$(basename "${input_path}")
printf '%s/%s\n' "$(CDPATH= cd -- "${input_dir}" && pwd)" "${input_base}"
;;
esac
}
resolve_java_cmd() {
configured_java_home="${BACKEND_JAVA_HOME}"
if [ -z "${configured_java_home}" ]; then
configured_java_home="${JAVA_HOME:-}"
fi
if [ -n "${configured_java_home}" ]; then
configured_java_home="${configured_java_home%/}"
if [ ! -x "${configured_java_home}/bin/java" ]; then
log_error "配置的 JAVA_HOME 无效,未找到可执行文件: ${configured_java_home}/bin/java"
exit 1
fi
JAVA_HOME="${configured_java_home}"
export JAVA_HOME
JAVA_CMD="${JAVA_HOME}/bin/java"
else
require_command "java"
JAVA_CMD="java"
fi
log_info "使用 Java 命令: ${JAVA_CMD}"
}
get_process_table() {
if ! ps -ef 2>/dev/null; then
log_error "执行 ps -ef 失败,无法扫描旧进程"
return 1
fi
}
is_backend_process_line() {
process_line="$1"
case "${process_line}" in
*"<defunct>"*)
return 1
;;
*" -jar ${JAR_PATH}"*)
return 0
;;
*"${APP_MARKER}"*)
return 0
;;
esac
if [ -n "${RELATIVE_JAR_PATH}" ]; then
case "${process_line}" in
*" -jar ${RELATIVE_JAR_PATH}"*)
return 0
;;
esac
fi
return 1
}
is_managed_pid() {
check_pid="$1"
if [ -z "${check_pid}" ] || ! kill -0 "${check_pid}" 2>/dev/null; then
return 1
fi
if ! process_table=$(get_process_table); then
return 1
fi
while IFS= read -r process_line; do
set -- ${process_line}
line_pid="${2:-}"
if [ "${line_pid}" = "${check_pid}" ] && is_backend_process_line "${process_line}"; then
return 0
fi
done <<EOF
${process_table}
EOF
return 1
}
collect_pids() {
all_pids=""
unique_pids=""
if ! process_table=$(get_process_table); then
return 1
fi
if [ -f "${PID_FILE}" ]; then
pid_file_value=$(sed -n '1p' "${PID_FILE}" 2>/dev/null || true)
if is_managed_pid "${pid_file_value}"; then
all_pids="${all_pids} ${pid_file_value}"
fi
fi
while IFS= read -r process_line; do
set -- ${process_line}
scan_pid="${2:-}"
case "${scan_pid}" in
''|*[!0-9]*)
continue
;;
esac
if is_backend_process_line "${process_line}"; then
all_pids="${all_pids} ${scan_pid}"
fi
done <<EOF
${process_table}
EOF
for scan_pid in ${all_pids}; do
case " ${unique_pids} " in
*" ${scan_pid} "*)
;;
*)
unique_pids="${unique_pids} ${scan_pid}"
;;
esac
done
printf '%s\n' "${unique_pids}" | xargs 2>/dev/null || true
}
start_backend() {
resolve_java_cmd
if [ ! -f "${JAR_PATH}" ]; then
log_error "未找到后端 Jar: ${JAR_PATH}"
exit 1
fi
if ! running_pids=$(collect_pids); then
log_error "扫描后端进程失败"
exit 1
fi
if [ -n "${running_pids}" ]; then
log_error "检测到后端已在运行: ${running_pids}"
exit 1
fi
mkdir -p "${LOG_DIR}"
printf '\n===== %s start =====\n' "$(timestamp)" >>"${CONSOLE_LOG}"
profile_arg=""
if [ -n "${SPRING_PROFILES_ACTIVE}" ]; then
profile_arg="--spring.profiles.active=${SPRING_PROFILES_ACTIVE}"
fi
log_info "开始启动后端 Jar: ${JAR_PATH}"
nohup "${JAVA_CMD}" "${APP_MARKER}" ${JAVA_OPTS} -jar "${JAR_PATH}" ${profile_arg} ${APP_ARGS} >>"${CONSOLE_LOG}" 2>&1 &
echo $! >"${PID_FILE}"
sleep 3
starter_pid=$(sed -n '1p' "${PID_FILE}" 2>/dev/null || true)
if [ -z "${starter_pid}" ] || ! kill -0 "${starter_pid}" 2>/dev/null; then
log_error "启动命令未保持运行,请检查日志: ${CONSOLE_LOG}"
exit 1
fi
log_info "后端启动完成PID: ${starter_pid}"
}
stop_backend() {
if ! pids=$(collect_pids); then
log_error "扫描后端进程失败"
exit 1
fi
if [ -z "${pids}" ]; then
log_info "未发现运行中的后端进程"
rm -f "${PID_FILE}"
return 0
fi
log_info "准备停止后端进程: ${pids}"
for pid in ${pids}; do
kill -TERM "${pid}" 2>/dev/null || true
done
elapsed=0
remaining_pids="${pids}"
while [ -n "${remaining_pids}" ] && [ "${elapsed}" -lt "${STOP_WAIT_SECONDS}" ]; do
sleep 1
elapsed=$((elapsed + 1))
remaining_pids=""
for pid in ${pids}; do
if kill -0 "${pid}" 2>/dev/null; then
remaining_pids="${remaining_pids} ${pid}"
fi
done
remaining_pids=$(printf '%s\n' "${remaining_pids}" | xargs 2>/dev/null || true)
done
if [ -n "${remaining_pids}" ]; then
log_info "仍有进程未退出,执行强制停止: ${remaining_pids}"
for pid in ${remaining_pids}; do
kill -KILL "${pid}" 2>/dev/null || true
done
fi
rm -f "${PID_FILE}"
log_info "后端停止完成"
}
follow_logs() {
require_command "tail"
mkdir -p "${LOG_DIR}"
touch "${CONSOLE_LOG}"
log_info "持续输出日志中,按 Ctrl+C 仅退出日志查看,不会停止后端进程"
tail -n 200 -F "${CONSOLE_LOG}"
}
restart_action() {
stop_backend
start_backend
follow_logs
}
resolve_release_zip() {
if [ -n "${RELEASE_ZIP}" ]; then
RELEASE_ZIP=$(resolve_path "${RELEASE_ZIP}")
else
mkdir -p "${WORK_ROOT}"
candidate_file="${WORK_ROOT}/zip-candidates-${TIMESTAMP}.txt"
find "${SCRIPT_DIR}" -maxdepth 1 -type f -name '*.zip' ! -name 'dist.zip' | sort >"${candidate_file}"
candidate_count=$(wc -l <"${candidate_file}" | tr -d ' ')
if [ "${candidate_count}" -eq 0 ]; then
log_error "未在脚本目录找到上线压缩包,请传入压缩包路径"
exit 1
fi
if [ "${candidate_count}" -gt 1 ]; then
log_error "脚本目录存在多个上线压缩包,请显式传入压缩包路径"
cat "${candidate_file}" >&2
exit 1
fi
RELEASE_ZIP=$(sed -n '1p' "${candidate_file}")
rm -f "${candidate_file}"
fi
if [ ! -f "${RELEASE_ZIP}" ]; then
log_error "上线压缩包不存在: ${RELEASE_ZIP}"
exit 1
fi
}
assert_deploy_layout() {
if [ ! -d "${APP_HOME}" ]; then
log_error "未找到后端目录: ${APP_HOME}"
exit 1
fi
if [ ! -d "${FRONTEND_DIR}" ]; then
log_error "未找到前端目录: ${FRONTEND_DIR}"
exit 1
fi
}
backup_dir() {
source_dir="$1"
target_dir="$2"
mkdir -p "${target_dir}"
if [ -n "$(find "${source_dir}" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
cp -a "${source_dir}/." "${target_dir}/"
log_info "已备份 ${source_dir}${target_dir}"
else
log_info "目录为空,已创建空备份目录: ${target_dir}"
fi
}
backup_current_files() {
mkdir -p "${BACKUP_DIR}"
backup_dir "${APP_HOME}" "${BACKUP_DIR}/backend"
backup_dir "${FRONTEND_DIR}" "${BACKUP_DIR}/frontend"
}
extract_release_package() {
mkdir -p "${WORK_DIR}/release" "${WORK_DIR}/frontend"
log_info "开始解压上线压缩包: ${RELEASE_ZIP}"
unzip -q "${RELEASE_ZIP}" -d "${WORK_DIR}/release"
RELEASE_JAR="${WORK_DIR}/release/${JAR_NAME}"
RELEASE_DIST_ZIP="${WORK_DIR}/release/dist.zip"
if [ ! -f "${RELEASE_JAR}" ]; then
log_error "上线压缩包根层缺少 ${JAR_NAME}"
exit 1
fi
if [ ! -f "${RELEASE_DIST_ZIP}" ]; then
log_error "上线压缩包根层缺少 dist.zip"
exit 1
fi
unzip -q "${RELEASE_DIST_ZIP}" -d "${WORK_DIR}/frontend"
FRONTEND_SOURCE_DIR="${WORK_DIR}/frontend/dist"
if [ ! -f "${FRONTEND_SOURCE_DIR}/index.html" ]; then
log_error "dist.zip 解压后未找到 dist/index.html"
exit 1
fi
}
deploy_backend() {
target_jar="${APP_HOME}/${JAR_NAME}"
deploying_jar="${APP_HOME}/.${JAR_NAME}.deploying"
cp "${RELEASE_JAR}" "${deploying_jar}"
mv "${deploying_jar}" "${target_jar}"
log_info "后端 Jar 已部署: ${target_jar}"
}
deploy_frontend() {
find "${FRONTEND_DIR}" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
cp -a "${FRONTEND_SOURCE_DIR}/." "${FRONTEND_DIR}/"
log_info "前端文件已部署到: ${FRONTEND_DIR}"
}
cleanup_work_dir() {
rm -rf "${WORK_DIR}"
}
deploy_action() {
RELEASE_ZIP="${1:-}"
require_command "find"
require_command "unzip"
resolve_release_zip
assert_deploy_layout
backup_current_files
trap cleanup_work_dir 0
extract_release_package
deploy_backend
deploy_frontend
cleanup_work_dir
trap - 0
log_info "部署完成,备份目录: ${BACKUP_DIR}"
log_info "开始重启后端并输出日志"
restart_action
}
main() {
action="${1:-help}"
case "${action}" in
deploy)
shift
if [ "$#" -gt 1 ]; then
usage
exit 1
fi
deploy_action "${1:-}"
;;
restart)
shift
if [ "$#" -ne 0 ]; then
usage
exit 1
fi
restart_action
;;
stop)
shift
if [ "$#" -ne 0 ]; then
usage
exit 1
fi
stop_backend
;;
-h|--help|help)
usage
;;
*)
usage
exit 1
;;
esac
}
main "$@"