topfans/docs/superpowers/plans/2026-04-13-hot-reload-dev-script-plan.md
zerosaturation 2de9e1dd57 docs: fix hot-reload plan - timestamp debounce, PID files, cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 12:09:34 +08:00

15 KiB
Raw Blame History

热更新开发脚本实现计划 (dev.sh)

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

目标: 创建 dev.sh 脚本,监听后端各服务目录的 .go 文件变更,自动重新编译并重启对应服务。

架构: 每个服务一个独立的文件监听器进程,监控该服务目录下的所有 .go 文件。检测到变更后,通过 300ms 防抖机制避免频繁重建,编译成功后才终止旧进程并启动新进程,确保服务不中断。

技术栈: Bash 脚本 + fswatch (Mac) / inotifywait (Linux)


文件结构

backend/
└── dev.sh   # 新增:热更新开发脚本

dev.sh 是唯一新增的文件,所有逻辑都在这个脚本中实现。


任务总览

任务 内容
Task 1 编写 dev.sh 主框架(检测依赖工具、加载 .env、解析参数
Task 2 实现服务启动函数build + start + log
Task 3 实现文件监听器函数fswatch/inotifywait + 防抖 + 重启逻辑)
Task 4 实现主启动流程(按顺序启动所有服务 + 启动所有监听器)
Task 5 实现信号捕获与优雅退出trap SIGINT/SIGTERM
Task 6 测试脚本是否能正常运行

Task 1: dev.sh 主框架

文件:

  • 新增: backend/dev.sh

  • Step 1: 创建 backend/dev.sh 文件头部

#!/bin/bash

# Hot-Reload Dev Script for TopFans Backend
# Usage: ./dev.sh
#
# Requires: fswatch (Mac: brew install fswatch) or inotifywait (Linux)

set -e

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"
  • Step 2: 检测依赖工具
# 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"
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"
    else
        echo "安装方法: sudo apt install inotify-tools  (Debian/Ubuntu)"
        echo "         sudo yum install inotify-tools    (CentOS/RHEL)"
    fi
    exit 1
fi
  • Step 3: 加载 .env 文件(同 start.sh
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")
  • Step 4: Commit
git add backend/dev.sh
git commit -m "feat: add dev.sh main framework header"

Task 2: 服务构建与启动函数

  • Step 1: 添加 start_service 函数
# 启动一个服务
# 用法: 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=$!

    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
}
  • Step 2: 添加 build_service 函数
# 构建一个服务(在服务目录下执行 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
}
  • Step 3: Commit
git add backend/dev.sh
git commit -m "feat: add build_service and start_service functions"

Task 3: 文件监听器函数

  • Step 1: 修改 start_service 函数(增加 PID 文件写入)

start_service 函数的 local pid=$! 之后,添加:

    # 保存 PID 到文件
    echo $pid > "/tmp/dev_sh_${name}.pid"
  • Step 2: 添加 restart_service 函数单服务重建重启PID 文件追踪)
# 重建并重启单个服务(构建成功后才杀旧进程)
# 用法: 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"

    # Step 1: 编译(先编译,编译成功才杀旧进程)
    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 "${RED}❌ [$name] 编译失败,旧进程保持运行${NC}"
        echo -e "${RED}   查看详细日志: tail -f /tmp/${name}.log${NC}"
        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
}
  • Step 3: 添加 start_watcher 函数(时间戳防抖,无 pipe race
# 启动文件监听器
# 用法: start_watcher name dir binary port use_db
start_watcher() {
    local name=$1
    local dir=$2
    local binary=$3
    local port=$4
    local use_db=$5
    local watch_path="$SCRIPT_DIR/$dir"
    local restart_marker="/tmp/dev_sh_${name}_restart"

    if [ ! -d "$watch_path" ]; then
        echo -e "${RED}❌ 监听目录不存在: $watch_path${NC}"
        return 1
    fi

    # 清理可能遗留的重启标记
    rm -f "$restart_marker"

    (
        if [[ "$(uname)" == "Darwin" ]]; then
            fswatch -r "$watch_path" --exclude='\.git' --exclude='_test\.go$'
        else
            inotifywait -r -m -e modify,create,write "$watch_path" --exclude='\.git' --exclude='_test\.go$'
        fi | while read event; do
            # 时间戳防抖每次事件更新标记文件restart_trigger.sh 负责延迟执行
            date +%s%N > "$restart_marker"
        done
    ) &
    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=$(date +%s%N)
            local last_time=$(cat "$restart_marker" 2>/dev/null || echo 0)
            local elapsed=$((now - last_time))
            if (( elapsed >= 300000000 )); then
                # 距上次事件已过 300ms执行重启
                rm -f "$restart_marker"
                restart_service "$name" "$dir" "$binary" "$port" "$use_db"
            fi
        done
    ) &
    local debounce_pid=$!
    echo "$debounce_pid" >> /tmp/dev_sh_watchers.tmp
}
  • Step 4: Commit
