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

500 lines
15 KiB
Markdown
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.

# 热更新开发脚本实现计划 (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` 文件头部**
```bash
#!/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: 检测依赖工具**
```bash
# 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**
```bash
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**
```bash
git add backend/dev.sh
git commit -m "feat: add dev.sh main framework header"
```
---
## Task 2: 服务构建与启动函数
- [ ] **Step 1: 添加 `start_service` 函数**
```bash
# 启动一个服务
# 用法: 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` 函数**
```bash
# 构建一个服务(在服务目录下执行 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**
```bash
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=$!` 之后,添加:
```bash
# 保存 PID 到文件
echo $pid > "/tmp/dev_sh_${name}.pid"
```
- [ ] **Step 2: 添加 `restart_service` 函数单服务重建重启PID 文件追踪)**
```bash
# 重建并重启单个服务(构建成功后才杀旧进程)
# 用法: 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**
```bash
# 启动文件监听器
# 用法: 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**
```bash
git add backend/dev.sh
git commit -m "feat: add restart_service and start_watcher functions"
```
---
## Task 4: 主启动流程
- [ ] **Step 1: 添加主启动流程**
```bash
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**
```bash
git add backend/dev.sh
git commit -m "feat: add main startup flow in dev.sh"
```
---
## Task 5: 信号捕获与优雅退出
- [ ] **Step 1: 在脚本头部set -e 之后)添加 trap**
```bash
# 信号捕获:优雅退出
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**
```bash
git add backend/dev.sh
git commit -m "feat: add signal trap and graceful shutdown to dev.sh"
```
---
## Task 6: 整体测试
- [ ] **Step 1: 添加执行权限并检查脚本语法**
```bash
chmod +x backend/dev.sh
bash -n backend/dev.sh # 语法检查,输出为空表示无语法错误
```
- [ ] **Step 2: 检查 fswatch 是否可用(如未安装给出提示)**
```bash
if command -v fswatch &> /dev/null; then
echo "fswatch 已安装: $(fswatch --version | head -1)"
else
echo "fswatch 未安装Mac 用户请运行: brew install fswatch"
fi
```
- [ ] **Step 3: Commit**
```bash
git add backend/dev.sh
git commit -m "chore: add execute permission and validation to dev.sh"
```
---
## 完成后验证
脚本创建完成后,运行以下命令验证:
```bash
# 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
```