Files
Agent-System-Dev/scripts/svc.sh
T
weichen 35d114307d Add the svc.sh environment service manager
Why: deploying a new build meant manually finding and killing the old
process, rebuilding and re-launching with the right flags; dev and prod
instances could collide on ports, data dirs or kill each other.

Evidence: live matrix on the worktree - dev deploy (stop -> make build
-> start with health check), redeploy swaps the pid and keeps seeded
data, prod deploys alongside dev on separate port/data with fully
isolated /api/projects, port takeover is refused with the owning
process named while the running env stays up, stop clears pidfile and
frees the port. A pid-capture bug (compound-background subshell wrote
the wrapper pid instead of the server) was caught and fixed during the
first live run.

Impact: scripts/svc.sh <dev|prod> [deploy|stop|start|restart|status|
logs]; runtime/<env>/{data,svc.pid,svc.log,svc.env} isolation; defaults
dev 127.0.0.1:8788 / prod 127.0.0.1:8787 overridable via SVC_* env
vars (incl. SVC_PROD_DSN for the PG tier); make svc ENV=... shortcut;
runtime/ gitignored. Safety: only processes started by the script are
stopped (pidfile + /proc cmdline double check).

Refs: feature/svc-env-script task card.
2026-09-30 11:38:43 +00:00

