topfans/backend/dev.sh
2026-05-17 19:03:34 +08:00

435 lines
16 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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; 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:-5432}"
DB_USER="${DB_USER:-haihuizhu}"
DB_PASSWORD="${DB_PASSWORD:-admin}"
DB_NAME="${DB_NAME:-top-fans}"
DB_ARGS=(-db-host="$DB_HOST" -db-port="$DB_PORT" -db-user="$DB_USER" -db-password="$DB_PASSWORD" -db-name="$DB_NAME")
# 启动一个服务
# 用法: start_service name binary port use_db
start_service() {
local name=$1
local binary=$2
local port=$3
local use_db=$4
echo -e "${GREEN}🚀 启动 $name...${NC}"
if [ "$use_db" = "1" ]; then
"$SCRIPT_DIR/$binary" -port=$port "${DB_ARGS[@]}" > "/tmp/${name}.log" 2>&1 &
else
"$SCRIPT_DIR/$binary" -port=$port > "/tmp/${name}.log" 2>&1 &
fi
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
restart_service() {
local name=$1
local dir=$2
local binary=$3
local port=$4
local use_db=$5
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"
if [ "$use_db" = "1" ]; then
"$SCRIPT_DIR/$binary" -port=$port "${DB_ARGS[@]}" > "/tmp/${name}.log" 2>&1 &
else
"$SCRIPT_DIR/$binary" -port=$port > "/tmp/${name}.log" 2>&1 &
fi
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
# 注意: 多个目录用冒号分隔
start_watcher() {
local name=$1
local dirs=$2
local binary=$3
local port=$4
local use_db=$5
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$' &
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$' &
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 变化时重启 gatewaygateway 是 proto 客户端的调用方)
echo -e "${YELLOW}🔄 [Proto 变化] 重启 gateway...${NC}"
restart_service "gateway" "gateway" "gateway/gateway" "8080" "0"
fi
restart_service "$name" "${watch_paths[0]}" "$binary" "$port" "$use_db"
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; 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; 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"
cd "$SCRIPT_DIR"
# 启动所有服务
echo ""
echo -e "${YELLOW}🚀 启动所有服务...${NC}"
start_service "userService" "services/userService/userService" 20000 1
start_service "assetService" "services/assetService/assetService" 20003 1
start_service "socialService" "services/socialService/socialService" 20002 1
# 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
start_service "taskService" "services/taskService/taskService" 20006 1
start_service "starbookService" "services/starbookService/starbookService" 20007 1
start_service "gateway" "gateway/gateway" 8080 0
# 启动所有文件监听器
echo ""
echo -e "${YELLOW}👁️ 启动所有文件监听器...${NC}"
start_watcher "gateway" "gateway:pkg/proto" "gateway/gateway" 8080 0
start_watcher "userService" "services/userService" "services/userService/userService" 20000 1
start_watcher "assetService" "services/assetService:pkg/proto/asset" "services/assetService/assetService" 20003 1
start_watcher "socialService" "services/socialService" "services/socialService/socialService" 20002 1
start_watcher "activityService" "services/activityService" "services/activityService/activityService" 20005 1
start_watcher "taskService" "services/taskService" "services/taskService/taskService" 20006 1
start_watcher "starbookService" "services/starbookService" "services/starbookService/starbookService" 20007 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