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.
This commit is contained in:
Executable
+179
@@ -0,0 +1,179 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user