#!/usr/bin/env bash set -euo pipefail # Colors RESET="\033[0m" GREEN="\033[32m" CYAN="\033[36m" YELLOW="\033[33m" RED="\033[31m" REPO_ROOT="$(cd "$(dirname "$0")"/.. && pwd)" BACKEND_DIR="$REPO_ROOT/backend" FRONTEND_DIR="$REPO_ROOT/frontend" CONFIG_FILE="$REPO_ROOT/config/config.json" # Guard to ensure cleanup runs only once __CLEANED_UP=0 # Port configuration BACKEND_PORT=8000 FRONTEND_PORT=3001 # Kill process using specified port kill_port() { local port=$1 local pids=$(lsof -nP -ti tcp:"$port" 2>/dev/null || true) if [[ -n "$pids" ]]; then echo -e "${YELLOW}[CLEANUP]${RESET} Killing process(es) using port $port: $pids" echo "$pids" | xargs kill -9 2>/dev/null || true sleep 1 fi } ensure_backend() { cd "$BACKEND_DIR" if [[ ! -d .venv ]]; then echo -e "${YELLOW}[SETUP]${RESET} Creating Python venv and installing backend requirements..." python3 -m venv .venv source .venv/bin/activate # Upgrade pip first pip install --upgrade pip --timeout 100 -i https://pypi.tuna.tsinghua.edu.cn/simple || \ pip install --upgrade pip --timeout 100 # Install requirements with timeout and mirror pip install -r requirements.txt --timeout 300 -i https://pypi.tuna.tsinghua.edu.cn/simple || \ pip install -r requirements.txt --timeout 300 else source .venv/bin/activate # Upgrade pip if needed pip install --upgrade pip --timeout 100 -i https://pypi.tuna.tsinghua.edu.cn/simple 2>/dev/null || \ pip install --upgrade pip --timeout 100 2>/dev/null || true # Check if key dependencies are installed if ! python -c "import uvicorn" 2>/dev/null; then echo -e "${YELLOW}[SETUP]${RESET} Installing missing backend requirements..." pip install -r requirements.txt --timeout 300 -i https://pypi.tuna.tsinghua.edu.cn/simple || \ pip install -r requirements.txt --timeout 300 fi fi # Export TUSHARE_TOKEN from config if available (prefer jq, fallback to node) if [[ -f "$CONFIG_FILE" ]]; then if command -v jq >/dev/null 2>&1; then TUSHARE_TOKEN_VAL=$(jq -r '.data_sources.tushare.api_key // empty' "$CONFIG_FILE" 2>/dev/null || true) else TUSHARE_TOKEN_VAL=$(node -e "const fs=require('fs');try{const c=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));const v=c?.data_sources?.tushare?.api_key||'';if(v)process.stdout.write(v)}catch{}" "$CONFIG_FILE" 2>/dev/null || true) fi if [[ -n "${TUSHARE_TOKEN_VAL:-}" ]]; then export TUSHARE_TOKEN="$TUSHARE_TOKEN_VAL" fi fi } run_backend() { ensure_backend cd "$BACKEND_DIR" # Run and colorize output (avoid stdbuf on macOS) UVICORN_CMD=(uvicorn app.main:app --reload --port "$BACKEND_PORT" --log-level info) "${UVICORN_CMD[@]}" 2>&1 | while IFS= read -r line; do printf "%b[%s] [BACKEND] %s%b\n" "$GREEN" "$(date '+%Y-%m-%d %H:%M:%S')" "$line" "$RESET" done } ensure_frontend() { cd "$FRONTEND_DIR" if [[ ! -d node_modules ]]; then echo -e "${YELLOW}[SETUP]${RESET} Installing frontend dependencies (npm install)..." npm install --no-fund --no-audit fi } run_frontend() { ensure_frontend cd "$FRONTEND_DIR" npm run dev 2>&1 | while IFS= read -r line; do printf "%b[%s] [FRONTEND] %s%b\n" "$CYAN" "$(date '+%Y-%m-%d %H:%M:%S')" "$line" "$RESET" done } # Recursively kill a process tree (children first), with optional signal (default TERM) kill_tree() { local pid="$1" local signal="${2:-TERM}" if [[ -z "${pid:-}" ]]; then return fi # Kill children first local children children=$(pgrep -P "$pid" 2>/dev/null || true) if [[ -n "${children:-}" ]]; then for child in $children; do kill_tree "$child" "$signal" done fi # Then the parent kill -"$signal" "$pid" 2>/dev/null || true } cleanup() { # Ensure this runs only once even if multiple signals (INT/TERM/EXIT) arrive if [[ $__CLEANED_UP -eq 1 ]]; then return fi __CLEANED_UP=1 echo -e "\n${YELLOW}[CLEANUP]${RESET} Stopping services..." # Gracefully stop trees for backend and frontend, then escalate if needed if [[ -n "${BACKEND_PID:-}" ]]; then kill_tree "$BACKEND_PID" TERM fi if [[ -n "${FRONTEND_PID:-}" ]]; then kill_tree "$FRONTEND_PID" TERM fi # Wait up to ~3s for graceful shutdown for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do local backend_alive=0 frontend_alive=0 if [[ -n "${BACKEND_PID:-}" ]] && kill -0 "$BACKEND_PID" 2>/dev/null; then backend_alive=1; fi if [[ -n "${FRONTEND_PID:-}" ]] && kill -0 "$FRONTEND_PID" 2>/dev/null; then frontend_alive=1; fi if [[ $backend_alive -eq 0 && $frontend_alive -eq 0 ]]; then break fi sleep 0.2 done # Escalate to KILL if still alive if [[ -n "${BACKEND_PID:-}" ]] && kill -0 "$BACKEND_PID" 2>/dev/null; then kill_tree "$BACKEND_PID" KILL fi if [[ -n "${FRONTEND_PID:-}" ]] && kill -0 "$FRONTEND_PID" 2>/dev/null; then kill_tree "$FRONTEND_PID" KILL fi # As a final safeguard, free the ports kill_port "$BACKEND_PORT" kill_port "$FRONTEND_PORT" echo -e "${GREEN}[CLEANUP]${RESET} All services stopped." } main() { echo -e "${CYAN}Dev Launcher${RESET}: Starting Backend ($BACKEND_PORT) and Frontend ($FRONTEND_PORT)\n" # Clean up ports before starting kill_port "$BACKEND_PORT" kill_port "$FRONTEND_PORT" echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] [BACKEND]${RESET} API: http://127.0.0.1:$BACKEND_PORT" echo -e "${CYAN}[$(date '+%Y-%m-%d %H:%M:%S')] [FRONTEND]${RESET} APP: http://127.0.0.1:$FRONTEND_PORT\n" run_backend & BACKEND_PID=$! run_frontend & FRONTEND_PID=$! trap cleanup INT TERM EXIT # Wait on both; if either exits, we exit (portable) while true; do if ! kill -0 "$BACKEND_PID" 2>/dev/null; then break; fi if ! kill -0 "$FRONTEND_PID" 2>/dev/null; then break; fi sleep 1 done } main "$@"