feat: deploy.sh 支持一键部署与就绪探测
- 新增 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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9d766a0b58
commit
b2982ae8f7
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# iOS 自动打包服务 — 部署与管理脚本
|
||||
# 用法: ./deploy.sh {build|start|stop|restart|status|watchdog|watchdog-stop}
|
||||
# 用法: ./deploy.sh {up|build|start|stop|restart|status|watchdog|watchdog-stop}
|
||||
|
||||
set -e
|
||||
|
||||
@@ -23,6 +23,11 @@ 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'
|
||||
@@ -64,6 +69,52 @@ get_server_pid() {
|
||||
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
|
||||
@@ -143,11 +194,23 @@ do_build() {
|
||||
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)"
|
||||
return 0
|
||||
warn "检测到旧服务 (PID: $existing_pid),强制重启(若有打包任务将被中断)..."
|
||||
do_stop
|
||||
fi
|
||||
|
||||
# 确保前端已构建
|
||||
@@ -156,28 +219,15 @@ do_start() {
|
||||
do_build
|
||||
fi
|
||||
|
||||
setup_venv
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
info "启动后端服务 ($BIND_HOST:$BACKEND_PORT)..."
|
||||
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 &
|
||||
_spawn_server
|
||||
|
||||
echo $! > "$PID_FILE"
|
||||
sleep 1
|
||||
|
||||
if kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||
info "服务已启动 (PID: $(cat "$PID_FILE"))"
|
||||
if _wait_ready; then
|
||||
info "服务已就绪 (PID: $(cat "$PID_FILE"))"
|
||||
info "访问地址: http://$BIND_HOST:$BACKEND_PORT"
|
||||
else
|
||||
error "启动失败,请查看日志: $LOG_DIR/server.log"
|
||||
rm -f "$PID_FILE"
|
||||
error "服务启动后 ${STARTUP_READY_TIMEOUT}s 内未就绪,请查看日志: $LOG_DIR/server.log"
|
||||
tail -20 "$LOG_DIR/server.log" 2>/dev/null | sed 's/^/ /'
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -252,12 +302,15 @@ check_health() {
|
||||
|
||||
# ========== Watchdog(前台模式,用于调试) ==========
|
||||
do_watchdog_fg() {
|
||||
info "Watchdog 启动(前台模式),检查间隔: ${WATCHDOG_INTERVAL}s"
|
||||
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)
|
||||
@@ -338,7 +391,10 @@ do_watchdog_loop() {
|
||||
local restart_count=0
|
||||
local last_restart_time=0
|
||||
|
||||
log_to_file "INFO" "Watchdog started, interval=${WATCHDOG_INTERVAL}s"
|
||||
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
|
||||
@@ -366,9 +422,17 @@ do_watchdog_loop() {
|
||||
|
||||
restart_count=$((restart_count + 1))
|
||||
last_restart_time=$(date +%s)
|
||||
log_to_file "WARN" "Restarting service (attempt: $restart_count)"
|
||||
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")
|
||||
@@ -377,24 +441,14 @@ do_watchdog_loop() {
|
||||
kill -9 "$old_pid" 2>/dev/null || true
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
_spawn_server
|
||||
fi
|
||||
|
||||
# 重新启动
|
||||
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"
|
||||
|
||||
sleep 3
|
||||
if kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||
log_to_file "INFO" "Service restarted successfully (PID: $(cat "$PID_FILE"))"
|
||||
# 无论哪种模式,都等待 /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"
|
||||
log_to_file "ERROR" "Service restart failed or not ready in ${STARTUP_READY_TIMEOUT}s"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -457,8 +511,21 @@ do_test() {
|
||||
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 ;;
|
||||
@@ -470,10 +537,11 @@ case "${1:-}" in
|
||||
watchdog-fg) do_watchdog_fg ;;
|
||||
_watchdog_loop) do_watchdog_loop ;;
|
||||
*)
|
||||
echo "用法: $0 {build|start|stop|restart|status|test|watchdog|watchdog-stop}"
|
||||
echo "用法: $0 {up|build|start|stop|restart|status|test|watchdog|watchdog-stop}"
|
||||
echo ""
|
||||
echo " up 一键部署:build + start + watchdog(强制重来)"
|
||||
echo " build 构建前端、安装依赖"
|
||||
echo " start 启动服务(后台运行)"
|
||||
echo " start 启动服务(强制重启并等待就绪;端口被占用会报错)"
|
||||
echo " stop 停止服务(同时停止 watchdog)"
|
||||
echo " restart 重启服务"
|
||||
echo " status 查看运行状态"
|
||||
@@ -485,6 +553,9 @@ case "${1:-}" in
|
||||
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
|
||||
|
||||
@@ -95,6 +95,8 @@ EOF
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>$VENV_DIR/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
<key>WATCHDOG_RESTART_MODE</key>
|
||||
<string>launchd</string>
|
||||
</dict>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
Reference in New Issue
Block a user