- 新增 up 子命令:build + start + watchdog 一键强制重来 - start 改为轮询 /api/health 就绪才返回,消除启动竞态误报 - start 增加端口占用守卫:非本项目进程占用时明确报错 - watchdog 增加启动宽限(WATCHDOG_STARTUP_GRACE),避开启动窗口误重启 - watchdog 支持 launchd 模式(WATCHDOG_RESTART_MODE),用 launchctl kickstart 重启以避免与 KeepAlive 抢端口 - 抽取 _spawn_server/_wait_ready/foreign_port_holder 公共函数 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
564 lines
18 KiB
Bash
Executable File
564 lines
18 KiB
Bash
Executable File
#!/bin/bash
|
||
# iOS 自动打包服务 — 部署与管理脚本
|
||
# 用法: ./deploy.sh {up|build|start|stop|restart|status|watchdog|watchdog-stop}
|
||
|
||
set -e
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
cd "$SCRIPT_DIR"
|
||
|
||
PID_FILE="$SCRIPT_DIR/.server.pid"
|
||
WATCHDOG_PID_FILE="$SCRIPT_DIR/.watchdog.pid"
|
||
LOG_DIR="$SCRIPT_DIR/logs"
|
||
VENV_DIR="$SCRIPT_DIR/.venv"
|
||
|
||
# 加载 .env
|
||
if [ -f .env ]; then
|
||
set -a; source .env; set +a
|
||
fi
|
||
|
||
BACKEND_PORT="${BACKEND_PORT:-8000}"
|
||
BIND_HOST="${BIND_HOST:-127.0.0.1}"
|
||
WATCHDOG_INTERVAL="${WATCHDOG_INTERVAL:-30}" # 健康检查间隔(秒)
|
||
WATCHDOG_TIMEOUT="${WATCHDOG_TIMEOUT:-10}" # 健康检查超时(秒)
|
||
MAX_RESTART_ATTEMPTS="${MAX_RESTART_ATTEMPTS:-3}" # 连续重启上限,超过则冷却
|
||
COOLDOWN_SECONDS="${COOLDOWN_SECONDS:-300}" # 冷却时间(秒)
|
||
WATCHDOG_STARTUP_GRACE="${WATCHDOG_STARTUP_GRACE:-10}" # watchdog 首次检查前的宽限(秒),避开正常启动窗口
|
||
STARTUP_READY_TIMEOUT="${STARTUP_READY_TIMEOUT:-20}" # 启动后等待 /api/health 就绪的最长时间(秒)
|
||
# 重启方式:nohup(默认,PID 文件托管)| launchd(交回 launchctl kickstart 托管,避免与 KeepAlive 抢端口)
|
||
WATCHDOG_RESTART_MODE="${WATCHDOG_RESTART_MODE:-nohup}"
|
||
APP_LAUNCHD_LABEL="${APP_LAUNCHD_LABEL:-com.readoor.buildserver}"
|
||
|
||
# ========== 颜色输出 ==========
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[0;33m'
|
||
NC='\033[0m'
|
||
|
||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||
|
||
# 在 PID 文件丢失或进程 PID 被复用时,仍能安全识别本服务。
|
||
is_server_process() {
|
||
local pid="$1"
|
||
local command
|
||
command=$(ps -p "$pid" -o command= 2>/dev/null || true)
|
||
[[ "$command" == *"uvicorn backend.main:app"* ]]
|
||
}
|
||
|
||
get_server_pid() {
|
||
local pid
|
||
if [ -f "$PID_FILE" ]; then
|
||
pid=$(cat "$PID_FILE")
|
||
if kill -0 "$pid" 2>/dev/null && is_server_process "$pid"; then
|
||
echo "$pid"
|
||
return 0
|
||
fi
|
||
rm -f "$PID_FILE"
|
||
fi
|
||
|
||
# 仅认领监听当前配置端口、且命令明确属于本项目的 uvicorn 进程。
|
||
for pid in $(lsof -tiTCP:"$BACKEND_PORT" -sTCP:LISTEN 2>/dev/null); do
|
||
if is_server_process "$pid"; then
|
||
echo "$pid" > "$PID_FILE"
|
||
echo "$pid"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# 返回占用目标端口、但命令不属于本项目的进程 PID(若有),用于区分“端口冲突”与“本服务在跑”。
|
||
foreign_port_holder() {
|
||
local pid
|
||
for pid in $(lsof -tiTCP:"$BACKEND_PORT" -sTCP:LISTEN 2>/dev/null); do
|
||
if ! is_server_process "$pid"; then
|
||
echo "$pid"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# 启动 uvicorn 后端进程(单点维护启动参数,供 start / watchdog 复用),PID 写入 PID_FILE。
|
||
_spawn_server() {
|
||
setup_venv
|
||
mkdir -p "$LOG_DIR"
|
||
nohup "$VENV_DIR/bin/python3" -m uvicorn backend.main:app \
|
||
--host "$BIND_HOST" \
|
||
--port "$BACKEND_PORT" \
|
||
--proxy-headers \
|
||
--forwarded-allow-ips 127.0.0.1 \
|
||
--workers 1 \
|
||
>> "$LOG_DIR/server.log" 2>&1 &
|
||
echo $! > "$PID_FILE"
|
||
}
|
||
|
||
# 轮询 /api/health 直到返回 200 或超时;就绪返回 0,否则返回 1。
|
||
# 健康检查固定走 127.0.0.1(与 check_health 一致),不受 BIND_HOST 影响。
|
||
_wait_ready() {
|
||
local timeout="${1:-$STARTUP_READY_TIMEOUT}"
|
||
local deadline=$(( $(date +%s) + timeout ))
|
||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||
# 进程若已退出则无需再等
|
||
if [ -f "$PID_FILE" ] && ! kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||
return 1
|
||
fi
|
||
local code
|
||
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
--connect-timeout 2 --max-time 3 \
|
||
"http://127.0.0.1:$BACKEND_PORT/api/health" 2>/dev/null || echo "000")
|
||
[ "$code" = "200" ] && return 0
|
||
sleep 1
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# 日志同时写文件和终端
|
||
log_to_file() {
|
||
local level="$1"; shift
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >> "$LOG_DIR/watchdog.log"
|
||
}
|
||
|
||
# ========== 环境检查 ==========
|
||
check_env() {
|
||
if [ ! -f .env ]; then
|
||
error ".env 文件不存在"
|
||
echo " 请执行: cp .env.example .env && vim .env"
|
||
exit 1
|
||
fi
|
||
|
||
if ! command -v python3 &>/dev/null; then
|
||
error "未找到 python3"
|
||
exit 1
|
||
fi
|
||
|
||
# 运行已构建的前端不需要 Node;仅在缺少静态资源时才需要它重新构建。
|
||
if [ ! -d "$SCRIPT_DIR/backend/static" ] && ! command -v node &>/dev/null; then
|
||
error "未找到 node,且前端尚未构建"
|
||
exit 1
|
||
fi
|
||
|
||
# 检查 Xcode
|
||
if ! command -v xcodebuild &>/dev/null; then
|
||
warn "未找到 xcodebuild,打包功能将不可用"
|
||
fi
|
||
}
|
||
|
||
# ========== Python 虚拟环境 ==========
|
||
setup_venv() {
|
||
# 项目目录移动后,venv 内 pip 的 shebang 会指向旧路径,需要重建。
|
||
if [ -d "$VENV_DIR" ] && ! "$VENV_DIR/bin/pip" --version >/dev/null 2>&1; then
|
||
warn "检测到失效的 Python 虚拟环境,正在重建..."
|
||
rm -rf "$VENV_DIR"
|
||
fi
|
||
if [ ! -d "$VENV_DIR" ]; then
|
||
info "创建 Python 虚拟环境..."
|
||
python3 -m venv "$VENV_DIR"
|
||
fi
|
||
"$VENV_DIR/bin/python3" -m pip install -r requirements.txt -q
|
||
}
|
||
|
||
# ========== 构建前端 ==========
|
||
do_build() {
|
||
# build 只需要 Python 和 Node,不要求 .env
|
||
if ! command -v python3 &>/dev/null; then
|
||
error "未找到 python3"; exit 1
|
||
fi
|
||
if ! command -v node &>/dev/null; then
|
||
error "未找到 node"; exit 1
|
||
fi
|
||
|
||
# 首次部署时自动创建 .env
|
||
if [ ! -f .env ]; then
|
||
info "首次部署,创建 .env 配置文件..."
|
||
cp .env.example .env
|
||
warn "已创建 .env,请按需修改配置(当前为默认值)"
|
||
fi
|
||
|
||
info "安装前端依赖..."
|
||
cd "$SCRIPT_DIR/frontend"
|
||
npm install --silent
|
||
|
||
info "构建前端..."
|
||
npm run build
|
||
cd "$SCRIPT_DIR"
|
||
|
||
setup_venv
|
||
|
||
info "构建完成"
|
||
}
|
||
|
||
# ========== 启动服务 ==========
|
||
do_start() {
|
||
check_env
|
||
|
||
# 端口被非本项目进程占用:明确报错,避免 uvicorn 静默绑定失败。
|
||
local foreign
|
||
foreign=$(foreign_port_holder || true)
|
||
if [ -n "$foreign" ]; then
|
||
error "端口 $BACKEND_PORT 被非本项目进程占用 (PID: $foreign):"
|
||
ps -p "$foreign" -o pid,command= 2>/dev/null | sed 's/^/ /'
|
||
error "请先释放该端口,或在 .env 修改 BACKEND_PORT"
|
||
return 1
|
||
fi
|
||
|
||
# 本项目实例已在运行:强制重来(用户选定语义)。
|
||
# 注意:这会中断正在进行的打包任务。
|
||
local existing_pid
|
||
existing_pid=$(get_server_pid || true)
|
||
if [ -n "$existing_pid" ]; then
|
||
warn "检测到旧服务 (PID: $existing_pid),强制重启(若有打包任务将被中断)..."
|
||
do_stop
|
||
fi
|
||
|
||
# 确保前端已构建
|
||
if [ ! -d "$SCRIPT_DIR/backend/static" ]; then
|
||
warn "前端未构建,先执行 build..."
|
||
do_build
|
||
fi
|
||
|
||
info "启动后端服务 ($BIND_HOST:$BACKEND_PORT)..."
|
||
_spawn_server
|
||
|
||
if _wait_ready; then
|
||
info "服务已就绪 (PID: $(cat "$PID_FILE"))"
|
||
info "访问地址: http://$BIND_HOST:$BACKEND_PORT"
|
||
else
|
||
error "服务启动后 ${STARTUP_READY_TIMEOUT}s 内未就绪,请查看日志: $LOG_DIR/server.log"
|
||
tail -20 "$LOG_DIR/server.log" 2>/dev/null | sed 's/^/ /'
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ========== 停止服务 ==========
|
||
do_stop() {
|
||
local pid
|
||
pid=$(get_server_pid || true)
|
||
if [ -z "$pid" ]; then
|
||
warn "服务未在运行"
|
||
return 0
|
||
fi
|
||
|
||
info "停止服务 (PID: $pid)..."
|
||
kill "$pid"
|
||
# 等待进程退出
|
||
for i in $(seq 1 10); do
|
||
kill -0 "$pid" 2>/dev/null || break
|
||
sleep 0.5
|
||
done
|
||
# 还没退出就强杀
|
||
if kill -0 "$pid" 2>/dev/null; then
|
||
warn "进程未响应,强制终止..."
|
||
kill -9 "$pid"
|
||
fi
|
||
info "服务已停止"
|
||
rm -f "$PID_FILE"
|
||
}
|
||
|
||
# ========== 查看状态 ==========
|
||
do_status() {
|
||
local pid
|
||
pid=$(get_server_pid || true)
|
||
if [ -n "$pid" ]; then
|
||
info "服务运行中 (PID: $pid), 端口: $BACKEND_PORT"
|
||
else
|
||
warn "服务未运行"
|
||
fi
|
||
|
||
if [ -f "$WATCHDOG_PID_FILE" ] && kill -0 "$(cat "$WATCHDOG_PID_FILE")" 2>/dev/null; then
|
||
info "Watchdog 运行中 (PID: $(cat "$WATCHDOG_PID_FILE")), 检查间隔: ${WATCHDOG_INTERVAL}s"
|
||
else
|
||
warn "Watchdog 未运行"
|
||
rm -f "$WATCHDOG_PID_FILE" 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# ========== 健康检查 ==========
|
||
check_health() {
|
||
# 检查进程是否存在
|
||
local pid
|
||
pid=$(get_server_pid || true)
|
||
if [ -z "$pid" ]; then
|
||
echo "process_down"
|
||
return
|
||
fi
|
||
|
||
# 检查 HTTP 是否响应
|
||
local http_code
|
||
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
--connect-timeout "$WATCHDOG_TIMEOUT" \
|
||
--max-time "$WATCHDOG_TIMEOUT" \
|
||
"http://127.0.0.1:$BACKEND_PORT/api/health" 2>/dev/null || echo "000")
|
||
|
||
if [ "$http_code" != "200" ]; then
|
||
echo "http_$http_code"
|
||
return
|
||
fi
|
||
|
||
echo "ok"
|
||
}
|
||
|
||
# ========== Watchdog(前台模式,用于调试) ==========
|
||
do_watchdog_fg() {
|
||
info "Watchdog 启动(前台模式),检查间隔: ${WATCHDOG_INTERVAL}s,启动宽限: ${WATCHDOG_STARTUP_GRACE}s"
|
||
info "按 Ctrl+C 停止"
|
||
|
||
local restart_count=0
|
||
local last_restart_time=0
|
||
|
||
# 启动宽限:避开服务正常启动/重启的就绪窗口,防止误判触发重启。
|
||
sleep "$WATCHDOG_STARTUP_GRACE"
|
||
|
||
while true; do
|
||
local result
|
||
result=$(check_health)
|
||
|
||
if [ "$result" = "ok" ]; then
|
||
restart_count=0
|
||
else
|
||
local now
|
||
now=$(date +%s)
|
||
warn "健康检查失败: $result"
|
||
|
||
# 冷却判断:连续重启超过上限,暂停一段时间
|
||
if [ "$restart_count" -ge "$MAX_RESTART_ATTEMPTS" ]; then
|
||
local elapsed=$((now - last_restart_time))
|
||
if [ "$elapsed" -lt "$COOLDOWN_SECONDS" ]; then
|
||
local wait=$((COOLDOWN_SECONDS - elapsed))
|
||
warn "连续重启 ${restart_count} 次,冷却中,${wait}s 后重试..."
|
||
log_to_file "WARN" "Cooldown: ${restart_count} restarts, waiting ${wait}s"
|
||
sleep "$wait"
|
||
continue
|
||
else
|
||
warn "冷却结束,重新尝试..."
|
||
restart_count=0
|
||
fi
|
||
fi
|
||
|
||
warn "正在重启服务..."
|
||
log_to_file "WARN" "Restarting service (cause: $result, attempt: $((restart_count+1)))"
|
||
|
||
do_stop
|
||
sleep 2
|
||
do_start
|
||
|
||
restart_count=$((restart_count + 1))
|
||
last_restart_time=$(date +%s)
|
||
|
||
if [ "$restart_count" -ge "$MAX_RESTART_ATTEMPTS" ]; then
|
||
error "已连续重启 ${restart_count} 次,进入冷却..."
|
||
log_to_file "ERROR" "Entered cooldown after $restart_count restarts"
|
||
fi
|
||
fi
|
||
|
||
sleep "$WATCHDOG_INTERVAL"
|
||
done
|
||
}
|
||
|
||
# ========== Watchdog(后台守护) ==========
|
||
do_watchdog() {
|
||
mkdir -p "$LOG_DIR"
|
||
|
||
if [ -f "$WATCHDOG_PID_FILE" ] && kill -0 "$(cat "$WATCHDOG_PID_FILE")" 2>/dev/null; then
|
||
warn "Watchdog 已在运行 (PID: $(cat "$WATCHDOG_PID_FILE"))"
|
||
return 0
|
||
fi
|
||
|
||
info "启动 Watchdog(后台模式),检查间隔: ${WATCHDOG_INTERVAL}s"
|
||
|
||
nohup bash -c "
|
||
cd '$SCRIPT_DIR'
|
||
source .env 2>/dev/null
|
||
exec bash '$(realpath "$0")' _watchdog_loop
|
||
" >> "$LOG_DIR/watchdog.log" 2>&1 &
|
||
|
||
echo $! > "$WATCHDOG_PID_FILE"
|
||
sleep 1
|
||
|
||
if kill -0 "$(cat "$WATCHDOG_PID_FILE")" 2>/dev/null; then
|
||
info "Watchdog 已启动 (PID: $(cat "$WATCHDOG_PID_FILE"))"
|
||
else
|
||
error "Watchdog 启动失败"
|
||
rm -f "$WATCHDOG_PID_FILE"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 内部循环入口(由后台 watchdog 进程调用)
|
||
do_watchdog_loop() {
|
||
local restart_count=0
|
||
local last_restart_time=0
|
||
|
||
log_to_file "INFO" "Watchdog started, interval=${WATCHDOG_INTERVAL}s, grace=${WATCHDOG_STARTUP_GRACE}s, mode=${WATCHDOG_RESTART_MODE}"
|
||
|
||
# 启动宽限:避开服务正常启动/重启的就绪窗口,防止误判触发重启。
|
||
sleep "$WATCHDOG_STARTUP_GRACE"
|
||
|
||
while true; do
|
||
local result
|
||
result=$(check_health)
|
||
|
||
if [ "$result" = "ok" ]; then
|
||
restart_count=0
|
||
else
|
||
local now
|
||
now=$(date +%s)
|
||
log_to_file "WARN" "Health check failed: $result"
|
||
|
||
if [ "$restart_count" -ge "$MAX_RESTART_ATTEMPTS" ]; then
|
||
local elapsed=$((now - last_restart_time))
|
||
if [ "$elapsed" -lt "$COOLDOWN_SECONDS" ]; then
|
||
local wait=$((COOLDOWN_SECONDS - elapsed))
|
||
log_to_file "WARN" "Cooldown: ${restart_count} restarts, waiting ${wait}s"
|
||
sleep "$wait"
|
||
continue
|
||
else
|
||
log_to_file "INFO" "Cooldown ended, resetting counter"
|
||
restart_count=0
|
||
fi
|
||
fi
|
||
|
||
restart_count=$((restart_count + 1))
|
||
last_restart_time=$(date +%s)
|
||
log_to_file "WARN" "Restarting service (attempt: $restart_count, mode: $WATCHDOG_RESTART_MODE)"
|
||
|
||
if [ "$WATCHDOG_RESTART_MODE" = "launchd" ]; then
|
||
# launchd 托管模式:交回 launchctl 重启,避免与 KeepAlive 抢占端口。
|
||
if launchctl kickstart -k "gui/$(id -u)/$APP_LAUNCHD_LABEL" 2>/dev/null; then
|
||
log_to_file "INFO" "launchctl kickstart issued for $APP_LAUNCHD_LABEL"
|
||
else
|
||
log_to_file "ERROR" "launchctl kickstart failed for $APP_LAUNCHD_LABEL"
|
||
fi
|
||
else
|
||
# nohup 托管模式:停掉旧进程后重新拉起。
|
||
if [ -f "$PID_FILE" ]; then
|
||
local old_pid
|
||
old_pid=$(cat "$PID_FILE")
|
||
kill "$old_pid" 2>/dev/null
|
||
sleep 2
|
||
kill -9 "$old_pid" 2>/dev/null || true
|
||
rm -f "$PID_FILE"
|
||
fi
|
||
_spawn_server
|
||
fi
|
||
|
||
# 无论哪种模式,都等待 /api/health 就绪再继续,避免连锁误判。
|
||
if _wait_ready; then
|
||
log_to_file "INFO" "Service restarted successfully (PID: $(cat "$PID_FILE" 2>/dev/null))"
|
||
else
|
||
log_to_file "ERROR" "Service restart failed or not ready in ${STARTUP_READY_TIMEOUT}s"
|
||
fi
|
||
fi
|
||
|
||
sleep "$WATCHDOG_INTERVAL"
|
||
done
|
||
}
|
||
|
||
# ========== 停止 Watchdog ==========
|
||
do_watchdog_stop() {
|
||
if [ ! -f "$WATCHDOG_PID_FILE" ]; then
|
||
warn "Watchdog 未在运行"
|
||
return 0
|
||
fi
|
||
|
||
local pid
|
||
pid=$(cat "$WATCHDOG_PID_FILE")
|
||
if kill -0 "$pid" 2>/dev/null; then
|
||
info "停止 Watchdog (PID: $pid)..."
|
||
kill "$pid"
|
||
sleep 1
|
||
kill -9 "$pid" 2>/dev/null || true
|
||
info "Watchdog 已停止"
|
||
else
|
||
warn "Watchdog 进程已不存在"
|
||
fi
|
||
rm -f "$WATCHDOG_PID_FILE"
|
||
}
|
||
|
||
# ========== 运行测试 ==========
|
||
do_test() {
|
||
local failed=0
|
||
|
||
# 确保依赖已安装
|
||
setup_venv
|
||
"$VENV_DIR/bin/python3" -m pip install -r requirements-dev.txt -q
|
||
|
||
if [ ! -d "frontend/node_modules" ]; then
|
||
cd frontend && npm install --silent && cd ..
|
||
fi
|
||
|
||
# 后端测试
|
||
info "运行后端测试..."
|
||
echo "────────────────────────────────────────"
|
||
"$VENV_DIR/bin/python3" -m pytest tests/ -v "$@" || failed=1
|
||
echo ""
|
||
|
||
# 前端测试
|
||
info "运行前端测试..."
|
||
echo "────────────────────────────────────────"
|
||
cd frontend && npx vitest run "$@" || failed=1
|
||
cd "$SCRIPT_DIR"
|
||
echo ""
|
||
|
||
# 结果
|
||
if [ "$failed" -eq 0 ]; then
|
||
info "全部测试通过"
|
||
else
|
||
error "存在失败的测试"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ========== 一键部署(强制重来)==========
|
||
# build → start(强制重启并等待就绪)→ watchdog,最后打印状态。
|
||
do_up() {
|
||
info "一键部署(强制重来):build → start → watchdog"
|
||
do_watchdog_stop >/dev/null 2>&1 || true # 先停监听,避免它在重启窗口内干预
|
||
do_build
|
||
do_start # do_start 内部会强制停旧服务并等待 health 就绪
|
||
do_watchdog
|
||
echo ""
|
||
do_status
|
||
}
|
||
|
||
# ========== 主入口 ==========
|
||
case "${1:-}" in
|
||
up) do_up ;;
|
||
build) do_build ;;
|
||
start) do_start ;;
|
||
stop) do_stop; do_watchdog_stop ;;
|
||
restart) do_stop; do_start ;;
|
||
status) do_status ;;
|
||
test) shift; do_test "$@" ;;
|
||
watchdog) do_watchdog ;;
|
||
watchdog-stop) do_watchdog_stop ;;
|
||
watchdog-fg) do_watchdog_fg ;;
|
||
_watchdog_loop) do_watchdog_loop ;;
|
||
*)
|
||
echo "用法: $0 {up|build|start|stop|restart|status|test|watchdog|watchdog-stop}"
|
||
echo ""
|
||
echo " up 一键部署:build + start + watchdog(强制重来)"
|
||
echo " build 构建前端、安装依赖"
|
||
echo " start 启动服务(强制重启并等待就绪;端口被占用会报错)"
|
||
echo " stop 停止服务(同时停止 watchdog)"
|
||
echo " restart 重启服务"
|
||
echo " status 查看运行状态"
|
||
echo " test 运行全部测试(后端 + 前端)"
|
||
echo " watchdog 启动健康监测(后台守护,自动重启故障服务)"
|
||
echo " watchdog-stop 停止健康监测"
|
||
echo " watchdog-fg 前台运行健康监测(调试用)"
|
||
echo ""
|
||
echo "环境变量(可在 .env 中配置):"
|
||
echo " WATCHDOG_INTERVAL 健康检查间隔(秒,默认 30)"
|
||
echo " WATCHDOG_TIMEOUT 健康检查超时(秒,默认 10)"
|
||
echo " WATCHDOG_STARTUP_GRACE watchdog 首次检查前宽限(秒,默认 10)"
|
||
echo " STARTUP_READY_TIMEOUT 启动后等待 health 就绪的最长时间(秒,默认 20)"
|
||
echo " WATCHDOG_RESTART_MODE 重启方式 nohup|launchd(默认 nohup)"
|
||
echo " MAX_RESTART_ATTEMPTS 连续重启上限(默认 3)"
|
||
echo " COOLDOWN_SECONDS 超过重启上限后冷却时间(秒,默认 300)"
|
||
exit 1
|
||
;;
|
||
esac
|