fix: refine planet startup script UX
This commit is contained in:
@@ -7,6 +7,24 @@ This project follows the repository versioning rule:
|
|||||||
- `feature` -> `+0.1.0`
|
- `feature` -> `+0.1.0`
|
||||||
- `bugfix` -> `+0.0.1`
|
- `bugfix` -> `+0.0.1`
|
||||||
|
|
||||||
|
## 0.23.3
|
||||||
|
|
||||||
|
Released: 2026-04-08
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- Refined `planet.sh` startup and restart presentation so service bring-up, health checks, and frontend dev-server readiness are easier to follow in real time.
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Improved [planet.sh](/home/ray/dev/linkong/planet/planet.sh) by standardizing the script on `zsh`, tightening frontend startup stage rendering, and making spinner-driven subtask feedback show up step by step instead of bunching at the end.
|
||||||
|
- Improved [planet.sh](/home/ray/dev/linkong/planet/planet.sh) by clarifying health-check subtask copy, filtering noisy frontend startup lines, and aligning terminal output spacing across spinner and status rows.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed several `zsh` compatibility issues in [planet.sh](/home/ray/dev/linkong/planet/planet.sh), including reserved variable name collisions during `restart` and container health checks.
|
||||||
|
- Fixed [planet.sh](/home/ray/dev/linkong/planet/planet.sh) so frontend readiness animation no longer appears frozen while waiting for Vite startup and health probes.
|
||||||
|
|
||||||
## 0.23.2
|
## 0.23.2
|
||||||
|
|
||||||
Released: 2026-04-08
|
Released: 2026-04-08
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
## Current Version
|
## Current Version
|
||||||
|
|
||||||
- `main` 当前主线历史推导到:`0.16.5`
|
- `main` 当前主线历史推导到:`0.16.5`
|
||||||
- `dev` 当前开发分支历史推导到:`0.23.0`
|
- `dev` 当前开发分支历史推导到:`0.23.3`
|
||||||
|
|
||||||
## Timeline
|
## Timeline
|
||||||
|
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
| `0.21.6` | bugfix | `dev` | `pending` | improve Earth legend generation, info-card interactions, and HUD messaging polish |
|
| `0.21.6` | bugfix | `dev` | `pending` | improve Earth legend generation, info-card interactions, and HUD messaging polish |
|
||||||
| `0.22.9` | bugfix | `dev` | `6bfcd053` | simplify `planet.sh` readiness messaging and only show retry counts on actual restart |
|
| `0.22.9` | bugfix | `dev` | `6bfcd053` | simplify `planet.sh` readiness messaging and only show retry counts on actual restart |
|
||||||
| `0.23.0` | feature | `dev` | `pending` | add dedicated `aiprovider` service, multi-protocol AI adapters, uv-only Python runtime, and AI Provider restart controls |
|
| `0.23.0` | feature | `dev` | `pending` | add dedicated `aiprovider` service, multi-protocol AI adapters, uv-only Python runtime, and AI Provider restart controls |
|
||||||
|
| `0.23.3` | bugfix | `dev` | `pending` | refine `planet.sh` zsh runtime compatibility, startup logging, and frontend readiness feedback |
|
||||||
|
|
||||||
## Maintenance Commits Not Counted as Version Bumps
|
## Maintenance Commits Not Counted as Version Bumps
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planet-frontend",
|
"name": "planet-frontend",
|
||||||
"version": "0.23.2",
|
"version": "0.23.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
|
|||||||
740
planet.sh
740
planet.sh
@@ -1,8 +1,9 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_PATH="${(%):-%N}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
RED='\033[38;5;203m'
|
RED='\033[38;5;203m'
|
||||||
@@ -15,20 +16,26 @@ DIM='\033[2m'
|
|||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
WAIT_SPINNER_FRAMES=(
|
WAIT_SPINNER_FRAMES=(
|
||||||
'⠉⠁'
|
'⠉⠁'
|
||||||
|
'⠈⠁'
|
||||||
'⠈⠉'
|
'⠈⠉'
|
||||||
'⠀⠙'
|
' ⠙'
|
||||||
'⠀⠸'
|
' ⠸'
|
||||||
'⠀⢰'
|
' ⢰'
|
||||||
'⠀⣠'
|
' ⣠'
|
||||||
'⢀⣀'
|
'⢀⣀'
|
||||||
'⣀⡀'
|
'⣀⡀'
|
||||||
'⣄⠀'
|
'⣄ '
|
||||||
'⡆⠀'
|
'⡆ '
|
||||||
'⠇⠀'
|
'⠇ '
|
||||||
'⠋⠀'
|
'⠋ '
|
||||||
|
'⠋⠁'
|
||||||
)
|
)
|
||||||
WAIT_SPINNER_STEP=0
|
WAIT_SPINNER_STEP=0
|
||||||
WAIT_SPINNER_TICKS_PER_SECOND=8
|
WAIT_SPINNER_TICKS_PER_SECOND=8
|
||||||
|
WAIT_SPINNER_DETAIL=""
|
||||||
|
WAIT_SPINNER_MESSAGE=""
|
||||||
|
WAIT_SESSION_ACTIVE=0
|
||||||
|
HTTP_CHECK_MAX_TIME="${HTTP_CHECK_MAX_TIME:-0.2}"
|
||||||
|
|
||||||
BACKEND_MAX_RETRIES="${BACKEND_MAX_RETRIES:-3}"
|
BACKEND_MAX_RETRIES="${BACKEND_MAX_RETRIES:-3}"
|
||||||
BACKEND_HEALTH_CHECK_ATTEMPTS="${BACKEND_HEALTH_CHECK_ATTEMPTS:-10}"
|
BACKEND_HEALTH_CHECK_ATTEMPTS="${BACKEND_HEALTH_CHECK_ATTEMPTS:-10}"
|
||||||
@@ -44,10 +51,16 @@ DATABASE_RETRY_INTERVAL="${DATABASE_RETRY_INTERVAL:-5}"
|
|||||||
FRONTEND_MAX_RETRIES="${FRONTEND_MAX_RETRIES:-3}"
|
FRONTEND_MAX_RETRIES="${FRONTEND_MAX_RETRIES:-3}"
|
||||||
FRONTEND_HEALTH_CHECK_ATTEMPTS="${FRONTEND_HEALTH_CHECK_ATTEMPTS:-10}"
|
FRONTEND_HEALTH_CHECK_ATTEMPTS="${FRONTEND_HEALTH_CHECK_ATTEMPTS:-10}"
|
||||||
FRONTEND_HEALTH_CHECK_INTERVAL="${FRONTEND_HEALTH_CHECK_INTERVAL:-2}"
|
FRONTEND_HEALTH_CHECK_INTERVAL="${FRONTEND_HEALTH_CHECK_INTERVAL:-2}"
|
||||||
|
PORT_RELEASE_ATTEMPTS="${PORT_RELEASE_ATTEMPTS:-45}"
|
||||||
|
PORT_RELEASE_INTERVAL="${PORT_RELEASE_INTERVAL:-1}"
|
||||||
DEFAULT_BACKEND_PORT="${DEFAULT_BACKEND_PORT:-8000}"
|
DEFAULT_BACKEND_PORT="${DEFAULT_BACKEND_PORT:-8000}"
|
||||||
DEFAULT_FRONTEND_PORT="${DEFAULT_FRONTEND_PORT:-3000}"
|
DEFAULT_FRONTEND_PORT="${DEFAULT_FRONTEND_PORT:-3000}"
|
||||||
DEFAULT_AI_PROVIDER_PORT="${DEFAULT_AI_PROVIDER_PORT:-8010}"
|
DEFAULT_AI_PROVIDER_PORT="${DEFAULT_AI_PROVIDER_PORT:-8010}"
|
||||||
|
FRONTEND_RUNTIME_BIN="${FRONTEND_RUNTIME_BIN:-}"
|
||||||
|
FRONTEND_RUNTIME_SOURCE="${FRONTEND_RUNTIME_SOURCE:-}"
|
||||||
|
FRONTEND_PID_FILE="/tmp/planet_frontend.pid"
|
||||||
|
|
||||||
|
# Shell / Docker helpers
|
||||||
compose_up() {
|
compose_up() {
|
||||||
if docker compose version >/dev/null 2>&1; then
|
if docker compose version >/dev/null 2>&1; then
|
||||||
docker compose "$@"
|
docker compose "$@"
|
||||||
@@ -56,12 +69,14 @@ compose_up() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Terminal rendering helpers
|
||||||
render_wait_spinner() {
|
render_wait_spinner() {
|
||||||
local message="$1"
|
local message="$1"
|
||||||
local step="${2:-1}"
|
local step="${2:-1}"
|
||||||
local frame_index=$(( (step - 1) % ${#WAIT_SPINNER_FRAMES[@]} ))
|
local frame_index=$(( (step - 1) % ${#WAIT_SPINNER_FRAMES[@]} ))
|
||||||
local frame="${WAIT_SPINNER_FRAMES[$frame_index]}"
|
local frame="${WAIT_SPINNER_FRAMES[$frame_index]}"
|
||||||
printf "\r${DIM}·${NC} ${CYAN}%s${NC} ${WHITE}%s${NC}\033[K" "$frame" "$message"
|
local detail="${WAIT_SPINNER_DETAIL:- }"
|
||||||
|
printf "\r${DIM}·${NC} ${CYAN}%-2s${NC} ${WHITE}%s${NC}\033[K\n${DIM} %s${NC}\033[K\033[1A\r" "$frame" "$message" "$detail" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
animate_wait_spinner() {
|
animate_wait_spinner() {
|
||||||
@@ -83,19 +98,144 @@ animate_wait_spinner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clear_wait_spinner() {
|
clear_wait_spinner() {
|
||||||
printf "\r\033[K"
|
printf "\r\033[K\n\033[K\033[1A\r" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
start_wait_session() {
|
||||||
|
WAIT_SESSION_ACTIVE=1
|
||||||
|
WAIT_SPINNER_MESSAGE="$1"
|
||||||
|
WAIT_SPINNER_DETAIL=""
|
||||||
|
WAIT_SPINNER_STEP=1
|
||||||
|
render_wait_spinner "$WAIT_SPINNER_MESSAGE" "$WAIT_SPINNER_STEP"
|
||||||
|
}
|
||||||
|
|
||||||
|
resume_wait_session() {
|
||||||
|
if [ "$WAIT_SESSION_ACTIVE" -eq 1 ] && [ -n "$WAIT_SPINNER_MESSAGE" ]; then
|
||||||
|
WAIT_SPINNER_STEP=$((WAIT_SPINNER_STEP + 1))
|
||||||
|
render_wait_spinner "$WAIT_SPINNER_MESSAGE" "$WAIT_SPINNER_STEP"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_wait_session() {
|
||||||
|
if [ "$WAIT_SESSION_ACTIVE" -eq 1 ]; then
|
||||||
|
clear_wait_spinner
|
||||||
|
fi
|
||||||
|
WAIT_SESSION_ACTIVE=0
|
||||||
|
reset_wait_spinner_state
|
||||||
|
}
|
||||||
|
|
||||||
|
close_wait_session_context() {
|
||||||
|
local owns_wait_session="$1"
|
||||||
|
|
||||||
|
if [ "$owns_wait_session" -eq 1 ]; then
|
||||||
|
stop_wait_session
|
||||||
|
else
|
||||||
|
reset_wait_spinner_state
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_wait_spinner_state() {
|
||||||
|
WAIT_SPINNER_DETAIL=""
|
||||||
|
WAIT_SPINNER_MESSAGE=""
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitize_wait_detail() {
|
||||||
|
local line="$1"
|
||||||
|
|
||||||
|
line="$(printf "%s" "$line" | tr '\t' ' ' | tr -d '\r')"
|
||||||
|
line="$(printf "%s" "$line" | sed -E 's/\x1B\[[0-9;]*[[:alpha:]]//g')"
|
||||||
|
line="$(printf "%s" "$line" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')"
|
||||||
|
|
||||||
|
if [ "${#line}" -gt 140 ]; then
|
||||||
|
line="${line:0:137}..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%s" "$line"
|
||||||
|
}
|
||||||
|
|
||||||
|
set_wait_detail() {
|
||||||
|
WAIT_SPINNER_DETAIL="$(sanitize_wait_detail "$1")"
|
||||||
|
if [ "$WAIT_SESSION_ACTIVE" -eq 1 ] && [ -n "$WAIT_SPINNER_MESSAGE" ]; then
|
||||||
|
WAIT_SPINNER_STEP=$((WAIT_SPINNER_STEP + 1))
|
||||||
|
render_wait_spinner "$WAIT_SPINNER_MESSAGE" "$WAIT_SPINNER_STEP"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_command_with_spinner() {
|
||||||
|
local message="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [ "$WAIT_SESSION_ACTIVE" -eq 1 ]; then
|
||||||
|
set_wait_detail "$message"
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$@" &
|
||||||
|
local command_pid=$!
|
||||||
|
local exit_code=0
|
||||||
|
|
||||||
|
while kill -0 "$command_pid" 2>/dev/null; do
|
||||||
|
WAIT_SPINNER_STEP=$((WAIT_SPINNER_STEP + 1))
|
||||||
|
if [ "$WAIT_SESSION_ACTIVE" -eq 1 ] && [ -n "$WAIT_SPINNER_MESSAGE" ]; then
|
||||||
|
render_wait_spinner "$WAIT_SPINNER_MESSAGE" "$WAIT_SPINNER_STEP"
|
||||||
|
else
|
||||||
|
render_wait_spinner "$message" "$WAIT_SPINNER_STEP"
|
||||||
|
fi
|
||||||
|
sleep 0.125
|
||||||
|
done
|
||||||
|
|
||||||
|
wait "$command_pid" || exit_code=$?
|
||||||
|
return "$exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_bun_from_shell_configs() {
|
||||||
|
local shell_name="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
local config_file
|
||||||
|
local candidate=""
|
||||||
|
local bun_install=""
|
||||||
|
|
||||||
|
for config_file in "$@"; do
|
||||||
|
[ -f "$config_file" ] || continue
|
||||||
|
|
||||||
|
if grep -Eq 'BUN_INSTALL|\.bun/bin|bun ' "$config_file" 2>/dev/null; then
|
||||||
|
bun_install="$(sed -nE 's/^[[:space:]]*(export[[:space:]]+)?BUN_INSTALL=["'\'']?([^"'\'']+)["'\'']?/\2/p' "$config_file" | tail -n 1)"
|
||||||
|
|
||||||
|
if [ -n "$bun_install" ]; then
|
||||||
|
bun_install="${bun_install/#\$HOME/$HOME}"
|
||||||
|
bun_install="${bun_install/#\~/$HOME}"
|
||||||
|
candidate="$bun_install/bin/bun"
|
||||||
|
if [ -x "$candidate" ]; then
|
||||||
|
FRONTEND_RUNTIME_BIN="$candidate"
|
||||||
|
FRONTEND_RUNTIME_SOURCE="${shell_name}-config"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
candidate="$HOME/.bun/bin/bun"
|
||||||
|
if [ -x "$candidate" ]; then
|
||||||
|
FRONTEND_RUNTIME_BIN="$candidate"
|
||||||
|
FRONTEND_RUNTIME_SOURCE="${shell_name}-config"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
finish_wait_spinner() {
|
finish_wait_spinner() {
|
||||||
log_line "done" "$GREEN" "$1"
|
log_line "done" "$GREEN" "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Logging helpers
|
||||||
log_line() {
|
log_line() {
|
||||||
local label="$1"
|
local label="$1"
|
||||||
local color="$2"
|
local color="$2"
|
||||||
local message="$3"
|
local message="$3"
|
||||||
clear_wait_spinner
|
clear_wait_spinner
|
||||||
printf "${DIM}·${NC} ${color}%-4s${NC} ${WHITE}%s${NC}\n" "$label" "$message"
|
printf "${DIM}·${NC} ${color}%-4s${NC} ${WHITE}%s${NC}\n" "$label" "$message"
|
||||||
|
resume_wait_session
|
||||||
}
|
}
|
||||||
|
|
||||||
log_step() {
|
log_step() {
|
||||||
@@ -106,6 +246,10 @@ log_warn() {
|
|||||||
log_line "warn" "$YELLOW" "$1"
|
log_line "warn" "$YELLOW" "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log_halt() {
|
||||||
|
log_line "stopped" "$RED" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
log_error() {
|
log_error() {
|
||||||
log_line "fail" "$RED" "$1"
|
log_line "fail" "$RED" "$1"
|
||||||
}
|
}
|
||||||
@@ -129,7 +273,6 @@ print_splash() {
|
|||||||
|___|_| |_|\__\___|_|_|_|\__, |\___|_| |_|\__|
|
|___|_| |_|\__\___|_|_|_|\__, |\___|_| |_|\__|
|
||||||
|___/
|
|___/
|
||||||
EOF
|
EOF
|
||||||
# printf "%b\n" "$NC"
|
|
||||||
printf "%b" "$BLUE"
|
printf "%b" "$BLUE"
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
____ _ _
|
____ _ _
|
||||||
@@ -138,7 +281,6 @@ EOF
|
|||||||
| __/| | (_| | | | | __/| |_
|
| __/| | (_| | | | | __/| |_
|
||||||
|_| |_|\__,_|_| |_|\___| \__|
|
|_| |_|\__,_|_| |_|\___| \__|
|
||||||
EOF
|
EOF
|
||||||
# printf "%b\n" "$NC"
|
|
||||||
printf "%b" "$WHITE"
|
printf "%b" "$WHITE"
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
____ _
|
____ _
|
||||||
@@ -150,6 +292,7 @@ EOF
|
|||||||
printf "%b\n\n" "$NC"
|
printf "%b\n\n" "$NC"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Retry / dependency helpers
|
||||||
run_with_retry() {
|
run_with_retry() {
|
||||||
local max_retries="$1"
|
local max_retries="$1"
|
||||||
local interval="$2"
|
local interval="$2"
|
||||||
@@ -160,7 +303,8 @@ run_with_retry() {
|
|||||||
local attempt=1
|
local attempt=1
|
||||||
|
|
||||||
while [ "$attempt" -le "$max_retries" ]; do
|
while [ "$attempt" -le "$max_retries" ]; do
|
||||||
if "$@"; then
|
if run_command_with_spinner "$retry_label" "$@"; then
|
||||||
|
WAIT_SPINNER_DETAIL=""
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -177,7 +321,7 @@ run_with_retry() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensure_uv_backend_deps() {
|
ensure_uv_backend_deps() {
|
||||||
log_step "检查后端 uv 环境"
|
set_wait_detail "检查后端 uv 环境"
|
||||||
|
|
||||||
if ! command -v uv >/dev/null 2>&1; then
|
if ! command -v uv >/dev/null 2>&1; then
|
||||||
log_error "未找到 uv,请先安装 uv 并加入 PATH"
|
log_error "未找到 uv,请先安装 uv 并加入 PATH"
|
||||||
@@ -204,48 +348,124 @@ ensure_uv_backend_deps() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_frontend_deps() {
|
# Frontend runtime resolution
|
||||||
log_step "检查前端依赖"
|
resolve_frontend_runtime() {
|
||||||
|
local shell_bin
|
||||||
|
local candidate
|
||||||
|
|
||||||
if ! command -v bun >/dev/null 2>&1; then
|
if [ -n "$FRONTEND_RUNTIME_BIN" ] && [ -x "$FRONTEND_RUNTIME_BIN" ]; then
|
||||||
log_error "未找到 bun,请先安装或加载 bun 到 PATH"
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
set_wait_detail "读取 zsh 配置中的 bun"
|
||||||
|
if resolve_bun_from_shell_configs "zsh" "$HOME/.zshrc" "$HOME/.zprofile"; then
|
||||||
|
WAIT_SPINNER_DETAIL="已找到 zsh 配置中的 bun"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v zsh >/dev/null 2>&1; then
|
||||||
|
set_wait_detail "检查 zsh 中的 bun"
|
||||||
|
candidate="$(zsh -ic 'command -v bun 2>/dev/null || true' 2>/dev/null | tail -n 1)"
|
||||||
|
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
||||||
|
FRONTEND_RUNTIME_BIN="$candidate"
|
||||||
|
FRONTEND_RUNTIME_SOURCE="zsh"
|
||||||
|
WAIT_SPINNER_DETAIL="已找到 zsh 中的 bun"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
for shell_bin in pwsh powershell; do
|
||||||
|
if command -v "$shell_bin" >/dev/null 2>&1; then
|
||||||
|
set_wait_detail "检查 powershell 中的 bun"
|
||||||
|
candidate="$("$shell_bin" -Command "(Get-Command bun -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source)" 2>/dev/null | tail -n 1 | tr -d '\r')"
|
||||||
|
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
||||||
|
FRONTEND_RUNTIME_BIN="$candidate"
|
||||||
|
FRONTEND_RUNTIME_SOURCE="powershell"
|
||||||
|
WAIT_SPINNER_DETAIL="已找到 powershell 中的 bun"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if command -v bun >/dev/null 2>&1; then
|
||||||
|
set_wait_detail "检查当前环境中的 bun"
|
||||||
|
candidate="$(command -v bun)"
|
||||||
|
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
||||||
|
FRONTEND_RUNTIME_BIN="$candidate"
|
||||||
|
FRONTEND_RUNTIME_SOURCE="current-shell"
|
||||||
|
WAIT_SPINNER_DETAIL="已找到当前环境中的 bun"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_frontend_deps() {
|
||||||
|
local owns_wait_session=0
|
||||||
|
|
||||||
|
if [ "$WAIT_SESSION_ACTIVE" -eq 0 ]; then
|
||||||
|
start_wait_session "检查前端依赖"
|
||||||
|
owns_wait_session=1
|
||||||
|
else
|
||||||
|
set_wait_detail "检查前端依赖"
|
||||||
|
fi
|
||||||
|
|
||||||
|
set_wait_detail "解析前端运行时"
|
||||||
|
if ! resolve_frontend_runtime; then
|
||||||
|
close_wait_session_context "$owns_wait_session"
|
||||||
|
log_error "未找到 bun,已按 zsh > powershell 顺序检查启动配置"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd "$SCRIPT_DIR/frontend"
|
cd "$SCRIPT_DIR/frontend"
|
||||||
|
|
||||||
|
set_wait_detail "检查 vite 是否已安装"
|
||||||
if [ ! -x "$SCRIPT_DIR/frontend/node_modules/.bin/vite" ]; then
|
if [ ! -x "$SCRIPT_DIR/frontend/node_modules/.bin/vite" ]; then
|
||||||
log_warn "前端依赖缺失,正在执行 bun install"
|
log_warn "前端依赖缺失,正在执行 bun install (${FRONTEND_RUNTIME_SOURCE})"
|
||||||
|
set_wait_detail "执行 ${FRONTEND_RUNTIME_SOURCE} bun install"
|
||||||
if ! run_with_retry \
|
if ! run_with_retry \
|
||||||
"$DEPENDENCY_INSTALL_MAX_RETRIES" \
|
"$DEPENDENCY_INSTALL_MAX_RETRIES" \
|
||||||
"$DEPENDENCY_INSTALL_RETRY_INTERVAL" \
|
"$DEPENDENCY_INSTALL_RETRY_INTERVAL" \
|
||||||
"前端依赖安装失败,已重试 ${DEPENDENCY_INSTALL_MAX_RETRIES} 次" \
|
"前端依赖安装失败,已重试 ${DEPENDENCY_INSTALL_MAX_RETRIES} 次" \
|
||||||
"bun install" \
|
"bun install" \
|
||||||
bun install; then
|
"$FRONTEND_RUNTIME_BIN" install; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -x "$SCRIPT_DIR/frontend/node_modules/.bin/vite" ]; then
|
if [ ! -x "$SCRIPT_DIR/frontend/node_modules/.bin/vite" ]; then
|
||||||
|
close_wait_session_context "$owns_wait_session"
|
||||||
log_error "前端依赖安装失败,未找到 vite"
|
log_error "前端依赖安装失败,未找到 vite"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
close_wait_session_context "$owns_wait_session"
|
||||||
|
if [ "$owns_wait_session" -eq 0 ]; then
|
||||||
|
resume_wait_session
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Health / readiness helpers
|
||||||
wait_for_http() {
|
wait_for_http() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local attempts="$2"
|
local attempts="$2"
|
||||||
local interval="$3"
|
local interval="$3"
|
||||||
local service_name="$4"
|
local service_name="$4"
|
||||||
local attempt=1
|
local attempt=1
|
||||||
|
local wait_message="检查${service_name}健康检查"
|
||||||
|
local wait_detail_prefix="请求"
|
||||||
|
|
||||||
|
set_wait_detail "${wait_detail_prefix} ${url} (1/${attempts})"
|
||||||
|
|
||||||
while [ "$attempt" -le "$attempts" ]; do
|
while [ "$attempt" -le "$attempts" ]; do
|
||||||
if curl -s "$url" > /dev/null 2>&1; then
|
if curl -s --max-time "$HTTP_CHECK_MAX_TIME" "$url" > /dev/null 2>&1; then
|
||||||
finish_wait_spinner "${service_name}已就绪"
|
finish_wait_spinner "${service_name}已就绪"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
animate_wait_spinner "等待${service_name}就绪" "$interval"
|
set_wait_detail "${wait_detail_prefix} ${url} (${attempt}/${attempts})"
|
||||||
|
animate_wait_spinner "${wait_message}" "$interval"
|
||||||
attempt=$((attempt + 1))
|
attempt=$((attempt + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -253,23 +473,127 @@ wait_for_http() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_log_tail_line() {
|
||||||
|
local log_file="$1"
|
||||||
|
local fallback="$2"
|
||||||
|
local last_line=""
|
||||||
|
local filtered_line=""
|
||||||
|
|
||||||
|
if [ -f "$log_file" ]; then
|
||||||
|
while IFS= read -r last_line; do
|
||||||
|
filtered_line="$(sanitize_wait_detail "$last_line")"
|
||||||
|
if [ -n "$filtered_line" ] &&
|
||||||
|
[[ ! "$filtered_line" =~ ^\$[[:space:]] ]] &&
|
||||||
|
[[ "$filtered_line" != "nohup: ignoring input" ]] &&
|
||||||
|
[[ "$filtered_line" != The\ CJS\ build\ of\ Vite\'s\ Node\ API\ is\ deprecated.* ]]; then
|
||||||
|
printf "%s" "$filtered_line"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done < <(tail -n 20 "$log_file" 2>/dev/null | tac)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$last_line" ]; then
|
||||||
|
printf "%s" "$fallback"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sanitize_wait_detail "$last_line"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_frontend_ready() {
|
||||||
|
local frontend_port="$1"
|
||||||
|
local frontend_pid="$2"
|
||||||
|
local log_file="$3"
|
||||||
|
local attempts="$4"
|
||||||
|
local interval="$5"
|
||||||
|
local owns_wait_session=0
|
||||||
|
local tick=0
|
||||||
|
local max_ticks=$(( attempts * interval * WAIT_SPINNER_TICKS_PER_SECOND ))
|
||||||
|
local probe_every_ticks=$(( WAIT_SPINNER_TICKS_PER_SECOND / 2 ))
|
||||||
|
local detail_every_ticks=2
|
||||||
|
local detail=""
|
||||||
|
|
||||||
|
if [ "$max_ticks" -lt 1 ]; then
|
||||||
|
max_ticks=1
|
||||||
|
fi
|
||||||
|
if [ "$probe_every_ticks" -lt 1 ]; then
|
||||||
|
probe_every_ticks=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$WAIT_SESSION_ACTIVE" -eq 0 ]; then
|
||||||
|
start_wait_session "启动前端服务"
|
||||||
|
owns_wait_session=1
|
||||||
|
else
|
||||||
|
set_wait_detail "启动 Vite 开发服务器"
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [ "$tick" -lt "$max_ticks" ]; do
|
||||||
|
WAIT_SPINNER_STEP=$((WAIT_SPINNER_STEP + 1))
|
||||||
|
render_wait_spinner "$WAIT_SPINNER_MESSAGE" "$WAIT_SPINNER_STEP"
|
||||||
|
|
||||||
|
if ! kill -0 "$frontend_pid" 2>/dev/null; then
|
||||||
|
detail="$(get_log_tail_line "$log_file" "前端进程已退出")"
|
||||||
|
WAIT_SPINNER_DETAIL="$detail"
|
||||||
|
clear_wait_spinner
|
||||||
|
if [ -n "$detail" ]; then
|
||||||
|
log_note "$detail"
|
||||||
|
fi
|
||||||
|
close_wait_session_context "$owns_wait_session"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $(( tick % detail_every_ticks )) -eq 0 ]; then
|
||||||
|
detail="$(get_log_tail_line "$log_file" "等待 Vite 输出启动日志")"
|
||||||
|
WAIT_SPINNER_DETAIL="$detail"
|
||||||
|
render_wait_spinner "$WAIT_SPINNER_MESSAGE" "$WAIT_SPINNER_STEP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $(( tick % probe_every_ticks )) -eq 0 ] &&
|
||||||
|
curl -s --max-time "$HTTP_CHECK_MAX_TIME" "http://localhost:${frontend_port}" > /dev/null 2>&1; then
|
||||||
|
if [ "$owns_wait_session" -eq 1 ]; then
|
||||||
|
stop_wait_session
|
||||||
|
finish_wait_spinner "前端已就绪"
|
||||||
|
else
|
||||||
|
reset_wait_spinner_state
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 0.125
|
||||||
|
tick=$((tick + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
WAIT_SPINNER_DETAIL="$(get_log_tail_line "$log_file" "等待前端就绪超时")"
|
||||||
|
clear_wait_spinner
|
||||||
|
if [ -n "$WAIT_SPINNER_DETAIL" ]; then
|
||||||
|
log_note "$WAIT_SPINNER_DETAIL"
|
||||||
|
fi
|
||||||
|
close_wait_session_context "$owns_wait_session"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
wait_for_container_health() {
|
wait_for_container_health() {
|
||||||
local container_name="$1"
|
local container_name="$1"
|
||||||
local attempts="$2"
|
local attempts="$2"
|
||||||
local interval="$3"
|
local interval="$3"
|
||||||
local service_name="$4"
|
local service_name="$4"
|
||||||
local attempt=1
|
local attempt=1
|
||||||
local status
|
local health_status
|
||||||
|
local wait_message="检查${service_name}容器健康检查"
|
||||||
|
local wait_detail_prefix="读取"
|
||||||
|
|
||||||
|
set_wait_detail "${wait_detail_prefix} ${container_name} 容器健康状态 (1/${attempts})"
|
||||||
|
|
||||||
while [ "$attempt" -le "$attempts" ]; do
|
while [ "$attempt" -le "$attempts" ]; do
|
||||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_name" 2>/dev/null || true)"
|
health_status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_name" 2>/dev/null || true)"
|
||||||
|
|
||||||
if [ "$status" = "healthy" ] || [ "$status" = "running" ]; then
|
if [ "$health_status" = "healthy" ] || [ "$health_status" = "running" ]; then
|
||||||
finish_wait_spinner "${service_name}容器健康检查已通过"
|
finish_wait_spinner "${service_name}容器健康检查已通过"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
animate_wait_spinner "等待${service_name}容器健康检查通过" "$interval"
|
set_wait_detail "${wait_detail_prefix} ${container_name} 容器健康状态 (${attempt}/${attempts})"
|
||||||
|
animate_wait_spinner "${wait_message}" "$interval"
|
||||||
attempt=$((attempt + 1))
|
attempt=$((attempt + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -277,6 +601,24 @@ wait_for_container_health() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fail_unreleased_port() {
|
||||||
|
local port="$1"
|
||||||
|
|
||||||
|
if [ -z "$(collect_port_pids "$port" || true)" ]; then
|
||||||
|
log_error "端口 ${port} 当前环境内未发现占用进程,但端口仍不可用,请检查宿主机或外部环境占用"
|
||||||
|
else
|
||||||
|
log_error "端口 ${port} 清理失败,请检查占用进程"
|
||||||
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
|
lsof -iTCP:"${port}" -sTCP:LISTEN -n -P 2>/dev/null || true
|
||||||
|
elif command -v ss >/dev/null 2>&1; then
|
||||||
|
ss -ltnp "( sport = :${port} )" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Service startup helpers
|
||||||
wait_for_database_health() {
|
wait_for_database_health() {
|
||||||
wait_for_container_health "planet_postgres" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$DATABASE_RETRY_INTERVAL" "PostgreSQL" &&
|
wait_for_container_health "planet_postgres" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$DATABASE_RETRY_INTERVAL" "PostgreSQL" &&
|
||||||
wait_for_container_health "planet_redis" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$DATABASE_RETRY_INTERVAL" "Redis"
|
wait_for_container_health "planet_redis" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$DATABASE_RETRY_INTERVAL" "Redis"
|
||||||
@@ -298,12 +640,17 @@ start_postgres_service() {
|
|||||||
docker start planet_postgres >/dev/null 2>&1 || compose_up up -d postgres >/dev/null 2>&1
|
docker start planet_postgres >/dev/null 2>&1 || compose_up up -d postgres >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Backend lifecycle helpers
|
||||||
|
cleanup_backend_processes() {
|
||||||
|
pkill -f "uvicorn" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
start_backend_with_retry() {
|
start_backend_with_retry() {
|
||||||
local backend_port="$1"
|
local backend_port="$1"
|
||||||
local retry=1
|
local retry=1
|
||||||
|
|
||||||
while [ "$retry" -le "$BACKEND_MAX_RETRIES" ]; do
|
while [ "$retry" -le "$BACKEND_MAX_RETRIES" ]; do
|
||||||
pkill -f "uvicorn" 2>/dev/null || true
|
cleanup_backend_processes
|
||||||
cd "$SCRIPT_DIR/backend"
|
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 &
|
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=$!
|
BACKEND_PID=$!
|
||||||
@@ -324,8 +671,7 @@ start_backend_with_retry() {
|
|||||||
start_ai_provider_service() {
|
start_ai_provider_service() {
|
||||||
local ai_provider_port="${1:-$DEFAULT_AI_PROVIDER_PORT}"
|
local ai_provider_port="${1:-$DEFAULT_AI_PROVIDER_PORT}"
|
||||||
local retry=1
|
local retry=1
|
||||||
|
set_wait_detail "启动 AI Provider"
|
||||||
log_step "启动 AI Provider"
|
|
||||||
|
|
||||||
while [ "$retry" -le "$AI_PROVIDER_START_MAX_RETRIES" ]; do
|
while [ "$retry" -le "$AI_PROVIDER_START_MAX_RETRIES" ]; do
|
||||||
if docker start planet_aiprovider >/dev/null 2>&1 || compose_up up -d aiprovider >/dev/null 2>&1; then
|
if docker start planet_aiprovider >/dev/null 2>&1 || compose_up up -d aiprovider >/dev/null 2>&1; then
|
||||||
@@ -351,30 +697,6 @@ start_ai_provider_service() {
|
|||||||
exit 1
|
exit 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
|
|
||||||
|
|
||||||
kill "$FRONTEND_PID" 2>/dev/null || true
|
|
||||||
animate_wait_spinner "前端第 ${retry}/${FRONTEND_MAX_RETRIES} 次启动未就绪,准备重试" "$FRONTEND_HEALTH_CHECK_INTERVAL"
|
|
||||||
retry=$((retry + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
clear_wait_spinner
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_database_services_healthy() {
|
ensure_database_services_healthy() {
|
||||||
local retry=1
|
local retry=1
|
||||||
|
|
||||||
@@ -418,8 +740,7 @@ ensure_postgres_service_healthy() {
|
|||||||
|
|
||||||
restart_database_service() {
|
restart_database_service() {
|
||||||
local retry=1
|
local retry=1
|
||||||
|
set_wait_detail "重启数据库"
|
||||||
log_step "重启数据库"
|
|
||||||
|
|
||||||
while [ "$retry" -le "$DATABASE_START_MAX_RETRIES" ]; do
|
while [ "$retry" -le "$DATABASE_START_MAX_RETRIES" ]; do
|
||||||
if restart_database_services && wait_for_database_health; then
|
if restart_database_services && wait_for_database_health; then
|
||||||
@@ -444,8 +765,9 @@ start_backend_service() {
|
|||||||
local backend_port_requested="$2"
|
local backend_port_requested="$2"
|
||||||
local ai_provider_port="${3:-$DEFAULT_AI_PROVIDER_PORT}"
|
local ai_provider_port="${3:-$DEFAULT_AI_PROVIDER_PORT}"
|
||||||
|
|
||||||
log_step "启动数据库"
|
set_wait_detail "启动数据库"
|
||||||
ensure_database_services_healthy
|
ensure_database_services_healthy
|
||||||
|
log_success "启动数据库已就绪"
|
||||||
sleep 3
|
sleep 3
|
||||||
|
|
||||||
start_ai_provider_service "$ai_provider_port"
|
start_ai_provider_service "$ai_provider_port"
|
||||||
@@ -454,7 +776,7 @@ start_backend_service() {
|
|||||||
kill_port_if_requested "$backend_port" "后端"
|
kill_port_if_requested "$backend_port" "后端"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_step "启动后端"
|
set_wait_detail "启动后端"
|
||||||
ensure_uv_backend_deps
|
ensure_uv_backend_deps
|
||||||
if ! start_backend_with_retry "$backend_port"; then
|
if ! start_backend_with_retry "$backend_port"; then
|
||||||
log_error "后端启动失败,已重试 ${BACKEND_MAX_RETRIES} 次"
|
log_error "后端启动失败,已重试 ${BACKEND_MAX_RETRIES} 次"
|
||||||
@@ -463,6 +785,210 @@ start_backend_service() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Port / process helpers
|
||||||
|
validate_port() {
|
||||||
|
local port="$1"
|
||||||
|
|
||||||
|
if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
|
||||||
|
log_error "非法端口: ${port}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_port_pids() {
|
||||||
|
local port="$1"
|
||||||
|
local pids=""
|
||||||
|
local pid=""
|
||||||
|
|
||||||
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
|
pids="$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)"
|
||||||
|
if [ -n "$pids" ]; then
|
||||||
|
printf "%s\n" "$pids" | awk '!seen[$0]++'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v fuser >/dev/null 2>&1; then
|
||||||
|
pids="$(fuser "${port}/tcp" 2>/dev/null || true)"
|
||||||
|
if [ -n "$pids" ]; then
|
||||||
|
for pid in $pids; do
|
||||||
|
printf "%s\n" "$pid"
|
||||||
|
done | awk '!seen[$0]++'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v ss >/dev/null 2>&1; then
|
||||||
|
pids="$(ss -ltnp "( sport = :${port} )" 2>/dev/null | sed -nE 's/.*pid=([0-9]+).*/\1/p' || true)"
|
||||||
|
if [ -n "$pids" ]; then
|
||||||
|
printf "%s\n" "$pids" | awk '!seen[$0]++'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate_process_tree() {
|
||||||
|
local signal="$1"
|
||||||
|
local pid="$2"
|
||||||
|
local child_pid
|
||||||
|
|
||||||
|
[ -n "$pid" ] || return 0
|
||||||
|
kill -0 "$pid" 2>/dev/null || return 0
|
||||||
|
|
||||||
|
if command -v pgrep >/dev/null 2>&1; then
|
||||||
|
while IFS= read -r child_pid; do
|
||||||
|
[ -n "$child_pid" ] || continue
|
||||||
|
terminate_process_tree "$signal" "$child_pid"
|
||||||
|
done < <(pgrep -P "$pid" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
kill "-${signal}" "$pid" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
can_bind_port() {
|
||||||
|
local port="$1"
|
||||||
|
python3 - "$port" <<'PY' >/dev/null 2>&1
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
port = int(sys.argv[1])
|
||||||
|
sockets = []
|
||||||
|
try:
|
||||||
|
for family, host in ((socket.AF_INET, "127.0.0.1"), (socket.AF_INET6, "::1")):
|
||||||
|
try:
|
||||||
|
sock = socket.socket(family)
|
||||||
|
sock.bind((host, port))
|
||||||
|
sockets.append(sock)
|
||||||
|
except OSError:
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
for sock in sockets:
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_port_release() {
|
||||||
|
local port="$1"
|
||||||
|
local attempt=1
|
||||||
|
|
||||||
|
while [ "$attempt" -le "$PORT_RELEASE_ATTEMPTS" ]; do
|
||||||
|
if can_bind_port "$port"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep "$PORT_RELEASE_INTERVAL"
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
kill_port_if_requested() {
|
||||||
|
local port="$1"
|
||||||
|
local service_name="$2"
|
||||||
|
local pids=""
|
||||||
|
local pid=""
|
||||||
|
|
||||||
|
log_warn "检测 ${service_name} 端口 ${port} 占用"
|
||||||
|
|
||||||
|
pids="$(collect_port_pids "$port" || true)"
|
||||||
|
|
||||||
|
if [ -z "$pids" ] && can_bind_port "$port"; then
|
||||||
|
log_success "端口 ${port} 未被占用"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_step "发现端口 ${port} 占用,正在终止"
|
||||||
|
for pid in $pids; do
|
||||||
|
terminate_process_tree TERM "$pid"
|
||||||
|
done
|
||||||
|
|
||||||
|
if wait_for_port_release "$port"; then
|
||||||
|
log_success "端口 ${port} 已释放"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_warn "端口 ${port} 仍被占用,正在强制终止"
|
||||||
|
pids="$(collect_port_pids "$port" || true)"
|
||||||
|
for pid in $pids; do
|
||||||
|
terminate_process_tree KILL "$pid"
|
||||||
|
done
|
||||||
|
|
||||||
|
if wait_for_port_release "$port"; then
|
||||||
|
log_success "端口 ${port} 已释放"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
fail_unreleased_port "$port"
|
||||||
|
}
|
||||||
|
|
||||||
|
frontend_log_indicates_port_conflict() {
|
||||||
|
local log_file="$1"
|
||||||
|
|
||||||
|
[ -f "$log_file" ] || return 1
|
||||||
|
grep -q "Port .* is already in use" "$log_file" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Frontend lifecycle helpers
|
||||||
|
cleanup_frontend_processes() {
|
||||||
|
local frontend_port="${1:-$DEFAULT_FRONTEND_PORT}"
|
||||||
|
local tracked_pid=""
|
||||||
|
|
||||||
|
if [ -f "$FRONTEND_PID_FILE" ]; then
|
||||||
|
tracked_pid="$(cat "$FRONTEND_PID_FILE" 2>/dev/null || true)"
|
||||||
|
if [ -n "$tracked_pid" ]; then
|
||||||
|
terminate_process_tree TERM "$tracked_pid"
|
||||||
|
sleep 1
|
||||||
|
terminate_process_tree KILL "$tracked_pid"
|
||||||
|
fi
|
||||||
|
rm -f "$FRONTEND_PID_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pkill -f "${SCRIPT_DIR}/frontend/node_modules/.bin/vite --port ${frontend_port} --strictPort" 2>/dev/null || true
|
||||||
|
pkill -f "${SCRIPT_DIR}/frontend/node_modules/.bin/vite" 2>/dev/null || true
|
||||||
|
pkill -f "bun run dev --port ${frontend_port}" 2>/dev/null || true
|
||||||
|
pkill -f "bun run dev" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
start_frontend_with_retry() {
|
||||||
|
local frontend_port="$1"
|
||||||
|
local frontend_port_requested="${2:-0}"
|
||||||
|
local retry=1
|
||||||
|
|
||||||
|
while [ "$retry" -le "$FRONTEND_MAX_RETRIES" ]; do
|
||||||
|
cleanup_frontend_processes "$frontend_port"
|
||||||
|
if ! wait_for_port_release "$frontend_port"; then
|
||||||
|
fail_unreleased_port "$frontend_port"
|
||||||
|
fi
|
||||||
|
cd "$SCRIPT_DIR/frontend"
|
||||||
|
: > /tmp/planet_frontend.log
|
||||||
|
nohup "$FRONTEND_RUNTIME_BIN" run dev --port "$frontend_port" --strictPort > /tmp/planet_frontend.log 2>&1 &
|
||||||
|
FRONTEND_PID=$!
|
||||||
|
printf "%s" "$FRONTEND_PID" > "$FRONTEND_PID_FILE"
|
||||||
|
|
||||||
|
if wait_for_frontend_ready "$frontend_port" "$FRONTEND_PID" "/tmp/planet_frontend.log" "$FRONTEND_HEALTH_CHECK_ATTEMPTS" "$FRONTEND_HEALTH_CHECK_INTERVAL"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_frontend_processes "$frontend_port"
|
||||||
|
reset_wait_spinner_state
|
||||||
|
|
||||||
|
if [ "$frontend_port_requested" -eq 1 ] && frontend_log_indicates_port_conflict "/tmp/planet_frontend.log"; then
|
||||||
|
kill_port_if_requested "$frontend_port" "前端"
|
||||||
|
fi
|
||||||
|
|
||||||
|
animate_wait_spinner "前端第 ${retry}/${FRONTEND_MAX_RETRIES} 次启动未就绪,准备重试" "$FRONTEND_HEALTH_CHECK_INTERVAL"
|
||||||
|
retry=$((retry + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
clear_wait_spinner
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
start_frontend_service() {
|
start_frontend_service() {
|
||||||
local frontend_port="$1"
|
local frontend_port="$1"
|
||||||
local frontend_port_requested="$2"
|
local frontend_port_requested="$2"
|
||||||
@@ -471,51 +997,24 @@ start_frontend_service() {
|
|||||||
kill_port_if_requested "$frontend_port" "前端"
|
kill_port_if_requested "$frontend_port" "前端"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_step "启动前端"
|
start_wait_session "检查前端依赖"
|
||||||
ensure_frontend_deps
|
ensure_frontend_deps
|
||||||
if ! start_frontend_with_retry "$frontend_port"; then
|
stop_wait_session
|
||||||
|
log_success "前端依赖已就绪"
|
||||||
|
|
||||||
|
start_wait_session "启动前端服务"
|
||||||
|
set_wait_detail "启动 Vite 开发服务器"
|
||||||
|
if ! start_frontend_with_retry "$frontend_port" "$frontend_port_requested"; then
|
||||||
|
stop_wait_session
|
||||||
log_error "前端启动失败,已重试 ${FRONTEND_MAX_RETRIES} 次"
|
log_error "前端启动失败,已重试 ${FRONTEND_MAX_RETRIES} 次"
|
||||||
tail -10 /tmp/planet_frontend.log
|
tail -10 /tmp/planet_frontend.log
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
stop_wait_session
|
||||||
|
log_success "前端已就绪"
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_port() {
|
# Argument parsing / command helpers
|
||||||
local port="$1"
|
|
||||||
|
|
||||||
if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
|
|
||||||
log_error "非法端口: ${port}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
kill_port_if_requested() {
|
|
||||||
local port="$1"
|
|
||||||
local service_name="$2"
|
|
||||||
|
|
||||||
log_warn "检测 ${service_name} 端口 ${port} 占用"
|
|
||||||
|
|
||||||
if command -v fuser >/dev/null 2>&1 && fuser "${port}/tcp" >/dev/null 2>&1; then
|
|
||||||
log_step "发现端口 ${port} 占用,正在终止"
|
|
||||||
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
|
|
||||||
log_step "发现端口 ${port} 占用,正在终止"
|
|
||||||
kill $pids 2>/dev/null || true
|
|
||||||
sleep 1
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_success "端口 ${port} 未被占用"
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_service_args() {
|
parse_service_args() {
|
||||||
BACKEND_PORT="$DEFAULT_BACKEND_PORT"
|
BACKEND_PORT="$DEFAULT_BACKEND_PORT"
|
||||||
FRONTEND_PORT="$DEFAULT_FRONTEND_PORT"
|
FRONTEND_PORT="$DEFAULT_FRONTEND_PORT"
|
||||||
@@ -580,17 +1079,43 @@ cleanup_exit_containers() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stop_container_if_running() {
|
||||||
|
local container_name="$1"
|
||||||
|
local service_name="$2"
|
||||||
|
local container_status=""
|
||||||
|
|
||||||
|
container_status="$(docker inspect --format '{{.State.Status}}' "$container_name" 2>/dev/null || true)"
|
||||||
|
if [ "$container_status" = "running" ]; then
|
||||||
|
docker stop "$container_name" >/dev/null 2>&1 || true
|
||||||
|
log_halt "${service_name}已停止"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
stop_backend_service() {
|
stop_backend_service() {
|
||||||
pkill -f "uvicorn" 2>/dev/null || true
|
if pgrep -f "uvicorn" >/dev/null 2>&1; then
|
||||||
|
cleanup_backend_processes
|
||||||
|
log_halt "后端服务已停止"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_ai_provider_service() {
|
stop_ai_provider_service() {
|
||||||
docker stop planet_aiprovider 2>/dev/null || true
|
stop_container_if_running "planet_aiprovider" "AI Provider"
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_frontend_service() {
|
stop_frontend_service() {
|
||||||
pkill -f "vite" 2>/dev/null || true
|
if pgrep -f "${SCRIPT_DIR}/frontend/node_modules/.bin/vite|bun run dev" >/dev/null 2>&1 || [ -f "$FRONTEND_PID_FILE" ]; then
|
||||||
pkill -f "bun run dev" 2>/dev/null || true
|
cleanup_frontend_processes "$DEFAULT_FRONTEND_PORT"
|
||||||
|
if [ -n "${FRONTEND_PORT:-}" ] && [ "$FRONTEND_PORT" != "$DEFAULT_FRONTEND_PORT" ]; then
|
||||||
|
cleanup_frontend_processes "$FRONTEND_PORT"
|
||||||
|
fi
|
||||||
|
log_halt "前端服务已停止"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_frontend_processes "$DEFAULT_FRONTEND_PORT"
|
||||||
|
if [ -n "${FRONTEND_PORT:-}" ] && [ "$FRONTEND_PORT" != "$DEFAULT_FRONTEND_PORT" ]; then
|
||||||
|
cleanup_frontend_processes "$FRONTEND_PORT"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
create_user() {
|
create_user() {
|
||||||
@@ -698,21 +1223,21 @@ PY
|
|||||||
log_note "邮箱: ${email}"
|
log_note "邮箱: ${email}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Top-level commands
|
||||||
start() {
|
start() {
|
||||||
parse_service_args "$@"
|
parse_service_args "$@"
|
||||||
cleanup_exit_containers
|
cleanup_exit_containers
|
||||||
|
|
||||||
print_splash
|
print_splash
|
||||||
log_step "启动智能星球计划"
|
|
||||||
|
|
||||||
start_backend_service "$BACKEND_PORT" "$BACKEND_PORT_REQUESTED" "$AI_PROVIDER_PORT"
|
start_backend_service "$BACKEND_PORT" "$BACKEND_PORT_REQUESTED" "$AI_PROVIDER_PORT"
|
||||||
start_frontend_service "$FRONTEND_PORT" "$FRONTEND_PORT_REQUESTED"
|
start_frontend_service "$FRONTEND_PORT" "$FRONTEND_PORT_REQUESTED"
|
||||||
|
|
||||||
echo ""
|
|
||||||
log_success "启动完成"
|
log_success "启动完成"
|
||||||
log_note "前端: http://localhost:${FRONTEND_PORT}"
|
log_note "智能星球计划: http://localhost:${FRONTEND_PORT}/earth"
|
||||||
log_note "后端: http://localhost:${BACKEND_PORT}"
|
log_note "智能星球仪表盘: http://localhost:${FRONTEND_PORT}/admin"
|
||||||
log_note "AI Provider: http://localhost:${AI_PROVIDER_PORT}"
|
log_note "智能星球开发文档: http://localhost:${BACKEND_PORT}/docs"
|
||||||
|
log_note "智能星球AI: http://localhost:${AI_PROVIDER_PORT}/chat"
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
@@ -720,7 +1245,8 @@ stop() {
|
|||||||
stop_backend_service
|
stop_backend_service
|
||||||
stop_ai_provider_service
|
stop_ai_provider_service
|
||||||
stop_frontend_service
|
stop_frontend_service
|
||||||
docker stop planet_postgres planet_redis 2>/dev/null || true
|
stop_container_if_running "planet_postgres" "PostgreSQL"
|
||||||
|
stop_container_if_running "planet_redis" "Redis"
|
||||||
log_success "已停止"
|
log_success "已停止"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user