dev.sh 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
  4. frontend_pid=""
  5. backend_pid=""
  6. cleanup_done=0
  7. run_for_seconds="${ZBT_DEV_RUN_SECONDS:-0}"
  8. startup_timeout_seconds="${ZBT_DEV_STARTUP_TIMEOUT_SECONDS:-120}"
  9. die() {
  10. echo "错误:$*" >&2
  11. exit 1
  12. }
  13. require_command() {
  14. local command_name="$1"
  15. command -v "$command_name" >/dev/null 2>&1 || \
  16. die "未找到 $command_name,请先安装并确保它位于 PATH 中。"
  17. }
  18. assert_port_available() {
  19. local port="$1"
  20. local application_name="$2"
  21. local listeners
  22. listeners="$(lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)"
  23. if [[ -n "$listeners" ]]; then
  24. echo "$listeners" >&2
  25. die "$application_name 端口 $port 已被占用,请先结束上方进程后重试。"
  26. fi
  27. }
  28. process_group_is_alive() {
  29. local group_id="$1"
  30. [[ -n "$group_id" ]] && kill -0 -- "-$group_id" 2>/dev/null
  31. }
  32. stop_process_group() {
  33. local group_id="$1"
  34. if process_group_is_alive "$group_id"; then
  35. kill -TERM -- "-$group_id" 2>/dev/null || true
  36. fi
  37. }
  38. cleanup() {
  39. local exit_status=$?
  40. local deadline
  41. if (( cleanup_done )); then
  42. return "$exit_status"
  43. fi
  44. cleanup_done=1
  45. trap - INT TERM HUP
  46. if [[ -n "$frontend_pid" || -n "$backend_pid" ]]; then
  47. echo
  48. echo "正在停止智保通开发服务..."
  49. fi
  50. stop_process_group "$frontend_pid"
  51. stop_process_group "$backend_pid"
  52. deadline=$((SECONDS + 5))
  53. while (( SECONDS < deadline )); do
  54. if ! process_group_is_alive "$frontend_pid" && \
  55. ! process_group_is_alive "$backend_pid"; then
  56. break
  57. fi
  58. sleep 0.2
  59. done
  60. if process_group_is_alive "$frontend_pid"; then
  61. kill -KILL -- "-$frontend_pid" 2>/dev/null || true
  62. fi
  63. if process_group_is_alive "$backend_pid"; then
  64. kill -KILL -- "-$backend_pid" 2>/dev/null || true
  65. fi
  66. if [[ -n "$frontend_pid" ]]; then
  67. wait "$frontend_pid" 2>/dev/null || true
  68. fi
  69. if [[ -n "$backend_pid" ]]; then
  70. wait "$backend_pid" 2>/dev/null || true
  71. fi
  72. if [[ -n "$frontend_pid" || -n "$backend_pid" ]]; then
  73. echo "智保通开发服务已停止。"
  74. fi
  75. return "$exit_status"
  76. }
  77. http_ready() {
  78. local url="$1"
  79. curl --fail --silent --show-error --max-time 2 "$url" >/dev/null 2>&1
  80. }
  81. wait_for_services() {
  82. local deadline=$((SECONDS + startup_timeout_seconds))
  83. while (( SECONDS < deadline )); do
  84. kill -0 "$frontend_pid" 2>/dev/null || \
  85. die "前端开发服务器启动失败,请检查上方 pnpm 输出。"
  86. kill -0 "$backend_pid" 2>/dev/null || \
  87. die "后端开发服务器启动失败,请检查上方 Uvicorn 输出。"
  88. if http_ready "http://127.0.0.1:5173/" && \
  89. http_ready "http://127.0.0.1:5174/" && \
  90. http_ready "http://127.0.0.1:8000/docs"; then
  91. return 0
  92. fi
  93. sleep 0.3
  94. done
  95. die "开发服务在 ${startup_timeout_seconds} 秒内未全部就绪,请检查上方启动日志。"
  96. }
  97. case "$run_for_seconds" in
  98. ''|*[!0-9]*)
  99. die "ZBT_DEV_RUN_SECONDS 必须是非负整数。"
  100. ;;
  101. esac
  102. case "$startup_timeout_seconds" in
  103. ''|*[!0-9]*)
  104. die "ZBT_DEV_STARTUP_TIMEOUT_SECONDS 必须是正整数。"
  105. ;;
  106. 0)
  107. die "ZBT_DEV_STARTUP_TIMEOUT_SECONDS 必须大于 0。"
  108. ;;
  109. esac
  110. trap cleanup EXIT
  111. trap 'exit 130' INT
  112. trap 'exit 143' TERM
  113. trap 'exit 129' HUP
  114. require_command pnpm
  115. require_command uv
  116. require_command curl
  117. require_command lsof
  118. assert_port_available 5173 "H5"
  119. assert_port_available 5174 "管理后台"
  120. assert_port_available 8000 "后端 API"
  121. echo "正在启动 H5、管理后台和后端 API..."
  122. # Bash 3.2(macOS 系统自带版本)没有 setsid。临时启用 job control,
  123. # 可让两个后台任务各自成为独立进程组,清理时连同 Vite/Uvicorn 子进程一起结束。
  124. set -m
  125. (
  126. cd "$project_root/frontend"
  127. exec pnpm dev
  128. ) &
  129. frontend_pid=$!
  130. (
  131. cd "$project_root/backend"
  132. exec uv run uvicorn zbt.main:app \
  133. --host 127.0.0.1 \
  134. --port 8000 \
  135. --reload
  136. ) &
  137. backend_pid=$!
  138. set +m
  139. wait_for_services
  140. echo
  141. echo "智保通开发服务已全部启动:"
  142. echo "H5:http://127.0.0.1:5173"
  143. echo "管理后台:http://127.0.0.1:5174"
  144. echo "后端 API:http://127.0.0.1:8000/docs"
  145. echo "按 Ctrl+C 可一次性停止全部服务。"
  146. ready_at=$SECONDS
  147. while true; do
  148. kill -0 "$frontend_pid" 2>/dev/null || \
  149. die "前端开发服务器意外退出,请检查上方日志。"
  150. kill -0 "$backend_pid" 2>/dev/null || \
  151. die "后端开发服务器意外退出,请检查上方日志。"
  152. if (( run_for_seconds > 0 && SECONDS - ready_at >= run_for_seconds )); then
  153. break
  154. fi
  155. sleep 0.5
  156. done