#!/bin/bash # Hot-Reload Dev Script for TopFans Backend # Usage: ./dev.sh # # Requires: fswatch (Mac: brew install fswatch) or inotifywait (Linux) set -e # 信号捕获:优雅退出 cleanup() { echo "" echo -e "${YELLOW}🛑 收到停止信号,正在关闭所有服务...${NC}" # 先杀所有 watcher 和 debounce 进程(防止继续触发重启) if [ -f /tmp/dev_sh_watchers.tmp ]; then while read watcher_pid; do kill "$watcher_pid" 2>/dev/null || true done < /tmp/dev_sh_watchers.tmp rm -f /tmp/dev_sh_watchers.tmp fi # 清理所有 PID 文件并杀服务进程 for service in gateway activityService galleryService socialService assetService userService taskService starbookService aiChatService; do pkill -9 -f "$service" 2>/dev/null || true rm -f "/tmp/dev_sh_${service}.pid" "/tmp/dev_sh_${service}_restart" "/tmp/dev_sh_${service}.lock" echo -e "${YELLOW} 🛑 $service 已停止${NC}" done echo -e "${GREEN}✅ 所有服务已关闭${NC}" exit 0 } trap cleanup SIGINT SIGTERM RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # Detect platform if [[ "$(uname)" == "Darwin" ]]; then WATCHER_TOOL="fswatch" WATCHER_CMD="fswatch -r" elif [[ "$(uname)" == "Linux" ]]; then WATCHER_TOOL="inotifywait" WATCHER_CMD="inotifywait -r -m -e modify,create,write" elif [[ "$(uname)" =~ ^MINGW ]] || [[ "$(uname)" =~ ^MSYS ]] || [[ "$(uname)" =~ ^CYGWIN ]]; then # Windows via Git Bash / MSYS2 / Cygwin WATCHER_TOOL="fswatch" WATCHER_CMD="fswatch -r" else echo -e "${RED}不支持的平台${NC}" exit 1 fi if ! command -v "$WATCHER_TOOL" &> /dev/null; then echo -e "${RED}缺少工具: $WATCHER_TOOL${NC}" if [[ "$(uname)" == "Darwin" ]]; then echo "安装方法: brew install fswatch" elif [[ "$(uname)" =~ ^MINGW ]] || [[ "$(uname)" =~ ^MSYS ]] || [[ "$(uname)" =~ ^CYGWIN ]]; then echo "安装方法: choco install fswatch (Chocolatey)" echo " scoop install fswatch (Scoop)" echo " winget install fswatch (WinGet)" else echo "安装方法: sudo apt install inotify-tools (Debian/Ubuntu)" echo " sudo yum install inotify-tools (CentOS/RHEL)" fi exit 1 fi ENV_FILE="$SCRIPT_DIR/.env" if [ -f "$ENV_FILE" ]; then echo -e "${GREEN}📄 加载 .env 文件...${NC}" set -a source "$ENV_FILE" set +a fi DB_HOST="${DB_HOST:-localhost}" DB_PORT="${DB_PORT:-15432}" DB_USER="${DB_USER:-postgres}" DB_PASSWORD="${DB_PASSWORD:-123456}" DB_NAME="${DB_NAME:-top-fans}" REDIS_HOST="${REDIS_HOST:-localhost}" REDIS_PORT="${REDIS_PORT:-6379}" REDIS_PASSWORD="${REDIS_PASSWORD:-123456}" REDIS_DB="${REDIS_DB:-0}" DB_ARGS=(-db-host="$DB_HOST" -db-port="$DB_PORT" -db-user="$DB_USER" -db-password="$DB_PASSWORD" -db-name="$DB_NAME") REDIS_ARGS=(-redis-host="$REDIS_HOST" -redis-port="$REDIS_PORT" -redis-db="$REDIS_DB" -redis-password="$REDIS_PASSWORD") # 加载服务私有 env(对齐 deploy/envs/ 部署路径) # 用法: load_service_env name -> source deploy/envs/.env load_service_env() { local name=$1 local service_env="$SCRIPT_DIR/deploy/envs/${name%Service}.env" if [ -f "$service_env" ]; then set -a source "$service_env" set +a fi } # 启动一个服务 # 用法: start_service name binary port use_db use_redis start_service() { local name=$1 local binary=$2 local port=$3 local use_db=$4 local use_redis=$5 echo -e "${GREEN}🚀 启动 $name...${NC}" # Only services whose deploy/envs entry adds new vars not in # backend/.env need the per-service source. Loading the rest # would clobber dev's ports/JWT_SECRET/OSS keys with prod values. case "$name" in userService) load_service_env "$name" ;; esac local args=("-port=$port") if [ "$use_db" = "1" ]; then args+=("${DB_ARGS[@]}") fi if [ "$use_redis" = "1" ]; then args+=("${REDIS_ARGS[@]}") fi "$SCRIPT_DIR/$binary" "${args[@]}" > "/tmp/${name}.log" 2>&1 & local pid=$! # 保存 PID 到文件 echo $pid > "/tmp/dev_sh_${name}.pid" sleep 2 if ps -p $pid > /dev/null 2>&1; then echo -e "${GREEN}✅ $name 已启动 (PID: $pid, 端口: $port)${NC}" else echo -e "${RED}❌ $name 启动失败${NC}" echo -e "${YELLOW}查看日志: tail -f /tmp/${name}.log${NC}" fi } # 构建一个服务(在服务目录下执行 go build) # 用法: build_service name dir binary build_service() { local name=$1 local dir=$2 local binary=$3 if [ ! -d "$SCRIPT_DIR/$dir" ]; then echo -e "${RED}❌ $dir 目录不存在${NC}" return 1 fi cd "$SCRIPT_DIR/$dir" if go build -o "$SCRIPT_DIR/$binary" . 2>&1; then echo -e "${GREEN}✅ $name 编译成功${NC}" return 0 else echo -e "${RED}❌ $name 编译失败${NC}" return 1 fi } # 重建并重启单个服务(构建成功后才杀旧进程) # 用法: restart_service name dir binary port use_db use_redis restart_service() { local name=$1 local dir=$2 local binary=$3 local port=$4 local use_db=$5 local use_redis=$6 local pid_file="/tmp/dev_sh_${name}.pid" local lock_file="/tmp/dev_sh_${name}.lock" # 加锁防止并发重启 if [ -f "$lock_file" ]; then return 0 fi touch "$lock_file" # Step 1: 编译(先编译,编译成功才杀旧进程) if [ ! -d "$SCRIPT_DIR/$dir" ]; then echo -e "${RED}❌ $dir 目录不存在,跳过${NC}" return 1 fi cd "$SCRIPT_DIR/$dir" # 重试编译(最多3次) local max_retries=3 local retry_count=0 local build_success=false while [ $retry_count -lt $max_retries ]; do if go build -o "$SCRIPT_DIR/$binary" . 2>&1; then build_success=true break fi retry_count=$((retry_count + 1)) if [ $retry_count -lt $max_retries ]; then echo -e "${YELLOW}⚠️ [$name] 编译失败,${retry_count}/${max_retries} 次重试...${NC}" sleep 1 fi done if [ "$build_success" = false ]; then echo -e "${RED}❌ [$name] 编译失败 (已重试 ${max_retries} 次),旧进程保持运行${NC}" echo -e "${RED} 查看详细日志: tail -f /tmp/${name}.log${NC}" rm -f "$lock_file" return 1 fi echo -e "${GREEN}✅ [$name] 编译成功${NC}" # Step 2: 编译成功后,杀旧进程(从 PID 文件读取) if [ -f "$pid_file" ]; then local old_pid=$(cat "$pid_file") if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then kill "$old_pid" 2>/dev/null || true echo -e "${YELLOW}🛑 [$name] 旧进程 (PID: $old_pid) 已终止${NC}" fi rm -f "$pid_file" fi # Step 3: 启动新进程 sleep 1 cd "$SCRIPT_DIR" # 重新加载服务私有 env(只有给 dev 引入新变量的服务才需要) case "$name" in userService) load_service_env "$name" ;; esac local args=("-port=$port") if [ "$use_db" = "1" ]; then args+=("${DB_ARGS[@]}") fi if [ "$use_redis" = "1" ]; then args+=("${REDIS_ARGS[@]}") fi "$SCRIPT_DIR/$binary" "${args[@]}" > "/tmp/${name}.log" 2>&1 & local new_pid=$! echo $new_pid > "$pid_file" sleep 2 if ps -p $new_pid > /dev/null 2>&1; then echo -e "${GREEN}✅ [$name] 已重启 (PID: $new_pid)${NC}" else echo -e "${RED}❌ [$name] 重启失败,查看日志: tail -f /tmp/${name}.log${NC}" fi # 解锁 rm -f "$lock_file" } # 启动文件监听器 # 用法: start_watcher name dir1:dir2:dir3 binary port use_db use_redis # 注意: 多个目录用冒号分隔 start_watcher() { local name=$1 local dirs=$2 local binary=$3 local port=$4 local use_db=$5 local use_redis=$6 local watch_paths=() local watch_path="" local restart_marker="/tmp/dev_sh_${name}_restart" # 分割多个目录 IFS=':' read -ra watch_paths <<< "$dirs" for d in "${watch_paths[@]}"; do if [ ! -d "$SCRIPT_DIR/$d" ]; then echo -e "${RED}❌ 监听目录不存在: $SCRIPT_DIR/$d${NC}" return 1 fi done # 清理可能遗留的重启标记 rm -f "$restart_marker" ( if [[ "$(uname)" == "Darwin" ]]; then # 排除: .git目录, 测试文件, 二进制文件(无扩展名), .exe, bin/目录 local exclude_args="" for d in "${watch_paths[@]}"; do if [[ "$d" == "pkg/proto"* ]] || [[ "$d" == "proto"* ]]; then # proto 目录监听所有 proto 文件变化 fswatch -r "$SCRIPT_DIR/$d" \ --exclude='\.git' \ --exclude='_test\.go$' \ --exclude='\.exe$' & else fswatch -r "$SCRIPT_DIR/$d" \ --exclude='\.git' \ --exclude='_test\.go$' \ --exclude='\.exe$' \ --exclude='gateway$' \ --exclude='userService$' \ --exclude='assetService$' \ --exclude='socialService$' \ --exclude='galleryService$' \ --exclude='activityService$' \ --exclude='taskService$' \ --exclude='starbookService$' \ --exclude='aiChatService$' & fi done else inotifywait -r -m -e modify,create,write "${watch_paths[@]}" \ --exclude='\.git' \ --exclude='_test\.go$' \ --exclude='\.exe$' \ --exclude='gateway$' \ --exclude='userService$' \ --exclude='assetService$' \ --exclude='socialService$' \ --exclude='galleryService$' \ --exclude='activityService$' \ --exclude='taskService$' \ --exclude='starbookService$' \ --exclude='aiChatService$' & fi wait ) & local watcher_pid=$! echo "$watcher_pid" >> /tmp/dev_sh_watchers.tmp echo -e "${GREEN}👁️ [$name] 文件监听器已启动 (PID: $watcher_pid)${NC}" # 后台防抖循环:每 300ms 检查一次是否需要重启 ( while true; do sleep 0.3 if [ ! -f "$restart_marker" ]; then continue fi local now if [[ "$(uname)" == "Darwin" ]] || [[ "$(uname)" =~ ^MINGW ]] || [[ "$(uname)" =~ ^MSYS ]] || [[ "$(uname)" =~ ^CYGWIN ]]; then now=$(python3 -c 'import time; print(int(time.time()*1e9))') else now=$(date +%s%N) fi local last_time=$(cat "$restart_marker" 2>/dev/null || echo 0) local elapsed=$((now - last_time)) if (( elapsed >= 500000000 )); then # 距上次事件已过 300ms,执行重启 rm -f "$restart_marker" # 检查是否包含 proto 目录变化,包含则先重新编译 proto local proto_changed=false for d in "${watch_paths[@]}"; do if [[ "$d" == "pkg/proto"* ]] || [[ "$d" == "proto"* ]]; then proto_changed=true break fi done if [ "$proto_changed" = true ]; then echo -e "${YELLOW}🔄 [Proto 文件变化] 重新编译 Proto...${NC}" build_proto # proto 变化时重启 gateway(gateway 是 proto 客户端的调用方) echo -e "${YELLOW}🔄 [Proto 变化] 重启 gateway...${NC}" restart_service "gateway" "gateway" "gateway/gateway" "8080" "0" "0" fi restart_service "$name" "${watch_paths[0]}" "$binary" "$port" "$use_db" "$use_redis" fi done ) & local debounce_pid=$! echo "$debounce_pid" >> /tmp/dev_sh_watchers.tmp } echo -e "${GREEN}========================================${NC}" echo -e "${GREEN} TopFans Backend 热更新开发模式${NC}" echo -e "${GREEN}========================================${NC}" echo "" echo -e "${YELLOW}数据库: ${DB_USER}@${DB_HOST}:${DB_PORT}/${DB_NAME}${NC}" echo -e "${YELLOW}文件监听器: $WATCHER_TOOL${NC}" echo "" # 初始化监听器 PID 列表 > /tmp/dev_sh_watchers.tmp # 清理残留 PID 文件(上次非正常退出可能留下) for service in activityService galleryService socialService assetService userService taskService gateway starbookService aiChatService; do rm -f "/tmp/dev_sh_${service}.pid" "/tmp/dev_sh_${service}_restart" done # 停止现有服务(清理环境) echo -e "${YELLOW}🛑 停止现有服务...${NC}" for service in gateway userService socialService assetService galleryService activityService taskService starbookService aiChatService; do pkill -9 -f "$service" 2>/dev/null || true done sleep 1 # 编译 proto 文件 build_proto() { echo -e "${YELLOW}📦 编译 Proto 文件...${NC}" if [ -f "$SCRIPT_DIR/scripts/compile-proto.sh" ]; then bash "$SCRIPT_DIR/scripts/compile-proto.sh" else echo -e "${RED}❌ compile-proto.sh 不存在,跳过${NC}" fi } # 先编译 proto 文件 echo "" echo -e "${YELLOW}🔨 预编译 Proto 文件...${NC}" build_proto # 先构建所有服务 echo "" echo -e "${YELLOW}🔨 预编译所有服务...${NC}" build_service "gateway" "gateway" "gateway/gateway" build_service "userService" "services/userService" "services/userService/userService" build_service "assetService" "services/assetService" "services/assetService/assetService" build_service "socialService" "services/socialService" "services/socialService/socialService" build_service "galleryService" "services/galleryService" "services/galleryService/galleryService" build_service "activityService" "services/activityService" "services/activityService/activityService" build_service "taskService" "services/taskService" "services/taskService/taskService" build_service "starbookService" "services/starbookService" "services/starbookService/starbookService" build_service "aiChatService" "services/aiChatService" "services/aiChatService/aiChatService" cd "$SCRIPT_DIR" # 启动所有服务 echo "" echo -e "${YELLOW}🚀 启动所有服务...${NC}" start_service "userService" "services/userService/userService" 20000 1 1 start_service "assetService" "services/assetService/assetService" 20003 1 0 start_service "socialService" "services/socialService/socialService" 20002 1 0 # galleryService 需要连接 taskService (20006),单独处理 echo -e "${GREEN}🚀 启动 galleryService...${NC}" "$SCRIPT_DIR/services/galleryService/galleryService" -port=20004 -task-service-url="tri://localhost:20006" "${DB_ARGS[@]}" > "/tmp/galleryService.log" 2>&1 & echo $! > "/tmp/dev_sh_galleryService.pid" sleep 2 echo -e "${GREEN}✅ galleryService 已启动 (PID: $(cat /tmp/dev_sh_galleryService.pid), 端口: 20004)${NC}" start_service "activityService" "services/activityService/activityService" 20005 1 0 start_service "taskService" "services/taskService/taskService" 20006 1 0 start_service "starbookService" "services/starbookService/starbookService" 20007 1 0 start_service "aiChatService" "services/aiChatService/aiChatService" 20008 1 1 start_service "gateway" "gateway/gateway" 8080 0 0 # 启动所有文件监听器 echo "" echo -e "${YELLOW}👁️ 启动所有文件监听器...${NC}" start_watcher "gateway" "gateway:pkg/proto" "gateway/gateway" 8080 0 0 start_watcher "userService" "services/userService" "services/userService/userService" 20000 1 1 start_watcher "assetService" "services/assetService:pkg/proto/asset" "services/assetService/assetService" 20003 1 0 start_watcher "socialService" "services/socialService" "services/socialService/socialService" 20002 1 0 start_watcher "galleryService" "services/galleryService" "services/galleryService/galleryService" 20004 1 0 start_watcher "activityService" "services/activityService" "services/activityService/activityService" 20005 1 0 start_watcher "taskService" "services/taskService" "services/taskService/taskService" 20006 1 0 start_watcher "starbookService" "services/starbookService" "services/starbookService/starbookService" 20007 1 0 start_watcher "aiChatService" "services/aiChatService:pkg/proto" "services/aiChatService/aiChatService" 20008 1 1 echo "" echo -e "${GREEN}========================================${NC}" echo -e "${GREEN} 热更新开发模式已启动!${NC}" echo -e "${GREEN}========================================${NC}" echo "" echo -e "${YELLOW}服务地址:${NC}" echo " - Gateway: http://localhost:8080" echo " - Swagger UI: http://localhost:8080/swagger/index.html" echo " - User Service: tri://localhost:20000" echo " - Social Service: tri://localhost:20002" echo " - Asset Service: tri://localhost:20003" echo " - Gallery Service: tri://localhost:20004" echo " - Activity Service: tri://localhost:20005" echo " - Task Service: tri://localhost:20006" echo " - Starbook Service: tri://localhost:20007" echo "" echo -e "${YELLOW}按 Ctrl+C 停止所有服务${NC}" echo "" # 保持脚本运行 wait