Files
shz-backend/buildAndStart-test.sh

349 lines
9.1 KiB
Bash
Raw Normal View History

2026-07-15 14:25:02 +08:00
#!/usr/bin/env bash
# SHZ 后端测试环境部署脚本
#
# 默认部署方式:
# ./buildAndStart-test.sh
#
# 常用命令:
# ./buildAndStart-test.sh status
# ./buildAndStart-test.sh stop
# ./buildAndStart-test.sh logs
#
2026-07-15 15:14:25 +08:00
# deploy/start 成功后会自动跟踪后端日志;按 Ctrl+C 退出日志跟踪,不会停止后端进程。
#
2026-07-15 14:25:02 +08:00
# 可通过环境变量覆盖默认配置,例如:
# PROFILE=test MAVEN_BIN=/opt/apache-maven-3.9.9/bin/mvn ./buildAndStart-test.sh
set -Eeuo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
# 目录和应用配置。默认使用脚本所在目录,适配当前服务器的
# /data/code/shz-backendBASE_PATH 仍可用于部署到其他目录。
BASE_PATH="${BASE_PATH:-$SCRIPT_DIR}"
JAR_PATH="${JAR_PATH:-$BASE_PATH/ruoyi-admin/target/ruoyi-admin.jar}"
LOG_PATH="${LOG_PATH:-$BASE_PATH/logs}"
LOG_FILE="${LOG_FILE:-$LOG_PATH/backend.log}"
PID_FILE="${PID_FILE:-$LOG_PATH/backend.pid}"
LOCK_FILE="${LOCK_FILE:-$LOG_PATH/deploy.lock}"
PROFILE="${PROFILE:-test}"
GIT_REMOTE="${GIT_REMOTE:-origin}"
GIT_BRANCH="${GIT_BRANCH:-main}"
UPDATE_CODE="${UPDATE_CODE:-true}"
MAVEN_THREADS="${MAVEN_THREADS:-4}"
STARTUP_TIMEOUT="${STARTUP_TIMEOUT:-120}"
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:9091/}"
# 测试 profile 默认使用这些本机目录。目录不存在时由脚本创建。
RUOYI_PROFILE_DIR="${RUOYI_PROFILE_DIR:-/home/ruoyi/uploadPath}"
FILE_UPLOAD_DIR="${FILE_UPLOAD_DIR:-/opt/service/file/data/file}"
JVM_MEMORY="${JVM_MEMORY:--Xms2048M -Xmx2048M -XX:MaxDirectMemorySize=2048M}"
JVM_DEBUG="${JVM_DEBUG:-}"
JAVA_BIN="${JAVA_BIN:-}"
MAVEN_BIN="${MAVEN_BIN:-}"
__green() {
printf '\033[1;32m%s\033[0m\n' "$1"
}
__red() {
printf '\033[1;31m%s\033[0m\n' "$1" >&2
}
__yellow() {
printf '\033[1;33m%s\033[0m\n' "$1"
}
is_true() {
case "${1:-}" in
1|true|TRUE|yes|YES|y|Y) return 0 ;;
*) return 1 ;;
esac
}
fail() {
__red "$1"
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "缺少必要命令: $1"
}
resolve_java() {
if [[ -n "$JAVA_BIN" ]]; then
[[ -x "$JAVA_BIN" ]] || fail "JAVA_BIN 不可执行: $JAVA_BIN"
return
fi
if command -v java >/dev/null 2>&1; then
JAVA_BIN="$(command -v java)"
elif [[ -n "${JAVA_HOME:-}" && -x "$JAVA_HOME/bin/java" ]]; then
JAVA_BIN="$JAVA_HOME/bin/java"
else
fail "未找到 Java。请安装 JDK 8+,或设置 JAVA_BIN/JAVA_HOME。"
fi
}
resolve_maven() {
if [[ -n "$MAVEN_BIN" ]]; then
[[ -x "$MAVEN_BIN" ]] || fail "MAVEN_BIN 不可执行: $MAVEN_BIN"
return
fi
if command -v mvn >/dev/null 2>&1; then
MAVEN_BIN="$(command -v mvn)"
return
fi
# 兼容服务器上未加入 PATH 的 Maven 安装。
local candidate
for candidate in \
"$BASE_PATH/mvnw" \
"${MAVEN_HOME:-}/bin/mvn" \
/usr/local/apache-maven/bin/mvn \
/opt/apache-maven/bin/mvn \
/opt/apache-maven-3.9.9/bin/mvn \
/root/apache-maven/bin/mvn \
/root/apache-maven-3.9.9/bin/mvn; do
if [[ -n "$candidate" && -x "$candidate" ]]; then
MAVEN_BIN="$candidate"
return
fi
done
fail "未找到 Maven。请先安装 Maven 3.9+,或执行时设置 MAVEN_BIN=/实际路径/bin/mvn。"
}
prepare_environment() {
[[ -d "$BASE_PATH" ]] || fail "项目目录不存在: $BASE_PATH"
[[ -f "$BASE_PATH/pom.xml" ]] || fail "不是有效的 Maven 项目目录(缺少 pom.xml: $BASE_PATH"
[[ -f "$BASE_PATH/ruoyi-admin/src/main/resources/application-${PROFILE}.yml" ]] \
|| fail "未找到 profile 配置文件: $BASE_PATH/ruoyi-admin/src/main/resources/application-${PROFILE}.yml"
mkdir -p "$LOG_PATH" "$RUOYI_PROFILE_DIR" "$FILE_UPLOAD_DIR"
}
find_app_pids() {
local pid main_class pid_from_file cmdline
declare -A seen=()
# 优先读取本脚本记录的 PID并确认该 PID 仍然是目标 JAR。
if [[ -s "$PID_FILE" ]]; then
pid_from_file="$(<"$PID_FILE")"
if [[ "$pid_from_file" =~ ^[0-9]+$ && -r "/proc/$pid_from_file/cmdline" ]]; then
cmdline="$(tr '\0' ' ' < "/proc/$pid_from_file/cmdline")"
if [[ "$cmdline" == *"-jar $JAR_PATH"* ]]; then
printf '%s\n' "$pid_from_file"
seen["$pid_from_file"]=1
fi
fi
fi
# 兼容脚本升级前启动的实例。
if command -v jps >/dev/null 2>&1; then
while read -r pid main_class; do
if [[ "$main_class" == "$JAR_PATH" && -z "${seen[$pid]+x}" ]]; then
printf '%s\n' "$pid"
seen["$pid"]=1
fi
done < <(jps -l 2>/dev/null || true)
fi
}
pid_is_running() {
local pid="$1"
[[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null
}
2026-07-15 15:17:50 +08:00
stop_log_tail() {
local tail_pid="${1:-}"
[[ "$tail_pid" =~ ^[0-9]+$ ]] || return 0
kill "$tail_pid" 2>/dev/null || true
wait "$tail_pid" 2>/dev/null || true
}
2026-07-15 14:25:02 +08:00
stop_app() {
local pid second
local -a pids=()
mapfile -t pids < <(find_app_pids)
if [[ "${#pids[@]}" -eq 0 ]]; then
rm -f "$PID_FILE"
__green "未找到运行中的测试环境进程"
return 0
fi
__green "停止进程: ${pids[*]}"
for pid in "${pids[@]}"; do
kill "$pid" 2>/dev/null || true
done
for second in $(seq 1 30); do
local remaining=()
for pid in "${pids[@]}"; do
pid_is_running "$pid" && remaining+=("$pid")
done
if [[ "${#remaining[@]}" -eq 0 ]]; then
rm -f "$PID_FILE"
__green "旧进程已停止"
return 0
fi
sleep 1
done
__yellow "进程未在 30 秒内退出,执行强制终止: ${remaining[*]}"
for pid in "${remaining[@]}"; do
kill -9 "$pid" 2>/dev/null || true
done
rm -f "$PID_FILE"
}
update_code() {
if ! is_true "$UPDATE_CODE"; then
__yellow "跳过 Git 更新UPDATE_CODE=$UPDATE_CODE"
return
fi
[[ -d "$BASE_PATH/.git" ]] || fail "项目不是 Git 仓库,无法更新代码: $BASE_PATH"
__green "更新代码: $GIT_REMOTE/$GIT_BRANCH"
git -C "$BASE_PATH" fetch "$GIT_REMOTE" "$GIT_BRANCH"
git -C "$BASE_PATH" reset --hard "$GIT_REMOTE/$GIT_BRANCH"
}
build_app() {
resolve_maven
__green "开始构建Maven: $MAVEN_BIN"
(
cd "$BASE_PATH"
"$MAVEN_BIN" -B -T "$MAVEN_THREADS" clean install -DskipTests
) || fail "Maven 构建失败"
[[ -s "$JAR_PATH" ]] || fail "构建完成但 JAR 文件不存在: $JAR_PATH"
__green "构建完成: $JAR_PATH"
}
start_app() {
2026-07-15 15:17:50 +08:00
local pid second log_tail_pid
2026-07-15 14:25:02 +08:00
local -a jvm_options=()
read -r -a jvm_options <<< "$JVM_MEMORY $JVM_DEBUG"
stop_app
: > "$LOG_FILE"
__green "启动测试环境profile=$PROFILE"
(
cd "$BASE_PATH"
nohup "$JAVA_BIN" "${jvm_options[@]}" -jar "$JAR_PATH" \
"--spring.profiles.active=$PROFILE" >> "$LOG_FILE" 2>&1 < /dev/null &
echo "$!" > "$PID_FILE"
)
pid="$(<"$PID_FILE")"
2026-07-15 15:17:50 +08:00
# 启动阶段也实时输出新增日志,避免健康检查等待期间看起来像脚本卡住。
tail -n 0 -F "$LOG_FILE" &
log_tail_pid="$!"
trap 'stop_log_tail "$log_tail_pid"; exit 130' INT TERM
__green "实时启动日志(健康检查通过后继续跟踪)"
2026-07-15 14:25:02 +08:00
for second in $(seq 1 "$STARTUP_TIMEOUT"); do
if ! pid_is_running "$pid"; then
2026-07-15 15:17:50 +08:00
stop_log_tail "$log_tail_pid"
trap - INT TERM
2026-07-15 14:25:02 +08:00
__red "应用启动失败,进程已退出。最近日志:"
tail -n 80 "$LOG_FILE" >&2 || true
rm -f "$PID_FILE"
return 1
fi
if command -v curl >/dev/null 2>&1 && curl -fsS --max-time 2 "$HEALTH_URL" >/dev/null 2>&1; then
2026-07-15 15:17:50 +08:00
stop_log_tail "$log_tail_pid"
trap - INT TERM
2026-07-15 14:25:02 +08:00
__green "应用启动成功: PID=$pid,健康检查通过($HEALTH_URL"
return 0
fi
sleep 1
done
2026-07-15 15:17:50 +08:00
stop_log_tail "$log_tail_pid"
trap - INT TERM
2026-07-15 14:25:02 +08:00
__red "应用进程仍在运行,但健康检查超时(${STARTUP_TIMEOUT}s: $HEALTH_URL"
__red "最近日志:"
tail -n 80 "$LOG_FILE" >&2 || true
return 1
}
deploy() {
prepare_environment
exec 9>"$LOCK_FILE"
flock -n 9 || fail "已有另一个部署任务正在执行: $LOCK_FILE"
update_code
build_app
start_app
__green "部署完成。日志文件: $LOG_FILE"
2026-07-15 15:20:33 +08:00
# 日志跟踪是长期运行操作,不应继续持有部署锁,否则下一次部署会被误判为重复任务。
flock -u 9 || true
exec 9>&-
2026-07-15 15:14:25 +08:00
logs
2026-07-15 14:25:02 +08:00
}
status() {
prepare_environment
local -a pids=()
mapfile -t pids < <(find_app_pids)
if [[ "${#pids[@]}" -eq 0 ]]; then
__yellow "测试环境未运行"
return 1
fi
__green "测试环境运行中: PID=${pids[*]}"
if command -v curl >/dev/null 2>&1 && curl -fsS --max-time 3 "$HEALTH_URL" >/dev/null 2>&1; then
__green "健康检查通过: $HEALTH_URL"
else
__yellow "健康检查未通过: $HEALTH_URL"
fi
}
logs() {
prepare_environment
touch "$LOG_FILE"
tail -f "$LOG_FILE"
}
main() {
# 非交互式部署也加载系统 PATH不存在时不影响运行。
# 某些服务器的 /etc/profile 会引用未定义变量,不能在 nounset 下直接 source。
if [[ -r /etc/profile ]]; then
set +u
source /etc/profile || true
set -u
fi
resolve_java
case "${1:-deploy}" in
deploy|start)
deploy
;;
stop)
prepare_environment
stop_app
;;
status)
status
;;
logs|log)
logs
;;
*)
__red "用法: $0 [deploy|start|stop|status|logs]"
exit 2
;;
esac
}
main "$@"