| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- #!/usr/bin/env bash
- # deep_agent 2.0 服务管理脚本
- #
- # bash scripts/start_server.sh start|stop|restart|status|logs
- #
- # 单进程是硬约束(见 AGENTS.md 不变式 2):SQLite checkpointer、并发闸计数、
- # APScheduler 都是进程内的,多 worker 会导致会话状态分裂、定时任务重复触发。
- # 所以这里固定 --workers 1,不要改成多 worker。
- set -uo pipefail
- APP_NAME="deep_agent_2.0"
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
- cd "${PROJECT_DIR}" || exit 1
- PID_FILE="${PROJECT_DIR}/.tmp/${APP_NAME}.pid"
- LOG_FILE="${PROJECT_DIR}/.tmp/${APP_NAME}.log"
- mkdir -p "${PROJECT_DIR}/.tmp"
- # 端口从 .env 读,与代码保持同一事实源(默认 8080)
- PORT="${PORT:-$(grep -E '^PORT=' .env 2>/dev/null | head -1 | cut -d= -f2 | tr -d '[:space:]')}"
- PORT="${PORT:-8080}"
- PYTHON="${PYTHON:-python}"
- is_running() {
- [ -f "${PID_FILE}" ] || return 1
- local pid
- pid="$(cat "${PID_FILE}" 2>/dev/null)"
- [ -n "${pid}" ] || return 1
- kill -0 "${pid}" 2>/dev/null
- }
- wait_port_free() {
- # 等端口释放,最多 10 秒。不检查就重启会撞上「Address already in use」,
- # 而旧进程其实还在跑——表面看是新进程启动失败,实际是没停干净。
- for _ in $(seq 1 20); do
- if ! netstat -ano 2>/dev/null | grep -q ":${PORT} .*LISTENING"; then
- return 0
- fi
- sleep 0.5
- done
- return 1
- }
- start() {
- if is_running; then
- echo "${APP_NAME} 已在运行(PID $(cat "${PID_FILE}"))"
- return 0
- fi
- echo "预检:结构自检 + 报告运行时"
- if ! "${PYTHON}" scripts/selfcheck.py; then
- echo "自检未通过,拒绝启动(避免起一个半可用的服务)"
- return 1
- fi
- echo "启动 ${APP_NAME} 端口=${PORT}"
- nohup "${PYTHON}" -m uvicorn main:app \
- --host 0.0.0.0 --port "${PORT}" --workers 1 \
- >> "${LOG_FILE}" 2>&1 &
- echo $! > "${PID_FILE}"
- for _ in $(seq 1 60); do
- if curl -sf "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then
- echo "已就绪:http://127.0.0.1:${PORT} (日志 ${LOG_FILE})"
- return 0
- fi
- sleep 1
- done
- echo "启动超时,最后 30 行日志:"
- tail -30 "${LOG_FILE}"
- return 1
- }
- stop() {
- if ! is_running; then
- echo "${APP_NAME} 未在运行"
- # 端口仍被占用说明有别的进程(可能是上次没写 PID 的孤儿),报出来
- if netstat -ano 2>/dev/null | grep -q ":${PORT} .*LISTENING"; then
- echo "警告:端口 ${PORT} 仍被占用,可能是孤儿进程,请手动处理"
- fi
- return 0
- fi
- local pid
- pid="$(cat "${PID_FILE}")"
- echo "停止 ${APP_NAME}(PID ${pid})"
- kill "${pid}" 2>/dev/null
- # 先 SIGTERM 给 lifespan 收尾的机会(关闭调度器与 checkpointer),10 秒后再强杀
- for _ in $(seq 1 20); do
- kill -0 "${pid}" 2>/dev/null || break
- sleep 0.5
- done
- if kill -0 "${pid}" 2>/dev/null; then
- echo "优雅停止超时,强制终止"
- kill -9 "${pid}" 2>/dev/null
- fi
- rm -f "${PID_FILE}"
- wait_port_free || echo "警告:端口 ${PORT} 仍未释放"
- echo "已停止"
- }
- status() {
- if is_running; then
- echo "${APP_NAME} 运行中(PID $(cat "${PID_FILE}"),端口 ${PORT})"
- curl -s "http://127.0.0.1:${PORT}/health" | "${PYTHON}" -m json.tool 2>/dev/null | head -30
- else
- echo "${APP_NAME} 未运行"
- fi
- }
- case "${1:-status}" in
- start) start ;;
- stop) stop ;;
- restart) stop; start ;;
- status) status ;;
- logs) tail -n 100 -f "${LOG_FILE}" ;;
- *)
- echo "用法:bash scripts/start_server.sh {start|stop|restart|status|logs}"
- exit 2
- ;;
- esac
|