349 lines
9.1 KiB
Bash
Executable File
349 lines
9.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
# SHZ 后端测试环境部署脚本
|
||
#
|
||
# 默认部署方式:
|
||
# ./buildAndStart-test.sh
|
||
#
|
||
# 常用命令:
|
||
# ./buildAndStart-test.sh status
|
||
# ./buildAndStart-test.sh stop
|
||
# ./buildAndStart-test.sh logs
|
||
#
|
||
# deploy/start 成功后会自动跟踪后端日志;按 Ctrl+C 退出日志跟踪,不会停止后端进程。
|
||
#
|
||
# 可通过环境变量覆盖默认配置,例如:
|
||
# 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-backend;BASE_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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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() {
|
||
local pid second log_tail_pid
|
||
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")"
|
||
# 启动阶段也实时输出新增日志,避免健康检查等待期间看起来像脚本卡住。
|
||
tail -n 0 -F "$LOG_FILE" &
|
||
log_tail_pid="$!"
|
||
trap 'stop_log_tail "$log_tail_pid"; exit 130' INT TERM
|
||
__green "实时启动日志(健康检查通过后继续跟踪)"
|
||
|
||
for second in $(seq 1 "$STARTUP_TIMEOUT"); do
|
||
if ! pid_is_running "$pid"; then
|
||
stop_log_tail "$log_tail_pid"
|
||
trap - INT TERM
|
||
__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
|
||
stop_log_tail "$log_tail_pid"
|
||
trap - INT TERM
|
||
__green "应用启动成功: PID=$pid,健康检查通过($HEALTH_URL)"
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
stop_log_tail "$log_tail_pid"
|
||
trap - INT TERM
|
||
__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"
|
||
|
||
# 日志跟踪是长期运行操作,不应继续持有部署锁,否则下一次部署会被误判为重复任务。
|
||
flock -u 9 || true
|
||
exec 9>&-
|
||
logs
|
||
}
|
||
|
||
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 "$@"
|