Why: the manager only took configuration from SVC_* environment variables; binding a public interface or pointing at a database meant exporting vars first, which is awkward for one-off deploys and invisible in shell history review. Evidence: live matrix on the worktree - --port (8801), --host 0.0.0.0 (bind verified via ss, health probe auto-maps to 127.0.0.1), --addr whole-form, CLI-over-env precedence (--port beats SVC_DEV_ADDR), --mock reaching the server cmdline, --dsn passthrough proven by the postgres connect attempt in the log, -- passthrough (--artifacts), and make svc ARGS passthrough. A set -e silent-exit trap in env_config was caught by the first run and fixed (condition-as-last-statement). Impact: scripts/svc.sh <env> [action] [--addr|--host|--port|--data| --dsn|--auth|--mock|--plugins| -- <extra>]; precedence CLI > env vars > defaults; help rewritten with examples; make svc gains ARGS; logs keeps the positional line-count form. The real-world port guard held: a dev instance started from the main checkout was refused with owner info, untouched. Refs: feature/svc-cli-args task card.
247 lines
9.3 KiB
Bash
Executable File
247 lines
9.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# svc.sh —— 多环境服务管理:按环境隔离地 停止旧服务 → 构建最新 → 启动。
|
||
#
|
||
# 用法:
|
||
# scripts/svc.sh <dev|prod> [action] [参数...]
|
||
# action:deploy(默认)| stop | start | restart | status | logs [行数]
|
||
# scripts/svc.sh status # 全环境总览
|
||
#
|
||
# 参数(优先级:命令行 > 环境变量 > 默认值):
|
||
# --addr host:port 监听地址(整体覆盖)
|
||
# --host host 只改主机部分(如 0.0.0.0 对外开放)
|
||
# --port port 只改端口部分
|
||
# --data dir 数据目录(SQLite 与工件)
|
||
# --dsn DSN PostgreSQL 档 DSN(数据库地址/端口/库名都在 DSN 内,如
|
||
# postgres://user:pass@dbhost:5432/agentdb)——含密码时建议
|
||
# 改用环境变量 SVC_PROD_DSN 传递,避免进 shell 历史
|
||
# --auth on|off|auto API key 认证(默认 auto:PG 档开、嵌入式关)
|
||
# --mock 演示模式(dev 常用)
|
||
# --plugins dir 插件目录(默认 <repo>/plugins)
|
||
# -- <剩余参数> 原样透传给 agent-system serve(escape hatch)
|
||
#
|
||
# 环境变量默认值:
|
||
# dev: 127.0.0.1:8788 数据 runtime/dev/data;SVC_DEV_ADDR/DATA/MOCK=1 可覆盖
|
||
# prod: 127.0.0.1:8787 数据 runtime/prod/data;SVC_PROD_ADDR/DATA/DSN/AUTH 可覆盖
|
||
#
|
||
# 示例:
|
||
# scripts/svc.sh dev --port 9000 --mock
|
||
# scripts/svc.sh prod --host 0.0.0.0 --dsn postgres://u:p@127.0.0.1:5432/as
|
||
# scripts/svc.sh dev deploy --addr 0.0.0.0:8788
|
||
# make svc ENV=dev ARGS="--port 9000"
|
||
#
|
||
# 安全约束:
|
||
# - 只停自己启动过的进程(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; }
|
||
|
||
usage() {
|
||
sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
||
}
|
||
|
||
# ---- 命令行参数(覆盖层)----
|
||
CLI_ADDR=""; CLI_HOST=""; CLI_PORT=""; CLI_DATA=""; CLI_DSN=""
|
||
CLI_AUTH=""; CLI_MOCK=0; CLI_PLUGINS=""; CLI_EXTRA=""; CLI_LOGS_N=""
|
||
|
||
need_value() { [ $# -ge 2 ] || { c_err "参数 $1 需要值"; exit 2; }; }
|
||
|
||
parse_args() {
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--addr) need_value "$@"; CLI_ADDR="$2"; shift 2 ;;
|
||
--host) need_value "$@"; CLI_HOST="$2"; shift 2 ;;
|
||
--port) need_value "$@"; CLI_PORT="$2"; shift 2 ;;
|
||
--data) need_value "$@"; CLI_DATA="$2"; shift 2 ;;
|
||
--dsn) need_value "$@"; CLI_DSN="$2"; shift 2 ;;
|
||
--auth) need_value "$@"; CLI_AUTH="$2"; shift 2 ;;
|
||
--plugins) need_value "$@"; CLI_PLUGINS="$2"; shift 2 ;;
|
||
--mock) CLI_MOCK=1; shift ;;
|
||
--) shift; CLI_EXTRA="$*"; break ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
*) c_err "未知参数:$1"; usage; exit 2 ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# addr_compose 基础值(host:port)按 CLI 的 --addr/--host/--port 组合出最终地址。
|
||
addr_compose() {
|
||
local base="$1" host port
|
||
host="${base%:*}"; port="${base##*:}"
|
||
[ -n "$CLI_ADDR" ] && { echo "$CLI_ADDR"; return 0; }
|
||
[ -n "$CLI_HOST" ] && host="$CLI_HOST"
|
||
[ -n "$CLI_PORT" ] && port="$CLI_PORT"
|
||
echo "$host:$port"
|
||
}
|
||
|
||
env_config() {
|
||
case "$1" in
|
||
dev)
|
||
ENV_ADDR="$(addr_compose "${SVC_DEV_ADDR:-127.0.0.1:8788}")"
|
||
ENV_DATA="${CLI_DATA:-${SVC_DEV_DATA:-$RUNTIME/dev/data}}"
|
||
ENV_DIR="$RUNTIME/dev"
|
||
;;
|
||
prod)
|
||
ENV_ADDR="$(addr_compose "${SVC_PROD_ADDR:-127.0.0.1:8787}")"
|
||
ENV_DATA="${CLI_DATA:-${SVC_PROD_DATA:-$RUNTIME/prod/data}}"
|
||
ENV_DIR="$RUNTIME/prod"
|
||
;;
|
||
*) c_err "未知环境:$1(可用:dev | prod)"; usage; exit 2 ;;
|
||
esac
|
||
ENV_PID="$ENV_DIR/svc.pid"
|
||
ENV_LOG="$ENV_DIR/svc.log"
|
||
ENV_PLUGINS="${CLI_PLUGINS:-$ROOT/plugins}"
|
||
|
||
# 运行参数:mock/DSN/auth(CLI 与环境变量取并集,CLI 优先)。
|
||
ENV_ARGS=""
|
||
if [ "$ENV_NAME" = dev ]; then
|
||
if [ "${SVC_DEV_MOCK:-0}" = "1" ] || [ "$CLI_MOCK" = 1 ]; then
|
||
ENV_ARGS="--mock"
|
||
fi
|
||
else
|
||
local dsn="${CLI_DSN:-${SVC_PROD_DSN:-}}"
|
||
if [ -n "$dsn" ]; then ENV_ARGS="$ENV_ARGS --dsn $dsn"; fi
|
||
ENV_ARGS="$ENV_ARGS --auth ${CLI_AUTH:-${SVC_PROD_AUTH:-auto}}"
|
||
fi
|
||
}
|
||
|
||
# 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" 2>/dev/null)" || 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/restart)"
|
||
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 "处理:停掉该进程,或换端口(--port 8799 / --addr host:port)"
|
||
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 $ENV_PLUGINS $ENV_ARGS $CLI_EXTRA"
|
||
# 直接 nohup 简单命令(不经 cd 复合后台作业):$! 即服务进程本身,pidfile 语义才成立。
|
||
nohup "$BIN" serve --addr "$ENV_ADDR" --data "$ENV_DATA" --plugins "$ENV_PLUGINS" $ENV_ARGS $CLI_EXTRA \
|
||
>> "$ENV_LOG" 2>&1 &
|
||
echo $! > "$ENV_PID"
|
||
printf 'addr=%s\ndata=%s\n' "$ENV_ADDR" "$ENV_DATA" > "$ENV_DIR/svc.env"
|
||
# 健康等待:最多 20s;探测主机取绑定主机(0.0.0.0/:: → 127.0.0.1)。
|
||
local host="${ENV_ADDR%:*}" port="${ENV_ADDR##*:}"
|
||
if [ "$host" = "0.0.0.0" ] || [ "$host" = "::" ]; then host="127.0.0.1"; fi
|
||
for _ in $(seq 1 100); do
|
||
if curl -s -o /dev/null -m 2 "http://$host:$port/" 2>/dev/null; then
|
||
c_ok "[$ENV_NAME] 已启动 pid=$(cat "$ENV_PID") → http://$host:$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_NAME="$e"; env_config "$e"; env_status_line; done
|
||
exit 0
|
||
fi
|
||
ENV_NAME="$1"; shift
|
||
# 动作词可省略(省略 = deploy);flags 可直接跟随环境名。
|
||
local action="deploy"
|
||
case "${1:-}" in
|
||
deploy|stop|start|restart|status|logs) action="$1"; shift ;;
|
||
esac
|
||
# logs 的行数(兼容 svc.sh dev logs 100)。
|
||
if [ "$action" = "logs" ] && [ "${1:-}" != "" ] && [[ "$1" =~ ^[0-9]+$ ]]; then
|
||
CLI_LOGS_N="$1"; shift
|
||
fi
|
||
parse_args "$@"
|
||
env_config "$ENV_NAME"
|
||
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 "${CLI_LOGS_N:-50}" -f "$ENV_LOG" || c_warn "[$ENV_NAME] 暂无日志" ;;
|
||
esac
|
||
}
|
||
|
||
main "$@"
|