start_server.sh 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. #!/usr/bin/env bash
  2. # deep_agent 2.0 服务管理脚本
  3. #
  4. # bash scripts/start_server.sh start|stop|restart|status|logs
  5. #
  6. # 单进程是硬约束(见 AGENTS.md 不变式 2):SQLite checkpointer、并发闸计数、
  7. # APScheduler 都是进程内的,多 worker 会导致会话状态分裂、定时任务重复触发。
  8. # 所以这里固定 --workers 1,不要改成多 worker。
  9. set -uo pipefail
  10. APP_NAME="deep_agent_2.0"
  11. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  12. PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
  13. cd "${PROJECT_DIR}" || exit 1
  14. PID_FILE="${PROJECT_DIR}/.tmp/${APP_NAME}.pid"
  15. LOG_FILE="${PROJECT_DIR}/.tmp/${APP_NAME}.log"
  16. mkdir -p "${PROJECT_DIR}/.tmp"
  17. # 端口从 .env 读,与代码保持同一事实源(默认 8080)
  18. PORT="${PORT:-$(grep -E '^PORT=' .env 2>/dev/null | head -1 | cut -d= -f2 | tr -d '[:space:]')}"
  19. PORT="${PORT:-8080}"
  20. PYTHON="${PYTHON:-python}"
  21. is_running() {
  22. [ -f "${PID_FILE}" ] || return 1
  23. local pid
  24. pid="$(cat "${PID_FILE}" 2>/dev/null)"
  25. [ -n "${pid}" ] || return 1
  26. kill -0 "${pid}" 2>/dev/null
  27. }
  28. wait_port_free() {
  29. # 等端口释放,最多 10 秒。不检查就重启会撞上「Address already in use」,
  30. # 而旧进程其实还在跑——表面看是新进程启动失败,实际是没停干净。
  31. for _ in $(seq 1 20); do
  32. if ! netstat -ano 2>/dev/null | grep -q ":${PORT} .*LISTENING"; then
  33. return 0
  34. fi
  35. sleep 0.5
  36. done
  37. return 1
  38. }
  39. start() {
  40. if is_running; then
  41. echo "${APP_NAME} 已在运行(PID $(cat "${PID_FILE}"))"
  42. return 0
  43. fi
  44. echo "预检:结构自检 + 报告运行时"
  45. if ! "${PYTHON}" scripts/selfcheck.py; then
  46. echo "自检未通过,拒绝启动(避免起一个半可用的服务)"
  47. return 1
  48. fi
  49. echo "启动 ${APP_NAME} 端口=${PORT}"
  50. nohup "${PYTHON}" -m uvicorn main:app \
  51. --host 0.0.0.0 --port "${PORT}" --workers 1 \
  52. >> "${LOG_FILE}" 2>&1 &
  53. echo $! > "${PID_FILE}"
  54. for _ in $(seq 1 60); do
  55. if curl -sf "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then
  56. echo "已就绪:http://127.0.0.1:${PORT} (日志 ${LOG_FILE})"
  57. return 0
  58. fi
  59. sleep 1
  60. done
  61. echo "启动超时,最后 30 行日志:"
  62. tail -30 "${LOG_FILE}"
  63. return 1
  64. }
  65. stop() {
  66. if ! is_running; then
  67. echo "${APP_NAME} 未在运行"
  68. # 端口仍被占用说明有别的进程(可能是上次没写 PID 的孤儿),报出来
  69. if netstat -ano 2>/dev/null | grep -q ":${PORT} .*LISTENING"; then
  70. echo "警告:端口 ${PORT} 仍被占用,可能是孤儿进程,请手动处理"
  71. fi
  72. return 0
  73. fi
  74. local pid
  75. pid="$(cat "${PID_FILE}")"
  76. echo "停止 ${APP_NAME}(PID ${pid})"
  77. kill "${pid}" 2>/dev/null
  78. # 先 SIGTERM 给 lifespan 收尾的机会(关闭调度器与 checkpointer),10 秒后再强杀
  79. for _ in $(seq 1 20); do
  80. kill -0 "${pid}" 2>/dev/null || break
  81. sleep 0.5
  82. done
  83. if kill -0 "${pid}" 2>/dev/null; then
  84. echo "优雅停止超时,强制终止"
  85. kill -9 "${pid}" 2>/dev/null
  86. fi
  87. rm -f "${PID_FILE}"
  88. wait_port_free || echo "警告:端口 ${PORT} 仍未释放"
  89. echo "已停止"
  90. }
  91. status() {
  92. if is_running; then
  93. echo "${APP_NAME} 运行中(PID $(cat "${PID_FILE}"),端口 ${PORT})"
  94. curl -s "http://127.0.0.1:${PORT}/health" | "${PYTHON}" -m json.tool 2>/dev/null | head -30
  95. else
  96. echo "${APP_NAME} 未运行"
  97. fi
  98. }
  99. case "${1:-status}" in
  100. start) start ;;
  101. stop) stop ;;
  102. restart) stop; start ;;
  103. status) status ;;
  104. logs) tail -n 100 -f "${LOG_FILE}" ;;
  105. *)
  106. echo "用法:bash scripts/start_server.sh {start|stop|restart|status|logs}"
  107. exit 2
  108. ;;
  109. esac