Files
live-hub-py/dev.sh
T

260 lines
7.6 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# ===== 本地调试启动脚本:后端 reload + 前端热更新 =====
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT_DIR"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
# 调试模式复用 Docker 部署的环境变量,尤其是 JWT 和敏感字段加密密钥。
# shellcheck disable=SC1091
source "$ROOT_DIR/.env"
set +a
fi
BACKEND_HOST="${BACKEND_HOST:-0.0.0.0}"
BACKEND_PORT="${BACKEND_PORT:-8800}"
FRONTEND_HOST="${FRONTEND_HOST:-0.0.0.0}"
FRONTEND_PORT="${FRONTEND_PORT:-5174}"
LOG_LEVEL="${LOG_LEVEL:-DEBUG}"
MYSQL_IMAGE="${MYSQL_IMAGE:-mysql:8.4}"
MYSQL_BIND_HOST="${MYSQL_BIND_HOST:-127.0.0.1}"
MYSQL_HOST_PORT="${MYSQL_HOST_PORT:-3307}"
MYSQL_DATABASE="${MYSQL_DATABASE:-douyu_login}"
MYSQL_USER="${MYSQL_USER:-douyu_login}"
DEV_DB_HOST="${DEV_DB_HOST:-127.0.0.1}"
DEV_DB_PORT="${DEV_DB_PORT:-$MYSQL_HOST_PORT}"
DB_HOST="$DEV_DB_HOST"
DB_PORT="$DEV_DB_PORT"
DB_NAME="${DB_NAME:-$MYSQL_DATABASE}"
DB_USER="${DB_USER:-$MYSQL_USER}"
DB_PASSWORD="${DB_PASSWORD:-${MYSQL_PASSWORD:-}}"
DB_POOL_SIZE="${DB_POOL_SIZE:-20}"
DB_MAX_OVERFLOW="${DB_MAX_OVERFLOW:-20}"
DB_POOL_TIMEOUT="${DB_POOL_TIMEOUT:-30}"
DB_POOL_RECYCLE="${DB_POOL_RECYCLE:-1800}"
YYB_WORKER_PORT="${YYB_WORKER_PORT:-8810}"
YYB_WORKER_IMAGE="${YYB_WORKER_IMAGE:-douyu-yyb-worker:local}"
DEV_YYB_WORKER_URL="${DEV_YYB_WORKER_URL:-http://127.0.0.1:${YYB_WORKER_PORT}}"
YYB_WORKER_CONTAINER="${YYB_WORKER_CONTAINER:-douyu-yyb-worker-dev}"
YYB_WORKER_DATA_DIR="${YYB_WORKER_DATA_DIR:-$ROOT_DIR/data/yyb-worker-dev}"
LOG_DIR="$ROOT_DIR/logs"
LOG_DATE="$(date +%Y-%m-%d)"
BACKEND_LOG="$LOG_DIR/backend-$LOG_DATE.log"
FRONTEND_LOG="$LOG_DIR/frontend-$LOG_DATE.log"
WORKER_LOG="$LOG_DIR/yyb-worker-$LOG_DATE.log"
BACKEND_PID=""
FRONTEND_PID=""
WORKER_LOG_PID=""
WORKER_STARTED_BY_DEV=false
stop_tree() {
local pid="$1"
local children
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
return
fi
children="$(pgrep -P "$pid" 2>/dev/null || true)"
for child in $children; do
stop_tree "$child"
done
kill "$pid" 2>/dev/null || true
}
cleanup() {
trap - EXIT INT TERM
stop_tree "$FRONTEND_PID"
stop_tree "$BACKEND_PID"
stop_tree "$WORKER_LOG_PID"
if [ "$WORKER_STARTED_BY_DEV" = true ]; then
docker stop "$YYB_WORKER_CONTAINER" >/dev/null 2>&1 || true
fi
}
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "缺少命令: $1"
exit 1
fi
}
require_cmd uv
require_cmd npm
require_cmd docker
detect_compose() {
if docker compose version >/dev/null 2>&1; then
echo "docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
echo "docker-compose"
else
echo ""
fi
}
detect_backend_proxy_host() {
if [ "$BACKEND_HOST" != "0.0.0.0" ] && [ "$BACKEND_HOST" != "::" ]; then
echo "$BACKEND_HOST"
return
fi
# 绑定 0.0.0.0 时,Vite 代理不要固定连 127.0.0.1;某些本地工具会抢占 loopback 端口。
local ip
ip="$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true)"
if [ -z "$ip" ] && command -v hostname >/dev/null 2>&1; then
ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
fi
echo "${ip:-127.0.0.1}"
}
if [ -n "${DATABASE_URL:-}" ] && [[ "$DATABASE_URL" != mysql+* ]]; then
echo "dev.sh 只支持 MySQL,请移除 SQLite DATABASE_URL: $DATABASE_URL"
exit 1
fi
if [ -z "${MYSQL_PASSWORD:-}" ] || [ -z "${MYSQL_ROOT_PASSWORD:-}" ]; then
echo "请先在 .env 中设置 MYSQL_PASSWORD 和 MYSQL_ROOT_PASSWORD"
exit 1
fi
if [ -z "$DB_PASSWORD" ]; then
echo "请先在 .env 中设置 DB_PASSWORD 或 MYSQL_PASSWORD"
exit 1
fi
if [ -z "${YYB_WORKER_KEY:-}" ]; then
echo "请先在 .env 中设置 YYB_WORKER_KEY"
exit 1
fi
COMPOSE=$(detect_compose)
if [ -z "$COMPOSE" ]; then
echo "缺少 docker compose"
exit 1
fi
BACKEND_PROXY_HOST="${BACKEND_PROXY_HOST:-$(detect_backend_proxy_host)}"
BACKEND_PROXY_TARGET="${VITE_BACKEND_TARGET:-http://${BACKEND_PROXY_HOST}:${BACKEND_PORT}}"
mkdir -p data "$LOG_DIR"
mkdir -p "$YYB_WORKER_DATA_DIR"
trap cleanup EXIT INT TERM
start_yyb_worker() {
if docker ps --format '{{.Names}}' | grep -qx "$YYB_WORKER_CONTAINER"; then
echo "本地 YYB Worker 已运行: ${DEV_YYB_WORKER_URL}"
return
fi
if docker ps -a --format '{{.Names}}' | grep -qx "$YYB_WORKER_CONTAINER"; then
echo "发现已停止的本地 YYB Worker,正在清理旧容器..."
docker rm "$YYB_WORKER_CONTAINER" >/dev/null
fi
echo "正在构建本地 YYB Worker..."
YYB_WORKER_KEY="$YYB_WORKER_KEY" $COMPOSE build yyb-worker
echo "正在启动本地 YYB Worker..."
docker run -d --rm \
--name "$YYB_WORKER_CONTAINER" \
-p "127.0.0.1:${YYB_WORKER_PORT}:8810" \
-e TZ="${TZ:-Asia/Shanghai}" \
-e YYB_WORKER_KEY="$YYB_WORKER_KEY" \
-v "$YYB_WORKER_DATA_DIR:/app/config/worker-jobs" \
"$YYB_WORKER_IMAGE" >/dev/null
WORKER_STARTED_BY_DEV=true
echo "等待本地 YYB Worker 就绪..."
for i in $(seq 1 15); do
if curl -fsS "${DEV_YYB_WORKER_URL}/health" >/dev/null 2>&1; then
echo "YYB Worker 已就绪"
return
fi
if [ "$i" = "15" ]; then
echo "YYB Worker 启动超时,查看日志:"
docker logs "$YYB_WORKER_CONTAINER" || true
exit 1
fi
sleep 1
done
}
stream_yyb_worker_logs() {
# docker logs 默认只输出到终端;持续转存后保留 Worker 的异常和协议脚本输出。
(docker logs --tail 100 --follow "$YYB_WORKER_CONTAINER" 2>&1 | tee -a "$WORKER_LOG") &
WORKER_LOG_PID=$!
}
start_yyb_worker
stream_yyb_worker_logs
echo "正在启动本地 MySQL..."
MYSQL_IMAGE="$MYSQL_IMAGE" \
MYSQL_BIND_HOST="$MYSQL_BIND_HOST" \
MYSQL_HOST_PORT="$MYSQL_HOST_PORT" \
$COMPOSE up -d mysql
echo "等待 MySQL 就绪..."
for i in $(seq 1 30); do
if $COMPOSE exec -T mysql sh -c 'mysqladmin ping -h localhost -uroot -p"$MYSQL_ROOT_PASSWORD" --silent' >/dev/null 2>&1; then
echo "MySQL 已就绪"
break
fi
if [ "$i" = "30" ]; then
echo "MySQL 启动超时,查看日志:"
$COMPOSE logs --tail 50 mysql
exit 1
fi
sleep 2
done
echo ""
echo "=============================="
echo " 本地调试模式"
echo "=============================="
echo " 后端: http://${BACKEND_HOST}:${BACKEND_PORT}"
echo " 前端: http://localhost:${FRONTEND_PORT}"
echo " API 代理: ${BACKEND_PROXY_TARGET}"
echo " MySQL: ${DB_HOST}:${DB_PORT}/${DB_NAME}"
echo " YYB Worker: ${DEV_YYB_WORKER_URL}"
echo " 后端日志: ${BACKEND_LOG}"
echo " 前端日志: ${FRONTEND_LOG}"
echo " Worker 日志: ${WORKER_LOG}"
echo " 退出: Ctrl+C"
echo "=============================="
echo ""
(
export DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD
export DB_POOL_SIZE DB_MAX_OVERFLOW DB_POOL_TIMEOUT DB_POOL_RECYCLE
export YYB_WORKER_KEY LOG_LEVEL
export YYB_WORKER_URL="$DEV_YYB_WORKER_URL"
uv run uvicorn web.backend.main:app \
--host "$BACKEND_HOST" \
--port "$BACKEND_PORT" \
--reload \
--reload-dir core \
--reload-dir utils \
--reload-dir web/backend 2>&1 | tee -a "$BACKEND_LOG"
) &
BACKEND_PID=$!
(
cd web/frontend
VITE_BACKEND_TARGET="$BACKEND_PROXY_TARGET" \
npm run dev -- --host "$FRONTEND_HOST" --port "$FRONTEND_PORT" 2>&1 | tee -a "$FRONTEND_LOG"
) &
FRONTEND_PID=$!
while true; do
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
wait "$BACKEND_PID"
exit $?
fi
if ! kill -0 "$FRONTEND_PID" 2>/dev/null; then
wait "$FRONTEND_PID"
exit $?
fi
sleep 1
done