Constraint: Finder launch must supervise Java and ngrok in one visible Terminal Rejected: LaunchAgent | obscures ownership and makes safe cleanup harder Confidence: high Scope-risk: narrow Directive: Never kill listeners not recorded as launcher-owned PIDs Tested: Shell unit suite, repeat-start reuse, cold start, local/public login and protected API smoke Not-tested: Finder Gatekeeper behavior on a different Mac
261 lines
7.2 KiB
Bash
Executable File
261 lines
7.2 KiB
Bash
Executable File
#!/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}"
|
||
JAVA_BIN="${ERP_RUN_JAVA_BIN:-$ROOT_DIR/.jdks/jdk-17.0.19+10/Contents/Home/bin/java}"
|
||
JAR_PATH="${ERP_RUN_JAR_PATH:-$BACKEND_DIR/build/libs/oa-backend-0.1.0.jar}"
|
||
BACKEND_PORT="${ERP_RUN_BACKEND_PORT:-8091}"
|
||
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_BIN=""
|
||
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
|
||
exec "$JAVA_BIN" -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
|
||
}
|
||
|
||
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; }
|
||
[ -x "$JAVA_BIN" ] || { fail "找不到项目 JDK:$JAVA_BIN"; return 1; }
|
||
[ -r "$JAR_PATH" ] || { fail "找不到可运行 JAR:$JAR_PATH"; return 1; }
|
||
[ -r "$BACKEND_DIR/data/oa.db" ] || { fail "找不到 SQLite 数据库:$BACKEND_DIR/data/oa.db"; return 1; }
|
||
NGROK_BIN="$(resolve_ngrok_bin)" || { fail '找不到 ngrok(预期 $HOME/bin/ngrok 或 PATH)'; 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"
|
||
"$NGROK_BIN" http --url=resonant-elated-launder.ngrok-free.dev "$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
|