From bf2c4a172d25ac5de3714b62966cacc666f7c8e8 Mon Sep 17 00:00:00 2001 From: linkong Date: Fri, 27 Mar 2026 11:13:01 +0800 Subject: [PATCH] fix: upgrade startup script controls --- VERSION | 2 +- backend/app/db/session.py | 24 +++ docs/CHANGELOG.md | 24 +++ docs/version-history.md | 3 +- frontend/package.json | 2 +- kill_port.sh | 40 ---- planet.sh | 380 +++++++++++++++++++++++++++++++++++--- 7 files changed, 402 insertions(+), 73 deletions(-) delete mode 100755 kill_port.sh diff --git a/VERSION b/VERSION index 59dad104..16eb94e7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.21.2 +0.21.3 diff --git a/backend/app/db/session.py b/backend/app/db/session.py index 4d3ccf69..82c81dbd 100644 --- a/backend/app/db/session.py +++ b/backend/app/db/session.py @@ -60,6 +60,28 @@ async def seed_default_datasources(session: AsyncSession): await session.commit() +async def ensure_default_admin_user(session: AsyncSession): + from app.core.security import get_password_hash + from app.models.user import User + + result = await session.execute( + text("SELECT id FROM users WHERE username = 'admin'") + ) + if result.fetchone(): + return + + session.add( + User( + username="admin", + email="admin@planet.local", + password_hash=get_password_hash("admin123"), + role="super_admin", + is_active=True, + ) + ) + await session.commit() + + async def init_db(): import app.models.user # noqa: F401 import app.models.gpu_cluster # noqa: F401 @@ -68,6 +90,7 @@ async def init_db(): import app.models.datasource # noqa: F401 import app.models.datasource_config # noqa: F401 import app.models.alert # noqa: F401 + import app.models.bgp_anomaly # noqa: F401 import app.models.collected_data # noqa: F401 import app.models.system_setting # noqa: F401 @@ -125,3 +148,4 @@ async def init_db(): async with async_session_factory() as session: await seed_default_datasources(session) + await ensure_default_admin_user(session) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9f2c21e9..3c7dc926 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,30 @@ This project follows the repository versioning rule: - `feature` -> `+0.1.0` - `bugfix` -> `+0.0.1` +## 0.21.3 + +Released: 2026-03-27 + +### Highlights + +- Upgraded the startup script into a more resilient local control entrypoint with retry-based service boot, selective restart targeting, and guided CLI user creation. +- Reduced friction when developing across slower machines by making backend and frontend startup checks more tolerant and operator-friendly. + +### Added + +- Added interactive `createuser` support to [planet.sh](/home/ray/dev/linkong/planet/planet.sh) for CLI-driven username, email, password, and admin-role creation. + +### Improved + +- Improved `start` and `restart` in [planet.sh](/home/ray/dev/linkong/planet/planet.sh) with optional backend/frontend port targeting and on-demand port cleanup. +- Improved startup robustness with repeated health checks and automatic retry loops for both backend and frontend services. +- Improved restart ergonomics so `restart -b` and `restart -f` can restart only the requested service instead of forcing a full stack restart. + +### Fixed + +- Fixed false startup failures on slower environments where services needed longer than a single fixed wait window to become healthy. +- Fixed first-run login dead-end by ensuring a default admin user is created during backend initialization when the database has no users. + ## 0.21.2 Released: 2026-03-26 diff --git a/docs/version-history.md b/docs/version-history.md index 920afb37..55587564 100644 --- a/docs/version-history.md +++ b/docs/version-history.md @@ -16,7 +16,7 @@ ## Current Version - `main` 当前主线历史推导到:`0.16.5` -- `dev` 当前开发分支历史推导到:`0.21.1` +- `dev` 当前开发分支历史推导到:`0.21.3` ## Timeline @@ -65,6 +65,7 @@ | `0.21.0` | feature | `dev` | `pending` | add Earth inertial drag, sync hover/trail state, and support unlimited satellite loading | | `0.21.1` | bugfix | `dev` | `pending` | polish Earth toolbar controls, icons, and loading copy | | `0.21.2` | bugfix | `dev` | `pending` | redesign Earth HUD with liquid-glass controls, dynamic legend switching, and info-card interaction polish | +| `0.21.3` | bugfix | `dev` | `30a29a6e` | harden `planet.sh` startup controls, add selective restart and interactive user creation | ## Maintenance Commits Not Counted as Version Bumps diff --git a/frontend/package.json b/frontend/package.json index 64412908..ad45e1c5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "planet-frontend", - "version": "0.21.0", + "version": "0.21.3", "private": true, "dependencies": { "@ant-design/icons": "^5.2.6", diff --git a/kill_port.sh b/kill_port.sh deleted file mode 100755 index 242f56dd..00000000 --- a/kill_port.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# kill_port.sh - 检测并杀掉占用指定端口的进程 - -PORT=${1:-8000} - -echo "==========================================" -echo "端口检测与清理脚本" -echo "==========================================" -echo "目标端口: $PORT" -echo "" - -# 检测并杀掉占用端口的进程 -echo "[1/3] 检测端口 $PORT ..." -if fuser $PORT/tcp >/dev/null 2>&1; then - echo " ✓ 发现占用端口 $PORT 的进程:" - fuser -v $PORT/tcp 2>&1 - - echo "" - echo "[2/3] 正在终止进程..." - fuser -k $PORT/tcp 2>&1 - echo " ✓ 进程已终止" -else - echo " ✓ 端口 $PORT 未被占用" -fi - -echo "" -echo "[3/3] 清理残留Docker容器..." -EXIT_CONTAINERS=$(docker ps -a | grep Exit | awk '{print $1}') -if [ -n "$EXIT_CONTAINERS" ]; then - echo " 发现残留容器,正在清理..." - echo "$EXIT_CONTAINERS" | xargs -r docker rm -f - echo " ✓ 残留容器已清理" -else - echo " ✓ 无残留容器" -fi - -echo "" -echo "==========================================" -echo "完成!" -echo "==========================================" diff --git a/planet.sh b/planet.sh index 217ad484..ab1bc899 100755 --- a/planet.sh +++ b/planet.sh @@ -11,6 +11,15 @@ YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' +BACKEND_MAX_RETRIES="${BACKEND_MAX_RETRIES:-3}" +BACKEND_HEALTH_CHECK_ATTEMPTS="${BACKEND_HEALTH_CHECK_ATTEMPTS:-10}" +BACKEND_HEALTH_CHECK_INTERVAL="${BACKEND_HEALTH_CHECK_INTERVAL:-2}" +FRONTEND_MAX_RETRIES="${FRONTEND_MAX_RETRIES:-3}" +FRONTEND_HEALTH_CHECK_ATTEMPTS="${FRONTEND_HEALTH_CHECK_ATTEMPTS:-10}" +FRONTEND_HEALTH_CHECK_INTERVAL="${FRONTEND_HEALTH_CHECK_INTERVAL:-2}" +DEFAULT_BACKEND_PORT="${DEFAULT_BACKEND_PORT:-8000}" +DEFAULT_FRONTEND_PORT="${DEFAULT_FRONTEND_PORT:-3000}" + ensure_uv_backend_deps() { echo -e "${BLUE}📦 检查后端 uv 环境...${NC}" @@ -53,57 +62,362 @@ ensure_frontend_deps() { fi } -start() { - echo -e "${BLUE}🚀 启动智能星球计划...${NC}" +wait_for_http() { + local url="$1" + local attempts="$2" + local interval="$3" + local service_name="$4" + local attempt=1 + + while [ "$attempt" -le "$attempts" ]; do + if curl -s "$url" > /dev/null 2>&1; then + return 0 + fi + + echo -e "${YELLOW}⏳ 等待${service_name}就绪 (${attempt}/${attempts})...${NC}" + sleep "$interval" + attempt=$((attempt + 1)) + done + + return 1 +} + +start_backend_with_retry() { + local backend_port="$1" + local retry=1 + + while [ "$retry" -le "$BACKEND_MAX_RETRIES" ]; do + pkill -f "uvicorn" 2>/dev/null || true + cd "$SCRIPT_DIR/backend" + PYTHONPATH="$SCRIPT_DIR/backend" nohup uv run --project "$SCRIPT_DIR" python -m uvicorn app.main:app --host 0.0.0.0 --port "$backend_port" --reload > /tmp/planet_backend.log 2>&1 & + BACKEND_PID=$! + + if wait_for_http "http://localhost:${backend_port}/health" "$BACKEND_HEALTH_CHECK_ATTEMPTS" "$BACKEND_HEALTH_CHECK_INTERVAL" "后端"; then + return 0 + fi + + echo -e "${YELLOW}⚠️ 后端第 ${retry}/${BACKEND_MAX_RETRIES} 次启动未就绪,准备重试...${NC}" + kill "$BACKEND_PID" 2>/dev/null || true + retry=$((retry + 1)) + done + + return 1 +} + +start_frontend_with_retry() { + local frontend_port="$1" + local retry=1 + + while [ "$retry" -le "$FRONTEND_MAX_RETRIES" ]; do + pkill -f "vite" 2>/dev/null || true + pkill -f "bun run dev" 2>/dev/null || true + cd "$SCRIPT_DIR/frontend" + nohup bun run dev --port "$frontend_port" > /tmp/planet_frontend.log 2>&1 & + FRONTEND_PID=$! + + if wait_for_http "http://localhost:${frontend_port}" "$FRONTEND_HEALTH_CHECK_ATTEMPTS" "$FRONTEND_HEALTH_CHECK_INTERVAL" "前端"; then + return 0 + fi + + echo -e "${YELLOW}⚠️ 前端第 ${retry}/${FRONTEND_MAX_RETRIES} 次启动未就绪,准备重试...${NC}" + kill "$FRONTEND_PID" 2>/dev/null || true + retry=$((retry + 1)) + done + + return 1 +} + +validate_port() { + local port="$1" + + if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then + echo -e "${RED}❌ 非法端口: ${port}${NC}" + exit 1 + fi +} + +kill_port_if_requested() { + local port="$1" + local service_name="$2" + + echo -e "${YELLOW}🧹 检测 ${service_name} 端口 ${port} 占用...${NC}" + + if command -v fuser >/dev/null 2>&1 && fuser "${port}/tcp" >/dev/null 2>&1; then + echo -e "${BLUE}🔌 发现端口 ${port} 占用,正在终止...${NC}" + fuser -k "${port}/tcp" >/dev/null 2>&1 || true + sleep 1 + return 0 + fi + + if command -v lsof >/dev/null 2>&1; then + local pids + pids="$(lsof -ti tcp:"${port}" 2>/dev/null || true)" + if [ -n "$pids" ]; then + echo -e "${BLUE}🔌 发现端口 ${port} 占用,正在终止...${NC}" + kill $pids 2>/dev/null || true + sleep 1 + return 0 + fi + fi + + echo -e "${GREEN}✅ 端口 ${port} 未被占用${NC}" +} + +parse_service_args() { + BACKEND_PORT="$DEFAULT_BACKEND_PORT" + FRONTEND_PORT="$DEFAULT_FRONTEND_PORT" + BACKEND_PORT_REQUESTED=0 + FRONTEND_PORT_REQUESTED=0 + + while [ "$#" -gt 0 ]; do + case "$1" in + -b|--backend-port) + BACKEND_PORT_REQUESTED=1 + if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then + BACKEND_PORT="$2" + shift 2 + else + shift 1 + fi + ;; + -f|--frontend-port) + FRONTEND_PORT_REQUESTED=1 + if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then + FRONTEND_PORT="$2" + shift 2 + else + shift 1 + fi + ;; + *) + echo -e "${RED}❌ 未知参数: $1${NC}" + exit 1 + ;; + esac + done + + validate_port "$BACKEND_PORT" + validate_port "$FRONTEND_PORT" +} + +cleanup_exit_containers() { + local exit_containers + exit_containers="$(docker ps -a --filter status=exited -q 2>/dev/null || true)" + if [ -n "$exit_containers" ]; then + echo -e "${BLUE}🗑️ 清理残留 Exit 容器...${NC}" + echo "$exit_containers" | xargs -r docker rm -f >/dev/null 2>&1 || true + echo -e "${GREEN}✅ 残留容器已清理${NC}" + fi +} + +stop_backend_service() { + pkill -f "uvicorn" 2>/dev/null || true +} + +stop_frontend_service() { + pkill -f "vite" 2>/dev/null || true + pkill -f "bun run dev" 2>/dev/null || true +} + +start_backend_service() { + local backend_port="$1" + local backend_port_requested="$2" echo -e "${BLUE}🗄️ 启动数据库...${NC}" docker start planet_postgres planet_redis 2>/dev/null || docker-compose up -d postgres redis - sleep 3 + if [ "$backend_port_requested" -eq 1 ]; then + kill_port_if_requested "$backend_port" "后端" + fi + echo -e "${BLUE}🔧 启动后端...${NC}" ensure_uv_backend_deps - pkill -f "uvicorn" 2>/dev/null || true - cd "$SCRIPT_DIR/backend" - PYTHONPATH="$SCRIPT_DIR/backend" nohup uv run --project "$SCRIPT_DIR" python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload > /tmp/planet_backend.log 2>&1 & - BACKEND_PID=$! - - sleep 3 - - if ! curl -s http://localhost:8000/health > /dev/null 2>&1; then - echo -e "${RED}❌ 后端启动失败${NC}" + if ! start_backend_with_retry "$backend_port"; then + echo -e "${RED}❌ 后端启动失败,已重试 ${BACKEND_MAX_RETRIES} 次${NC}" tail -10 /tmp/planet_backend.log exit 1 fi +} + +start_frontend_service() { + local frontend_port="$1" + local frontend_port_requested="$2" + + if [ "$frontend_port_requested" -eq 1 ]; then + kill_port_if_requested "$frontend_port" "前端" + fi echo -e "${BLUE}🌐 启动前端...${NC}" ensure_frontend_deps - pkill -f "vite" 2>/dev/null || true - pkill -f "bun run dev" 2>/dev/null || true - cd "$SCRIPT_DIR/frontend" - nohup bun run dev --port 3000 > /tmp/planet_frontend.log 2>&1 & + if ! start_frontend_with_retry "$frontend_port"; then + echo -e "${RED}❌ 前端启动失败,已重试 ${FRONTEND_MAX_RETRIES} 次${NC}" + tail -10 /tmp/planet_frontend.log + exit 1 + fi +} - sleep 3 +create_user() { + local username + local password + local password_confirm + local email + local generated_email + local role="viewer" + local is_admin + + ensure_uv_backend_deps + + echo -e "${BLUE}🗄️ 启动数据库...${NC}" + docker start planet_postgres 2>/dev/null || docker-compose up -d postgres + sleep 2 + + echo -e "${BLUE}👤 创建用户${NC}" + read -r -p "用户名: " username + + if [ -z "$username" ]; then + echo -e "${RED}❌ 用户名不能为空${NC}" + exit 1 + fi + + generated_email="${username}@planet.local" + read -r -p "邮箱(留空自动使用 ${generated_email}): " email + if [ -z "$email" ]; then + email="$generated_email" + fi + + read -r -s -p "密码: " password + echo "" + read -r -s -p "确认密码: " password_confirm + echo "" + + if [ -z "$password" ]; then + echo -e "${RED}❌ 密码不能为空${NC}" + exit 1 + fi + + if [ "${#password}" -lt 8 ]; then + echo -e "${RED}❌ 密码长度不能少于 8 位${NC}" + exit 1 + fi + + if [ "$password" != "$password_confirm" ]; then + echo -e "${RED}❌ 两次输入的密码不一致${NC}" + exit 1 + fi + + read -r -p "是否为管理员? (Y/N): " is_admin + if [[ "$is_admin" =~ ^[Yy]$ ]]; then + role="super_admin" + fi + + cd "$SCRIPT_DIR/backend" + PLANET_CREATEUSER_USERNAME="$username" \ + PLANET_CREATEUSER_PASSWORD="$password" \ + PLANET_CREATEUSER_EMAIL="$email" \ + PLANET_CREATEUSER_ROLE="$role" \ + PYTHONPATH="$SCRIPT_DIR/backend" \ + "$SCRIPT_DIR/.venv/bin/python" - <<'PY' +import asyncio +import os + +from sqlalchemy import text + +from app.core.security import get_password_hash +from app.db.session import async_session_factory +from app.models.user import User + + +async def main(): + username = os.environ["PLANET_CREATEUSER_USERNAME"].strip() + password = os.environ["PLANET_CREATEUSER_PASSWORD"] + email = os.environ["PLANET_CREATEUSER_EMAIL"].strip() + role = os.environ["PLANET_CREATEUSER_ROLE"].strip() + + async with async_session_factory() as session: + result = await session.execute( + text("SELECT id FROM users WHERE username = :username OR email = :email"), + {"username": username, "email": email}, + ) + if result.fetchone(): + raise SystemExit("用户名已存在") + + user = User( + username=username, + email=email, + password_hash=get_password_hash(password), + role=role, + is_active=True, + ) + session.add(user) + await session.commit() + + +asyncio.run(main()) +PY + + echo -e "${GREEN}✅ 用户创建成功${NC}" + echo " 用户名: ${username}" + echo " 角色: ${role}" + echo " 邮箱: ${email}" +} + +start() { + parse_service_args "$@" + cleanup_exit_containers + + echo -e "${BLUE}🚀 启动智能星球计划...${NC}" + + start_backend_service "$BACKEND_PORT" "$BACKEND_PORT_REQUESTED" + start_frontend_service "$FRONTEND_PORT" "$FRONTEND_PORT_REQUESTED" echo "" echo -e "${GREEN}✅ 启动完成!${NC}" - echo " 前端: http://localhost:3000" - echo " 后端: http://localhost:8000" + echo " 前端: http://localhost:${FRONTEND_PORT}" + echo " 后端: http://localhost:${BACKEND_PORT}" } stop() { echo -e "${YELLOW}🛑 停止服务...${NC}" - pkill -f "uvicorn" 2>/dev/null || true - pkill -f "vite" 2>/dev/null || true - pkill -f "bun run dev" 2>/dev/null || true + stop_backend_service + stop_frontend_service docker stop planet_postgres planet_redis 2>/dev/null || true echo -e "${GREEN}✅ 已停止${NC}" } restart() { - stop - sleep 1 - start + parse_service_args "$@" + cleanup_exit_containers + + if [ "$BACKEND_PORT_REQUESTED" -eq 0 ] && [ "$FRONTEND_PORT_REQUESTED" -eq 0 ]; then + stop + sleep 1 + start + return 0 + fi + + echo -e "${YELLOW}🔄 按需重启服务...${NC}" + + if [ "$BACKEND_PORT_REQUESTED" -eq 1 ]; then + stop_backend_service + sleep 1 + start_backend_service "$BACKEND_PORT" 1 + fi + + if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then + stop_frontend_service + sleep 1 + start_frontend_service "$FRONTEND_PORT" 1 + fi + + echo "" + echo -e "${GREEN}✅ 重启完成!${NC}" + if [ "$BACKEND_PORT_REQUESTED" -eq 1 ]; then + echo " 后端: http://localhost:${BACKEND_PORT}" + fi + if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then + echo " 前端: http://localhost:${FRONTEND_PORT}" + fi } health() { @@ -147,13 +461,18 @@ log() { case "$1" in start) - start + shift + start "$@" ;; stop) stop ;; restart) - restart + shift + restart "$@" + ;; + createuser) + create_user ;; health) health @@ -162,12 +481,13 @@ case "$1" in log "$2" ;; *) - echo "用法: ./planet.sh {start|stop|restart|health|log}" + echo "用法: ./planet.sh {start|stop|restart|createuser|health|log}" echo "" echo "命令:" - echo " start 启动服务" + echo " start 启动服务,可选: -b <后端端口> -f <前端端口>" echo " stop 停止服务" - echo " restart 重启服务" + echo " restart 重启服务,可选: -b [后端端口] -f [前端端口]" + echo " createuser 交互创建用户" echo " health 检查健康状态" echo " log 查看日志" echo " log -f 查看前端日志"