#!/usr/bin/env bash set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" VENV_DIR="${ROOT_DIR}/.venv" PYTHON_BIN="${VENV_DIR}/bin/python" UVICORN_BIN="${VENV_DIR}/bin/uvicorn" CELERY_BIN="${VENV_DIR}/bin/celery" API_HOST="${API_HOST:-0.0.0.0}" API_PORT="${API_PORT:-8000}" CELERY_POOL="${CELERY_POOL:-solo}" SKIP_INSTALL="${SKIP_INSTALL:-0}" SHUTDOWN_TIMEOUT="${SHUTDOWN_TIMEOUT:-12}" API_PID="" CELERY_PID="" CLEANED_UP=0 INFRA_STARTED=0 print_header() { echo -e "\n${BLUE}========================================${NC}" echo -e "${BLUE}$1${NC}" echo -e "${BLUE}========================================${NC}" } print_ok() { echo -e "${GREEN}✓ $1${NC}" } print_warn() { echo -e "${YELLOW}⚠ $1${NC}" } print_err() { echo -e "${RED}✗ $1${NC}" } is_pid_alive() { local pid="$1" [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null } wait_pid_exit() { local pid="$1" local timeout="$2" local waited=0 while is_pid_alive "${pid}"; do if (( waited >= timeout )); then return 1 fi sleep 1 waited=$((waited + 1)) done return 0 } kill_children_term() { local pid="$1" local children children="$(pgrep -P "${pid}" 2>/dev/null || true)" if [[ -n "${children}" ]]; then # 先递归处理子进程,避免 reloader/server 残留 while IFS= read -r child_pid; do [[ -z "${child_pid}" ]] && continue kill_children_term "${child_pid}" kill -TERM "${child_pid}" 2>/dev/null || true done <<< "${children}" fi } stop_process_gracefully() { local name="$1" local pid="$2" local timeout="${3:-10}" if ! is_pid_alive "${pid}"; then print_ok "${name} 已退出" return 0 fi print_warn "正在停止 ${name}(PID: ${pid})..." kill_children_term "${pid}" kill -TERM "${pid}" 2>/dev/null || true if wait_pid_exit "${pid}" "${timeout}"; then print_ok "${name} 已停止" return 0 fi print_warn "${name} 在 ${timeout}s 内未退出,准备强制结束" kill -KILL "${pid}" 2>/dev/null || true wait_pid_exit "${pid}" 3 || true print_ok "${name} 已强制结束" } cleanup() { if [[ "${CLEANED_UP}" == "1" ]]; then return 0 fi CLEANED_UP=1 print_header "正在关闭开发环境" if is_pid_alive "${API_PID}"; then stop_process_gracefully "FastAPI" "${API_PID}" "${SHUTDOWN_TIMEOUT}" fi if is_pid_alive "${CELERY_PID}"; then stop_process_gracefully "Celery" "${CELERY_PID}" "${SHUTDOWN_TIMEOUT}" fi if [[ "${INFRA_STARTED}" == "1" ]]; then print_warn "正在停止 PostgreSQL / Redis 容器..." ( cd "${ROOT_DIR}" && docker compose -f docker-compose.dev.yml stop ) >/dev/null 2>&1 || true print_ok "PostgreSQL/Redis 容器已停止" fi } require_cmd() { local cmd="$1" if ! command -v "${cmd}" >/dev/null 2>&1; then print_err "未找到命令: ${cmd}" exit 1 fi } start_infra() { print_header "启动 PostgreSQL 和 Redis" cd "${ROOT_DIR}" docker compose -f docker-compose.dev.yml up -d INFRA_STARTED=1 print_ok "基础设施已就绪" } ensure_venv() { print_header "检查 Python 虚拟环境" if [[ ! -d "${VENV_DIR}" ]]; then print_warn ".venv 不存在,正在创建" uv venv "${VENV_DIR}" fi if [[ "${SKIP_INSTALL}" != "1" ]]; then print_header "安装 Python 依赖" uv pip install --python "${PYTHON_BIN}" -r "${ROOT_DIR}/requirements.txt" print_ok "依赖安装完成" else print_warn "已跳过依赖安装 (SKIP_INSTALL=1)" fi } check_env_file() { print_header "检查环境变量文件" if [[ ! -f "${ROOT_DIR}/.env" ]]; then print_warn "未找到 .env,应用可能因缺少配置启动失败" print_warn "请参考 api/README.md 创建 .env" else print_ok "检测到 .env" fi } start_services() { print_header "启动 FastAPI 和 Celery" cd "${ROOT_DIR}" "${UVICORN_BIN}" main:app --reload --host "${API_HOST}" --port "${API_PORT}" & API_PID=$! print_ok "FastAPI 已启动 (PID: ${API_PID})" "${CELERY_BIN}" -A tasks.celery_app worker --loglevel=info --pool="${CELERY_POOL}" & CELERY_PID=$! print_ok "Celery 已启动 (PID: ${CELERY_PID})" echo echo -e "${GREEN}开发环境启动完成${NC}" echo "API 文档: http://localhost:${API_PORT}/docs" echo "健康检查: http://localhost:${API_PORT}/health" echo "按 Ctrl+C 停止所有进程" } main() { print_header "Life Echo 开发环境一键启动" require_cmd "docker" require_cmd "uv" trap cleanup EXIT INT TERM start_infra ensure_venv check_env_file start_services wait "${API_PID}" "${CELERY_PID}" } main "$@"