Files
ERP/run.command

345 lines
9.9 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/bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
ROOT_DIR="${ERP_RUN_ROOT_DIR:-$SCRIPT_DIR}"
BACKEND_DIR="${ERP_RUN_BACKEND_DIR:-$ROOT_DIR/oa-backend}"
ERP_ENV_FILE="${ERP_RUN_CONFIG_FILE:-${ERP_CONFIG_FILE:-}}"
if [ -z "$ERP_ENV_FILE" ] && [ -n "${ERP_INSTALL_ROOT:-}" ]; then
ERP_ENV_FILE="$ERP_INSTALL_ROOT/config/erp.env"
fi
if [ -n "$ERP_ENV_FILE" ] && [ -r "$ERP_ENV_FILE" ]; then
set -a
# The production installer writes shell-escaped values to this file.
# shellcheck disable=SC1090
source "$ERP_ENV_FILE"
set +a
fi
JAVA_BIN="${ERP_RUN_JAVA_BIN:-${ERP_JAVA_BIN:-}}"
JAR_PATH="${ERP_RUN_JAR_PATH:-}"
BACKEND_PORT="${ERP_RUN_BACKEND_PORT:-${SERVER_PORT:-8091}}"
SPRING_PROFILE="${ERP_RUN_PROFILE:-${SPRING_PROFILES_ACTIVE:-}}"
NGROK_API_PORT="${ERP_RUN_NGROK_API_PORT:-4040}"
LOCAL_URL="http://127.0.0.1:$BACKEND_PORT"
PUBLIC_URL="${ERP_RUN_PUBLIC_URL:-https://resonant-elated-launder.ngrok-free.dev}"
NGROK_TARGET="http://localhost:$BACKEND_PORT"
LOG_DIR="${ERP_RUN_LOG_DIR:-${TMPDIR:-/tmp}/kaidi-erp-run}"
BACKEND_LOG="$LOG_DIR/backend.log"
NGROK_LOG="$LOG_DIR/ngrok.log"
NGROK_RUNTIME_CONFIG="$LOG_DIR/ngrok-runtime.yml"
NGROK_BIN=""
NGROK_BASE_CONFIG=""
BACKEND_PID=""
NGROK_PID=""
CLEANED_UP=0
say() {
printf '[ERP] %s\n' "$*"
}
fail() {
printf '[ERP] ERROR: %s\n' "$*" >&2
return 1
}
port_listener_pid() {
lsof -nP -iTCP:"$1" -sTCP:LISTEN -t 2>/dev/null | head -n 1
}
backend_is_healthy() {
local body
body="$(curl -fsS --connect-timeout 1 --max-time 3 "$LOCAL_URL/" 2>/dev/null)" || return 1
[[ "$body" == *'<title>凯迪协同办公平台</title>'* ]]
}
login_is_healthy() {
curl -fsS --connect-timeout 1 --max-time 5 \
-H 'Content-Type: application/json' \
-d '{"loginName":"admin","password":"123456"}' \
"$LOCAL_URL/api/oa/auth/login" 2>/dev/null \
| python3 -c 'import json,sys; d=json.load(sys.stdin); raise SystemExit(0 if d.get("code")==0 and d.get("data",{}).get("token") else 1)'
}
start_backend() {
: > "$BACKEND_LOG"
(
cd "$BACKEND_DIR" || exit 1
IFS=$' \t' read -r -a java_opts <<< "${ERP_RUN_JAVA_OPTS:--Xms512m -Xmx2g}"
if [ -n "$SPRING_PROFILE" ]; then
exec "$JAVA_BIN" "${java_opts[@]}" -jar "$JAR_PATH" \
--spring.profiles.active="$SPRING_PROFILE" --server.port="$BACKEND_PORT"
fi
exec "$JAVA_BIN" "${java_opts[@]}" -jar "$JAR_PATH" --server.port="$BACKEND_PORT"
) >> "$BACKEND_LOG" 2>&1 &
BACKEND_PID=$!
}
wait_for_backend() {
local deadline=$((SECONDS + 60))
while [ "$SECONDS" -lt "$deadline" ]; do
if backend_is_healthy && login_is_healthy; then
return 0
fi
if [ -n "$BACKEND_PID" ] && ! kill -0 "$BACKEND_PID" 2>/dev/null; then
return 1
fi
sleep 1
done
return 1
}
ensure_backend() {
local listener
listener="$(port_listener_pid "$BACKEND_PORT" || true)"
if [ -n "$listener" ]; then
if ! backend_is_healthy; then
fail "端口 $BACKEND_PORT 已被非本项目服务占用(PID ${listener}"
return 1
fi
say "复用已运行的 ERP 服务(PID ${listener}"
return 0
fi
say '启动 ERP 服务...'
start_backend || return 1
if ! wait_for_backend; then
tail -n 40 "$BACKEND_LOG" >&2 || true
fail 'ERP 服务未在 60 秒内就绪'
return 1
fi
}
stop_owned_process() {
local pid="$1"
[ -n "$pid" ] || return 0
kill -0 "$pid" 2>/dev/null || return 0
kill "$pid" 2>/dev/null || true
local attempt=0
while kill -0 "$pid" 2>/dev/null && [ "$attempt" -lt 5 ]; do
sleep 1
attempt=$((attempt + 1))
done
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
fi
wait "$pid" 2>/dev/null || true
}
cleanup() {
[ "$CLEANED_UP" -eq 0 ] || return 0
CLEANED_UP=1
stop_owned_process "$NGROK_PID"
stop_owned_process "$BACKEND_PID"
}
resolve_ngrok_bin() {
if [ -n "${ERP_RUN_NGROK_BIN:-}" ]; then
printf '%s\n' "$ERP_RUN_NGROK_BIN"
return 0
fi
if [ -x "$HOME/bin/ngrok" ]; then
printf '%s\n' "$HOME/bin/ngrok"
return 0
fi
command -v ngrok 2>/dev/null || return 1
}
resolve_ngrok_config() {
if [ -n "${ERP_RUN_NGROK_CONFIG:-}" ]; then
printf '%s\n' "$ERP_RUN_NGROK_CONFIG"
return 0
fi
local output
output="$("$NGROK_BIN" config check 2>/dev/null)" || return 1
output="${output#* at }"
[ -r "$output" ] || return 1
printf '%s\n' "$output"
}
resolve_java_bin() {
if [ -n "$JAVA_BIN" ]; then
printf '%s\n' "$JAVA_BIN"
return 0
fi
local project_java="$ROOT_DIR/.jdks/jdk-17.0.19+10/Contents/Home/bin/java"
if [ -x "$project_java" ]; then
printf '%s\n' "$project_java"
return 0
fi
command -v java 2>/dev/null || return 1
}
resolve_jar_path() {
if [ -n "$JAR_PATH" ]; then
printf '%s\n' "$JAR_PATH"
return 0
fi
if [ -n "${ERP_INSTALL_ROOT:-}" ] && [ -r "$ERP_INSTALL_ROOT/current/app/kaidi-erp.jar" ]; then
printf '%s\n' "$ERP_INSTALL_ROOT/current/app/kaidi-erp.jar"
return 0
fi
local candidate newest=""
for candidate in "$BACKEND_DIR"/build/libs/oa-backend-*.jar; do
[ -r "$candidate" ] || continue
case "$candidate" in *-plain.jar) continue ;; esac
if [ -z "$newest" ] || [ "$candidate" -nt "$newest" ]; then
newest="$candidate"
fi
done
[ -n "$newest" ] || return 1
printf '%s\n' "$newest"
}
preflight() {
command -v curl >/dev/null || { fail '缺少 curl'; return 1; }
command -v lsof >/dev/null || { fail '缺少 lsof'; return 1; }
command -v python3 >/dev/null || { fail '缺少 python3'; return 1; }
if [ -n "$ERP_ENV_FILE" ] && [ ! -r "$ERP_ENV_FILE" ]; then
fail "找不到运行配置:$ERP_ENV_FILE"
return 1
fi
JAVA_BIN="$(resolve_java_bin)" || { fail '找不到 Java 17+'; return 1; }
JAR_PATH="$(resolve_jar_path)" || { fail '找不到可运行 oa-backend JAR'; return 1; }
local java_major
java_major="$("$JAVA_BIN" -version 2>&1 | awk -F'[".]' '/version/ {print $2; exit}')"
[ -n "$java_major" ] && [ "$java_major" -ge 17 ] 2>/dev/null \
|| { fail '需要 Java 17 或更高版本'; return 1; }
if [ "$SPRING_PROFILE" = 'postgres' ]; then
[ -n "${OA_DB_URL:-}" ] || { fail 'PostgreSQL 模式缺少 OA_DB_URL'; return 1; }
[ -n "${OA_DB_USERNAME:-}" ] || { fail 'PostgreSQL 模式缺少 OA_DB_USERNAME'; return 1; }
fi
NGROK_BIN="$(resolve_ngrok_bin)" || { fail '找不到 ngrok(预期 $HOME/bin/ngrok 或 PATH'; return 1; }
NGROK_BASE_CONFIG="$(resolve_ngrok_config)" || {
fail '找不到有效 ngrok 配置(可设置 ERP_RUN_NGROK_CONFIG'
return 1
}
mkdir -p "$LOG_DIR" || { fail "无法创建日志目录:$LOG_DIR"; return 1; }
}
ngrok_is_healthy() {
local payload
payload="$(curl -fsS --connect-timeout 1 --max-time 3 "http://127.0.0.1:$NGROK_API_PORT/api/tunnels" 2>/dev/null)" || return 1
python3 -c 'import json,sys; d=json.load(sys.stdin); pub,target=sys.argv[1:3]; raise SystemExit(0 if any(t.get("public_url")==pub and t.get("config",{}).get("addr")==target for t in d.get("tunnels",[])) else 1)' \
"$PUBLIC_URL" "$NGROK_TARGET" <<< "$payload"
}
start_ngrok() {
: > "$NGROK_LOG"
printf 'version: "3"\nagent:\n web_addr: "127.0.0.1:%s"\n' \
"$NGROK_API_PORT" > "$NGROK_RUNTIME_CONFIG"
"$NGROK_BIN" http --config "$NGROK_BASE_CONFIG" --config "$NGROK_RUNTIME_CONFIG" \
--url="$PUBLIC_URL" "$BACKEND_PORT" \
--log=stdout --log-format=json >> "$NGROK_LOG" 2>&1 &
NGROK_PID=$!
}
wait_for_ngrok() {
local deadline=$((SECONDS + 30))
while [ "$SECONDS" -lt "$deadline" ]; do
if ngrok_is_healthy; then
return 0
fi
if [ -n "$NGROK_PID" ] && ! kill -0 "$NGROK_PID" 2>/dev/null; then
return 1
fi
sleep 1
done
return 1
}
ensure_ngrok() {
if ngrok_is_healthy; then
say '复用已运行的 ngrok 隧道'
return 0
fi
local listener
listener="$(port_listener_pid "$NGROK_API_PORT" || true)"
if [ -n "$listener" ]; then
fail "端口 $NGROK_API_PORT 已有其他 ngrok/服务(PID ${listener}),但固定隧道不匹配"
return 1
fi
say '启动 ngrok...'
start_ngrok || return 1
if ! wait_for_ngrok; then
tail -n 40 "$NGROK_LOG" >&2 || true
fail 'ngrok 未在 30 秒内建立固定隧道'
return 1
fi
}
public_is_healthy() {
local body
body="$(curl -fsS --connect-timeout 3 --max-time 10 \
-H 'ngrok-skip-browser-warning: true' "$PUBLIC_URL/" 2>/dev/null)" || return 1
[[ "$body" == *'<title>凯迪协同办公平台</title>'* ]]
}
wait_for_public() {
local deadline=$((SECONDS + 30))
while [ "$SECONDS" -lt "$deadline" ]; do
if public_is_healthy; then
return 0
fi
sleep 1
done
fail '公网地址未在 30 秒内可访问'
}
maybe_open_browser() {
[ "${ERP_RUN_NO_OPEN:-0}" = '1' ] && return 0
command -v open >/dev/null || { fail '找不到 macOS open 命令'; return 1; }
open "$PUBLIC_URL"
}
monitor_services() {
local backend_active ngrok_active
backend_active="$(port_listener_pid "$BACKEND_PORT" || true)"
ngrok_active="$(port_listener_pid "$NGROK_API_PORT" || true)"
say "本地:$LOCAL_URL"
say "公网:$PUBLIC_URL"
say "进程:ERP PID ${backend_active:-未知}ngrok PID ${ngrok_active:-未知}"
say "日志:${BACKEND_LOG}${NGROK_LOG}"
say '运行中;按 Ctrl+C 同时停止本次启动的服务。'
while :; do
if ! backend_is_healthy; then
[ -n "$BACKEND_PID" ] && tail -n 40 "$BACKEND_LOG" >&2 || true
fail 'ERP 服务失去响应'
return 1
fi
if ! ngrok_is_healthy; then
[ -n "$NGROK_PID" ] && tail -n 40 "$NGROK_LOG" >&2 || true
fail 'ngrok 隧道失去响应'
return 1
fi
sleep 5
done
}
handle_signal() {
printf '\n'
say '正在停止本次启动的服务...'
cleanup
exit 0
}
main() {
trap handle_signal HUP INT TERM
trap cleanup EXIT
say '检查运行环境...'
preflight || return 1
ensure_backend || return 1
ensure_ngrok || return 1
wait_for_public || return 1
maybe_open_browser || return 1
monitor_services
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi