#!/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