fix: upgrade startup script controls

This commit is contained in:
linkong
2026-03-27 11:13:01 +08:00
parent 30a29a6e34
commit bf2c4a172d
7 changed files with 402 additions and 73 deletions

View File

@@ -1 +1 @@
0.21.2
0.21.3

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.21.0",
"version": "0.21.3",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.2.6",

View File

@@ -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 "=========================================="

374
planet.sh
View File

@@ -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() {
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 查看前端日志"