180 lines
6.6 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# svc.sh —— 多环境服务管理:按环境隔离地 停止旧服务 → 构建最新 → 启动。
#
# 用法:
# scripts/svc.sh dev # 部署 dev(停止旧 dev → make build → 启动)
# scripts/svc.sh prod # 部署 prod(dev 不受影响;反之亦然)
# scripts/svc.sh dev stop|start|restart|status|logs
# scripts/svc.sh status # 全环境总览
#
# 环境隔离(默认值,可用环境变量覆盖):
# dev: 127.0.0.1:8788 数据 runtime/dev/data 日志 runtime/dev/svc.log
# prod: 127.0.0.1:8787 数据 runtime/prod/data 日志 runtime/prod/svc.log
# 覆盖:SVC_DEV_ADDR / SVC_PROD_ADDR、SVC_DEV_DATA / SVC_PROD_DATA、
# SVC_DEV_MOCK=1(dev 加 --mock)、SVC_PROD_DSN(PG 档)、
# SVC_PROD_AUTH(on|off|auto,默认 auto)。
#
# 安全约束:
# - 只停自己启动过的进程(pidfile + /proc/<pid>/cmdline 双重校验),绝不动他人现场;
# - 端口被非本脚本进程占用时明确报错并给出占用者信息,不抢不杀。
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RUNTIME="$ROOT/runtime"
BIN="$ROOT/bin/agent-system"
c_info() { printf '\033[1;34m[svc]\033[0m %s\n' "$*"; }
c_ok() { printf '\033[1;32m[svc]\033[0m %s\n' "$*"; }
c_warn() { printf '\033[1;33m[svc]\033[0m %s\n' "$*"; }
c_err() { printf '\033[1;31m[svc]\033[0m %s\n' "$*" >&2; }
env_config() {
case "$1" in
dev)
ENV_ADDR="${SVC_DEV_ADDR:-127.0.0.1:8788}"
ENV_DATA="${SVC_DEV_DATA:-$RUNTIME/dev/data}"
ENV_DIR="$RUNTIME/dev"
ENV_ARGS=""
[ "${SVC_DEV_MOCK:-0}" = "1" ] && ENV_ARGS="--mock"
;;
prod)
ENV_ADDR="${SVC_PROD_ADDR:-127.0.0.1:8787}"
ENV_DATA="${SVC_PROD_DATA:-$RUNTIME/prod/data}"
ENV_DIR="$RUNTIME/prod"
ENV_ARGS=""
[ -n "${SVC_PROD_DSN:-}" ] && ENV_ARGS="$ENV_ARGS --dsn $SVC_PROD_DSN"
ENV_ARGS="$ENV_ARGS --auth ${SVC_PROD_AUTH:-auto}"
;;
*) c_err "未知环境:$1(可用:dev | prod)"; usage; exit 2 ;;
esac
ENV_PID="$ENV_DIR/svc.pid"
ENV_LOG="$ENV_DIR/svc.log"
}
usage() {
sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
}
# pid_alive <pid>:进程存在且 cmdline 是我们的 serve(防 PID 复用误杀)。
pid_alive() {
local pid="$1"
[ -n "$pid" ] && [ -r "/proc/$pid/cmdline" ] || return 1
local cmd
cmd="$(tr '\0' ' ' < "/proc/$pid/cmdline")" || return 1
case "$cmd" in
*agent-system*serve*) return 0 ;;
*) return 1 ;;
esac
}
read_pid() { [ -f "$ENV_PID" ] && cat "$ENV_PID" 2>/dev/null || true; }
do_stop() {
local pid; pid="$(read_pid)"
if [ -z "$pid" ]; then
c_info "[$ENV_NAME] 无 pidfile——没有由本脚本启动的实例"
return 0
fi
if ! pid_alive "$pid"; then
c_warn "[$ENV_NAME] pidfile 指向的进程已不存在(pid=$pid),清理 pidfile"
rm -f "$ENV_PID"
return 0
fi
c_info "[$ENV_NAME] 停止旧服务 pid=$pid(SIGTERM,等待优雅停机)"
kill -TERM "$pid"
for _ in $(seq 1 100); do
pid_alive "$pid" || { c_ok "[$ENV_NAME] 已停止"; rm -f "$ENV_PID"; return 0; }
sleep 0.1
done
c_warn "[$ENV_NAME] 10s 未退出,SIGKILL 兜底"
kill -KILL "$pid" 2>/dev/null || true
rm -f "$ENV_PID"
c_ok "[$ENV_NAME] 已强制停止"
}
port_owner() {
local port="${ENV_ADDR##*:}"
if command -v ss >/dev/null 2>&1; then
ss -ltnp 2>/dev/null | awk -v p=":$port" '$4 ~ p"$" {print $0; exit}'
fi
}
do_build() {
c_info "[$ENV_NAME] 构建最新组合(make build:前端 + 内嵌后端)"
if ! make -C "$ROOT" build; then
c_err "[$ENV_NAME] 构建失败——旧服务已停止,本次部署中止(修复后重跑)"
exit 1
fi
}
do_start() {
if pid_alive "$(read_pid)"; then
c_warn "[$ENV_NAME] 已在运行 pid=$(read_pid)(如需换新版本请用 deploy)"
return 0
fi
mkdir -p "$ENV_DATA" "$ENV_DIR"
# 端口占用预检:他人现场不动,只报告。
local owner; owner="$(port_owner)"
if [ -n "$owner" ]; then
c_err "[$ENV_NAME] 端口被占用(非本脚本管理):$owner"
c_err "处理:停掉该进程,或用环境变量换端口(如 SVC_${ENV_NAME^^}_ADDR=127.0.0.1:8799)"
exit 1
fi
[ -x "$BIN" ] || { c_err "[$ENV_NAME] 缺少二进制 $BIN——先构建(deploy 会自动构建)"; exit 1; }
c_info "[$ENV_NAME] 启动:$BIN serve --addr $ENV_ADDR --data $ENV_DATA --plugins $ROOT/plugins $ENV_ARGS"
# 直接 nohup 简单命令(不经 cd 复合后台作业):$! 即服务进程本身,pidfile 语义才成立。
nohup "$BIN" serve --addr "$ENV_ADDR" --data "$ENV_DATA" --plugins "$ROOT/plugins" $ENV_ARGS \
>> "$ENV_LOG" 2>&1 &
echo $! > "$ENV_PID"
printf 'addr=%s\ndata=%s\n' "$ENV_ADDR" "$ENV_DATA" > "$ENV_DIR/svc.env"
# 健康等待:最多 20s。
local port="${ENV_ADDR##*:}"
for _ in $(seq 1 100); do
if curl -s -o /dev/null -m 2 "http://127.0.0.1:$port/" 2>/dev/null; then
c_ok "[$ENV_NAME] 已启动 pid=$(cat "$ENV_PID") → http://127.0.0.1:$port(日志:$ENV_LOG)"
return 0
fi
pid_alive "$(read_pid)" || { c_err "[$ENV_NAME] 进程启动后即退出,查看日志:tail -40 $ENV_LOG"; exit 1; }
sleep 0.2
done
c_err "[$ENV_NAME] 20s 健康检查未通过(进程存活但未响应),查看日志:tail -40 $ENV_LOG"
exit 1
}
env_status_line() {
local pid; pid="$(read_pid)"
# 运行态显示实际启动参数(svc.env 由 start 落盘);未运行显示当前配置默认值。
local addr="$ENV_ADDR" data="$ENV_DATA"
if pid_alive "$pid" && [ -f "$ENV_DIR/svc.env" ]; then
addr="$(sed -n 's/^addr=//p' "$ENV_DIR/svc.env")"
data="$(sed -n 's/^data=//p' "$ENV_DIR/svc.env")"
fi
if pid_alive "$pid"; then
printf ' %-5s 运行中 pid=%-7s %s 数据 %s\n' "$ENV_NAME" "$pid" "$addr" "$data"
else
printf ' %-5s 未运行 %s 数据 %s\n' "$ENV_NAME" "$addr" "$data"
fi
}
main() {
[ $# -ge 1 ] || { usage; exit 2; }
if [ "$1" = "status" ] && [ $# -eq 1 ]; then
c_info "服务总览(root: $ROOT)"
for e in dev prod; do env_config "$e"; ENV_NAME="$e"; env_status_line; done
exit 0
fi
ENV_NAME="$1"; env_config "$ENV_NAME"
local action="${2:-deploy}"
case "$action" in
deploy) do_stop; do_build; do_start ;;
stop) do_stop ;;
start) do_start ;;
restart) do_stop; do_start ;;
status) env_status_line ;;
logs) [ -f "$ENV_LOG" ] && tail -n "${3:-50}" -f "$ENV_LOG" || c_warn "[$ENV_NAME] 暂无日志" ;;
*) c_err "未知动作:$action(可用:deploy|stop|start|restart|status|logs)"; usage; exit 2 ;;
esac
}
main "$@"