#!/usr/bin/env bash # Cyrene v2 本地一键控制:启动 / 重启 / 终止 后台 cyrene 进程。 # # 用法: # bash scripts/ctrl.sh # 启动(默认) # bash scripts/ctrl.sh start # 启动 # bash scripts/ctrl.sh restart # 重启 # bash scripts/ctrl.sh stop # 终止 # bash scripts/ctrl.sh status # 查看状态 # # 说明: # - 管理单个 cyrene 后端进程(单二进制)。llama-server / PG 若需一并管理, # 可另用 docker / scripts/dev-up.sh。 # - cyrene 在仓库根下运行(读 .env 的 PORT / DATA_DIR 等),日志**追加**写 ~/cy.log # (重启不清空历史;Windows 侧 scripts\ctrl.bat 因 Start-Process 无法追加,改为 # 按时间戳归档,见该脚本的注释)。 # - 用 setsid + nohup 脱离进程组,避免终端/后台 shell 超时误杀。 set -euo pipefail REPO="$(cd "$(dirname "$0")/.." && pwd)" APP_BIN="$HOME/apps/cyrene/cyrene" PID_FILE="$HOME/.cyrene.pid" LOG_FILE="$HOME/cy.log" # 端口优先读 .env 的 PORT(用户可改),找不到才回退 10080 GW_PORT="$(grep -E '^PORT=' "$REPO/.env" 2>/dev/null | head -1 | cut -d= -f2)" GW_PORT="${GW_PORT:-10080}" HEALTH_URL="http://127.0.0.1:$GW_PORT/api/v1/health" # 读取进程 PID:先看 PID 文件,没有则从监听端口探测(能识别外部已起的 cyrene) read_pid() { # 1) PID 文件优先 if [ -f "$PID_FILE" ]; then local pid pid="$(cat "$PID_FILE" 2>/dev/null)" if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then echo "$pid" return 0 fi fi # 2) 端口探测兜底(ss 找监听 GW_PORT 的 cyrene 进程) local p p="$(ss -tlnp 2>/dev/null | grep ":$GW_PORT" | grep -oE 'pid=[0-9]+' | head -1 | sed -E 's/[^0-9]//g')" if [ -n "$p" ] && kill -0 "$p" 2>/dev/null; then echo "$p" return 0 fi return 1 } is_running() { read_pid >/dev/null 2>&1 } # 健康检查:进程在 + HTTP 200 healthy() { read_pid >/dev/null 2>&1 && curl -s --max-time 2 "$HEALTH_URL" >/dev/null 2>&1 } start() { if is_running; then echo "cyrene 已在运行 (PID $(read_pid)),跳过。用 restart 重启。" return 0 fi [ -x "$APP_BIN" ] || { echo "未找到二进制 $APP_BIN,请先 go build"; return 1; } echo "启动 cyrene → $APP_BIN (端口 $GW_PORT)" # setsid 只脱离会话,但 cyrene 仍是本 bash 的后台 job;脚本退出时 bash 会等待 # 它结束,导致 restart 命令"挂起超时"。disown 把它从作业表剔除,脚本立即返回。 # 日志**追加**(>>)而不是覆盖(>):重启不再截断上一轮日志——排障证据必须能跨重启 # 留存(「日志重启即清空」曾让一个线上问题无从取证)。 cd "$REPO" || return 1 setsid nohup "$APP_BIN" >> "$LOG_FILE" 2>&1 < /dev/null & disown # 等待健康 local i for i in $(seq 1 20); do sleep 0.5 if healthy; then echo "已启动: http://localhost:$GW_PORT (日志 $LOG_FILE)" return 0 fi done echo "启动后 ${GW_PORT} 未就绪,请查看日志 $LOG_FILE" return 1 } stop() { local pid if read_pid >/dev/null 2>&1; then pid="$(read_pid)" kill "$pid" 2>/dev/null || true echo "已发送终止信号 → PID $pid" local i for i in $(seq 1 20); do sleep 0.4 kill -0 "$pid" 2>/dev/null || { echo "已退出"; rm -f "$PID_FILE"; return 0; } done echo "进程未在 8s 内退出,强制 kill -9" kill -9 "$pid" 2>/dev/null || true rm -f "$PID_FILE" else echo "cyrene 未在运行(无 PID 文件或进程已退出)" rm -f "$PID_FILE" fi } restart() { echo "重启 cyrene…" stop sleep 1 start } status() { if healthy; then echo "cyrene: 运行中 (PID $(read_pid)) · 端口 $GW_PORT · health ok" elif is_running; then echo "cyrene: 进程在 (PID $(read_pid)) 但 health 未就绪 ($HEALTH_URL)" else echo "cyrene: 未运行" fi } usage() { echo "用法: bash scripts/ctrl.sh [start|stop|restart|status] (无参 = start)" } # start 成功后把探测到的 PID 写入文件,供后续 stop 精确管理 record_pid() { if healthy; then local p p="$(ss -tlnp 2>/dev/null | grep ":$GW_PORT" | grep -oE 'pid=[0-9]+' | head -1 | sed -E 's/[^0-9]//g')" [ -n "$p" ] && echo "$p" > "$PID_FILE" fi } case "${1:-start}" in start) start; record_pid ;; restart) restart; record_pid ;; stop) stop ;; status) status ;; *) usage; exit 1 ;; esac