git add backend/dev.sh
git commit -m "feat: add restart_service and start_watcher functions"

Task 4: 主启动流程

  • Step 1: 添加主启动流程
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 gateway; 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; do
    pkill -9 -f "$service" 2>/dev/null || true
done
sleep 1

# 先构建所有服务
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"
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
start_service "galleryService" "services/galleryService/galleryService"   20004  1
start_service "activityService" "services/activityService/activityService" 20005  1
start_service "gateway"        "gateway/gateway"                          8080  0

# 启动所有文件监听器
echo ""
echo -e "${YELLOW}👁️  启动所有文件监听器...${NC}"
start_watcher "gateway"        "gateway"              "gateway/gateway"        8080  0
start_watcher "userService"    "services/userService"    "services/userService/userService"    20000  1
start_watcher "assetService"   "services/assetService"   "services/assetService/assetService"   20003  1
start_watcher "socialService"  "services/socialService"  "services/socialService/socialService" 20002  1
start_watcher "galleryService" "services/galleryService" "services/galleryService/galleryService" 20004  1
start_watcher "activityService" "services/activityService" "services/activityService/activityService" 20005  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 ""
echo -e "${YELLOW}按 Ctrl+C 停止所有服务${NC}"
echo ""

# 保持脚本运行
wait
  • Step 2: Commit
git add backend/dev.sh
git commit -m "feat: add main startup flow in dev.sh"

Task 5: 信号捕获与优雅退出

  • Step 1: 在脚本头部set -e 之后)添加 trap
# 信号捕获:优雅退出
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 activityService galleryService socialService assetService userService gateway; do
        pkill -9 -f "$service" 2>/dev/null || true
        rm -f "/tmp/dev_sh_${service}.pid" "/tmp/dev_sh_${service}_restart"
        echo -e "${YELLOW}  🛑 $service 已停止${NC}"
    done

    echo -e "${GREEN}✅ 所有服务已关闭${NC}"
    exit 0
}

trap cleanup SIGINT SIGTERM
  • Step 2: 将 trap 代码移到 set -e 之后Task 1 的 Step 1 中添加)

在文件头部的 set -e 之后、SCRIPT_DIR= 之前插入 trap 代码。

  • Step 3: Commit
git add backend/dev.sh
git commit -m "feat: add signal trap and graceful shutdown to dev.sh"

Task 6: 整体测试

  • Step 1: 添加执行权限并检查脚本语法
chmod +x backend/dev.sh
bash -n backend/dev.sh  # 语法检查,输出为空表示无语法错误
  • Step 2: 检查 fswatch 是否可用(如未安装给出提示)
if command -v fswatch &> /dev/null; then
    echo "fswatch 已安装: $(fswatch --version | head -1)"
else
    echo "fswatch 未安装Mac 用户请运行: brew install fswatch"
fi
  • Step 3: Commit
git add backend/dev.sh
git commit -m "chore: add execute permission and validation to dev.sh"

完成后验证

脚本创建完成后,运行以下命令验证:

# 1. 语法检查
bash -n backend/dev.sh

# 2. 查看帮助信息
./backend/dev.sh --help 2>&1 | head -20

# 3. (如果有 fswatch后台启动并检查进程
./backend/dev.sh &
sleep 5
ps aux | grep -E "(dev.sh|fswatch|gateway|userService)" | grep -v grep