Add service config-file switching with schema validation to svc.sh
Why: beyond database binding, services needed a way to switch the JSON config file they run with and have the new config take effect promptly; hand-editing exec= risks restarting a service into an invalid config. Evidence: in the task worktree with a real agent binary -- a config with an unknown key is rejected by the binary's strict schema check before any state change; a valid file binds via --config in exec= + conffile= (survives record_state rewrites), restarts the running service, and the config's dsn verifiably drives the process (wrong-password variant failed PG auth in the log; correct variant connected and created 21 tables); editing the file and re-running 'config <same file>' re-validates and restarts (pid changes); '--clear' removes both the flag and conffile; db and config coexist in exec=; view mode shows masked JSON content plus 'config explain' effective values with masked dsn; list DB column also resolves dsns that live only inside the config file. Full log in the task card. Impact: new service-level action 'svc.sh <name> config [file | --clear] [--no-restart]' for agent services; config-file content snapshots (masked, --reveal to show) in info; a config-file line in status; conffile persisted in svc.env. No agent binary changes. Refs: task card .task/feature-svc-db-bind.md (same doc as PR #182); docs/svc.md sections 2/4.5/4.6/6/10.1
This commit is contained in:
+183
-13
@@ -6,6 +6,7 @@
|
||||
# ════════════════════ 命令一览 ════════════════════
|
||||
# scripts/svc.sh <name> [action] [参数...] 服务级:deploy(默认)|start|stop|restart|status|logs [N]
|
||||
# |db [pg名|--clear](agent 数据库绑定/切换)
|
||||
# |config [file|--clear](agent 配置文件更新/生效)
|
||||
# scripts/svc.sh list [--tag T] 服务总览(status 等价)
|
||||
# scripts/svc.sh info [name] [--tag T] [--reveal] 综合报告(只读):服务/数据库/配置快照/最近事件
|
||||
# scripts/svc.sh set <name> [--port N --apply] [--tag T] 变更登记(换端口/标签)
|
||||
@@ -42,6 +43,8 @@
|
||||
# --plugins/--dsn/--auth/--mock/--data agent 运行参数
|
||||
# <name> db <pg名> [--db X] [--no-restart] agent 绑定/切换数据库(实例运行中缺库自动建;
|
||||
# --clear 解绑回内嵌 SQLite;无参查看当前绑定与可用实例)
|
||||
# <name> config <file.json> [--no-restart] agent 更换使用的配置文件(schema 校验通过才切换,
|
||||
# 运行中自动重启即时生效;--clear 移除;无参查看当前配置与生效值)
|
||||
# --model-base/--model-key/--model-name 与 --light-*/--tavily-keys 启动后播种设置
|
||||
# -- <剩余参数> 原样透传给 agent-system serve(escape hatch)
|
||||
# db create:--user U(默认 agent)--password P(缺省自动生成)--db NAME --auth-mode scram-sha-256|md5
|
||||
@@ -75,6 +78,8 @@
|
||||
# scripts/svc.sh dev db pg1 --db dev # 把 dev 服务绑定到 pg1/dev 库(自动重启)
|
||||
# scripts/svc.sh dev db pg2 --no-restart # 切换但不重启(下次 start 生效)
|
||||
# scripts/svc.sh dev db --clear # 解绑回内嵌 SQLite 档
|
||||
# scripts/svc.sh dev config .svc/dev.json # 更换配置文件(校验+重启即时生效)
|
||||
# scripts/svc.sh dev config # 查看当前配置文件与生效值
|
||||
# scripts/svc.sh db copy pg1 pg2 --db app # 把 pg1 的 app 库搬到 pg2
|
||||
# scripts/svc.sh db restore pg2 --file <dump> --db app --force
|
||||
# scripts/svc.sh list --tag core && scripts/svc.sh history
|
||||
@@ -130,6 +135,7 @@ svc.sh —— 本机服务管理:后端(agent) / 前端(static) / 数据库(po
|
||||
scripts/svc.sh <name> start|stop|restart|logs [N] 生命周期与日志
|
||||
scripts/svc.sh <name> status 服务详情(版本/来源/DSN 等)
|
||||
scripts/svc.sh <name> db [pg名|--clear] agent 数据库绑定/切换(缺省查看)
|
||||
scripts/svc.sh <name> config [file|--clear] agent 配置文件更新/即时生效(缺省查看)
|
||||
scripts/svc.sh db create pg1 --port 8890 建数据库
|
||||
scripts/svc.sh db dsn|backup|backups|restore|clone|copy|ls|set pg1 ... 数据库运维
|
||||
scripts/svc.sh set pg1 --port 8895 --apply 换端口(--tag 改标签)
|
||||
@@ -264,6 +270,10 @@ EOF
|
||||
绑定与可用实例;给 pg名=绑定/切换(DSN 写入 exec=,dbref= 登记,
|
||||
实例运行中缺库自动建,服务运行中自动重启生效);--clear 解绑回内嵌
|
||||
SQLite 档;--no-restart 只改登记
|
||||
scripts/svc.sh <name> config [file|--clear] [--no-restart] agent 配置文件:缺省查看当前
|
||||
文件与生效值;给 file=更换(agent 二进制 --config,JSON schema 校验
|
||||
通过才切换,运行中自动重启即时生效);--clear 移除回命令行参数形态。
|
||||
绑定后改文件内容 + restart 即生效(agent 无热重载,重启秒级)
|
||||
scripts/svc.sh <name> logs [N] 跟随日志(默认 50 行起;如 svc.sh pg1 logs 200)
|
||||
scripts/svc.sh <name> status 单服务详情(kind/状态/地址/来源/版本/标签/DSN/目录)
|
||||
scripts/svc.sh list [--tag T] 全服务表格:NAME KIND STATE PID ADDR SOURCE TAGS
|
||||
@@ -394,6 +404,11 @@ dotenv 格式:KEY=VALUE,支持 # 注释、成对引号、export 前缀;只
|
||||
- 裸 start/restart 复用 exec=(上次完整参数);任何显式参数(或 --config 文件、
|
||||
SVC_* 变量)出现即整体重建参数
|
||||
- tags 随每次登记合并去重,跨重启保留
|
||||
|
||||
服务级配置文件(与上述 svc.sh dotenv 层不同——这是 agent 二进制自己的 JSON 配置):
|
||||
scripts/svc.sh <name> config [file|--clear] 更换/移除/查看(见 help lifecycle);
|
||||
切换前 schema 校验(agent-system config explain -config <file>),运行中自动重启
|
||||
即时生效;登记于 svc.env conffile=,info/status 可见。
|
||||
EOF
|
||||
return 0 ;;
|
||||
examples)
|
||||
@@ -644,15 +659,17 @@ svc_merge_tags() {
|
||||
}
|
||||
|
||||
# record_state EXTRA_KEY=VALUE...:把当前态+持久化配置写入 svc.env(600)。
|
||||
# 基本键:kind/env/pid/addr/tags/source/version/started/pidcheck/exec + dbref(跨重启
|
||||
# 保留的数据库绑定);kind 特有键(user/password/dbname/authmode/pgdata/htdocs...)经参数追加。
|
||||
# 基本键:kind/env/pid/addr/tags/source/version/started/pidcheck/exec + dbref/
|
||||
# conffile(跨重启保留的数据库绑定与配置文件);kind 特有键(user/password/dbname/
|
||||
# authmode/pgdata/htdocs...)经参数追加。
|
||||
record_state() {
|
||||
# 先读完旧文件再开重定向(块内 {} > svc.env 会先截断文件,读旧值必须在其之前)。
|
||||
local pid prev_src prev_dbref merged_tags
|
||||
local pid prev_src prev_dbref prev_conffile merged_tags
|
||||
pid="$(read_pid)"
|
||||
prev_src=""
|
||||
[ -f "$ENV_DIR/svc.env" ] && prev_src="$(svc_get source)"
|
||||
prev_dbref="$(svc_get dbref)"
|
||||
prev_conffile="$(svc_get conffile)"
|
||||
SVC_SOURCE="${SRC_LABEL:-${prev_src:-}}"
|
||||
merged_tags="$(svc_merge_tags)"
|
||||
mkdir -p "$ENV_DIR"
|
||||
@@ -668,6 +685,7 @@ record_state() {
|
||||
printf 'pidcheck=%s\n' "${SVC_PIDCHECK:-}"
|
||||
printf 'exec=%s\n' "${SVC_EXEC:-}"
|
||||
[ -n "$prev_dbref" ] && printf 'dbref=%s\n' "$prev_dbref"
|
||||
[ -n "$prev_conffile" ] && printf 'conffile=%s\n' "$prev_conffile"
|
||||
[ $# -gt 0 ] && printf '%s\n' "$@"
|
||||
} > "$ENV_DIR/svc.env"
|
||||
chmod 600 "$ENV_DIR/svc.env"
|
||||
@@ -1895,6 +1913,127 @@ db_ls() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# mask_json TEXT:JSON 文本里键名含 key/pass/secret/token/dsn(不区分大小写)的
|
||||
# 字符串值掩为 ***(配置文件内容快照用)。输出带尾换行。
|
||||
mask_json() {
|
||||
printf '%s\n' "$1" | sed -E 's/("[^"]*(key|pass|secret|token|dsn)[^"]*"[[:space:]]*:[[:space:]]*")[^"]*"/\1***"/Ig'
|
||||
}
|
||||
|
||||
# ---- agent 服务的配置文件(<name> config ...)----
|
||||
|
||||
# svc_config [FILE|--clear] [--no-restart]:查看 / 更新 / 移除 agent 服务使用的
|
||||
# 配置文件(agent 二进制的 --config,JSON,schema 严格校验)。更新 = 校验通过后
|
||||
# 把 --config <绝对路径> 写入持久化 exec= 并登记 conffile=,运行中自动重启生效
|
||||
# (agent 无 SIGHUP 热重载,重启即"及时生效",秒级);--clear 移除回命令行参数形态。
|
||||
svc_config() {
|
||||
[ "$SVC_KIND" = agent ] || { c_err "[$ENV_NAME] kind=$SVC_KIND 无配置文件绑定(仅 agent 服务)"; return 2; }
|
||||
[ -f "$ENV_DIR/svc.env" ] || { c_err "[$ENV_NAME] 无登记(先 deploy)"; return 2; }
|
||||
local target="" clear=0 restart=1
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--clear) clear=1; shift ;;
|
||||
--no-restart) restart=0; shift ;;
|
||||
-h|--help)
|
||||
cat >&2 <<EOF
|
||||
[$ENV_NAME] 配置文件:
|
||||
scripts/svc.sh $ENV_NAME config 查看当前配置文件与生效值
|
||||
scripts/svc.sh $ENV_NAME config <file.json> [--no-restart] 更新使用的配置文件
|
||||
(schema 校验通过才切换;运行中自动重启生效)
|
||||
scripts/svc.sh $ENV_NAME config --clear [--no-restart] 移除(回命令行参数形态)
|
||||
提示:配置文件绑定后,改文件内容 + restart 即生效;config <同一文件> 也会校验并重启。
|
||||
EOF
|
||||
return 0 ;;
|
||||
-*) c_err "[$ENV_NAME] config 未知参数:$1(可用:<文件> | --clear,--no-restart)"; return 2 ;;
|
||||
*) [ -z "$target" ] || { c_err "[$ENV_NAME] config 只接受一个文件路径"; return 2; }
|
||||
target="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 当前配置文件(conffile= 优先;旧服务回退 exec= 的 --config)。
|
||||
local cur=""
|
||||
cur="$(svc_get conffile)"
|
||||
[ -z "$cur" ] && cur="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')"
|
||||
|
||||
# 无参:查看(文件状态 + 掩码内容 + 二进制视角的生效值)。
|
||||
if [ -z "$target" ] && [ "$clear" = 0 ]; then
|
||||
if [ -n "$cur" ]; then
|
||||
printf ' [%s] 配置文件:%s\n' "$ENV_NAME" "$cur"
|
||||
if [ -f "$cur" ]; then
|
||||
printf ' 权限 %s;内容(敏感值掩码):\n' "$(info_file_mode "$cur")"
|
||||
mask_json "$(cat "$cur")" | sed 's/^/ /'
|
||||
if [ -x "$ENV_BIN" ] && "$ENV_BIN" config explain -config "$cur" >/dev/null 2>&1; then
|
||||
printf ' 生效值(agent 视角,敏感值掩码):\n'
|
||||
"$ENV_BIN" config explain -config "$cur" 2>/dev/null \
|
||||
| sed -E 's#(postgres(ql)?://[^:/@[:space:]]+:)[^@[:space:]]+@#\1***@#g; s/^/ /'
|
||||
else
|
||||
printf ' (当前产物无法校验该文件:config explain 不可用或文件不合法)\n'
|
||||
fi
|
||||
else
|
||||
printf ' 文件不存在!重启将失败——请恢复文件或 config --clear\n'
|
||||
fi
|
||||
else
|
||||
printf ' [%s] 配置文件:无(命令行参数形态;绑定:config <file.json>)\n' "$ENV_NAME"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 移除。
|
||||
if [ "$clear" = 1 ]; then
|
||||
[ -n "$target" ] && { c_err "--clear 不接受文件参数"; return 2; }
|
||||
local exec_old new_exec
|
||||
exec_old="$(svc_get exec)"
|
||||
if printf '%s' "$exec_old" | grep -q -- '--config '; then
|
||||
new_exec="$(printf '%s' "$exec_old" | sed -E 's/(^| )--config [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
||||
svc_set_key exec "$new_exec"
|
||||
fi
|
||||
svc_del_key conffile
|
||||
hist_event set ok "$(read_pid)" "config unbind"
|
||||
c_ok "[$ENV_NAME] 已移除配置文件(回命令行参数形态)"
|
||||
if [ "$restart" = 1 ] && [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then
|
||||
do_stop; do_start || { c_err "[$ENV_NAME] 重启失败——移除已登记,排查后 start"; return 1; }
|
||||
else
|
||||
c_info "[$ENV_NAME] 未运行或 --no-restart——下次 start 生效"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 更新。
|
||||
[ -f "$target" ] || { c_err "配置文件不存在:$target"; return 2; }
|
||||
local abs
|
||||
abs="$(cd "$(dirname "$target")" && pwd)/$(basename "$target")"
|
||||
if [ -x "$ENV_BIN" ]; then
|
||||
if ! "$ENV_BIN" config explain -config "$abs" >/dev/null 2>&1; then
|
||||
c_err "[$ENV_NAME] 配置文件未通过 schema 校验:$abs"
|
||||
"$ENV_BIN" config explain -config "$abs" >&2 || true
|
||||
c_err "(键与取值以 config explain 为准;先修正文件再切换)"
|
||||
return 2
|
||||
fi
|
||||
if ! "$ENV_BIN" serve -h 2>&1 | grep -Eq -- "(^|[[:space:]])-{1,2}config([[:space:]]|=|$)"; then
|
||||
c_warn "[$ENV_NAME] 当前产物不识别 --config(旧版本二进制)——绑定已登记,但重启将忽略该参数"
|
||||
fi
|
||||
fi
|
||||
local exec_old new_exec
|
||||
exec_old="$(svc_get exec)"
|
||||
if printf '%s' "$exec_old" | grep -q -- '--config '; then
|
||||
new_exec="$(printf '%s' "$exec_old" | sed -E "s|--config [^ ]+|--config $abs|")"
|
||||
else
|
||||
new_exec="${exec_old:+$exec_old }--config $abs"
|
||||
fi
|
||||
svc_set_key exec "$new_exec"
|
||||
svc_set_key conffile "$abs"
|
||||
hist_event set ok "$(read_pid)" "config → $abs"
|
||||
c_ok "[$ENV_NAME] 配置文件已更新:$abs(schema 校验通过;svc.env 600 保存)"
|
||||
if [ "$restart" = 1 ] && [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then
|
||||
c_info "[$ENV_NAME] 重启以应用新配置……"
|
||||
do_stop
|
||||
do_start || { c_err "[$ENV_NAME] 重启失败——绑定已登记,排查日志后 scripts/svc.sh $ENV_NAME start"; return 1; }
|
||||
c_ok "[$ENV_NAME] 新配置已生效:$abs"
|
||||
else
|
||||
c_info "[$ENV_NAME] 未运行或 --no-restart——下次 start 生效"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- agent 服务的数据库绑定(<name> db ...)----
|
||||
|
||||
# svc_db_bind [pg名] [--db X] [--clear] [--no-restart]:查看 / 绑定切换 / 解除
|
||||
@@ -2183,13 +2322,21 @@ registered_names() {
|
||||
}
|
||||
|
||||
# db_ref_of NAME:agent 服务的数据库绑定标识(list/status/info 共用)。
|
||||
# dbref= 优先;否则从 exec= 的 --dsn 提取 host:port 匹配已登记 postgres 实例
|
||||
# (兼容直接 --dsn 部署、尚未登记 dbref 的服务);都匹配不上则回 外部@host:port。
|
||||
# dbref= 优先;否则从 exec= 的 --dsn 或配置文件(conffile=)的 dsn 键提取 host:port
|
||||
# 匹配已登记 postgres 实例(兼容直接 --dsn/--config 部署、尚未登记 dbref 的服务);
|
||||
# 都匹配不上则回 外部@host:port。
|
||||
db_ref_of() {
|
||||
local name="$1" ref dsn hostport pg addr
|
||||
local name="$1" ref dsn hostport pg addr cf
|
||||
ref="$(env_kv_of "$name" dbref)"
|
||||
if [ -n "$ref" ]; then printf '%s' "$ref"; return 0; fi
|
||||
dsn="$(printf '%s' "$(env_kv_of "$name" exec)" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')"
|
||||
if [ -z "$dsn" ]; then
|
||||
cf="$(env_kv_of "$name" conffile)"
|
||||
[ -z "$cf" ] && cf="$(printf '%s' "$(env_kv_of "$name" exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')"
|
||||
if [ -n "$cf" ] && [ -f "$cf" ]; then
|
||||
dsn="$(grep -oE '"dsn"[[:space:]]*:[[:space:]]*"[^"]*"' "$cf" 2>/dev/null | head -1 | sed -E 's/.*"([^"]*)"$/\1/')"
|
||||
fi
|
||||
fi
|
||||
[ -n "$dsn" ] || return 0
|
||||
hostport="$(printf '%s' "$dsn" | sed -n 's#.*@\([^/?]*\).*#\1#p')"
|
||||
while IFS= read -r pg; do
|
||||
@@ -2250,8 +2397,12 @@ env_status_detail() {
|
||||
[ -n "$tags" ] && printf ' 标签 %s\n' "$tags"
|
||||
case "$kind" in
|
||||
agent)
|
||||
local dbref; dbref="$(db_ref_of "$ENV_NAME")"
|
||||
printf ' 数据库 %s\n' "${dbref:-无(内嵌 SQLite 档;绑定:<name> db <pg名>)}" ;;
|
||||
local dbref cfile
|
||||
dbref="$(db_ref_of "$ENV_NAME")"
|
||||
printf ' 数据库 %s\n' "${dbref:-无(内嵌 SQLite 档;绑定:<name> db <pg名>)}"
|
||||
cfile="$(svc_get conffile)"
|
||||
[ -z "$cfile" ] && cfile="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')"
|
||||
printf ' 配置文件 %s\n' "${cfile:-无(命令行参数形态;<name> config <file> 绑定)}" ;;
|
||||
postgres)
|
||||
printf ' 库 %s 用户 %s\n' "$(svc_get dbname)" "$(svc_get user)"
|
||||
printf ' DSN postgresql://%s:***@%s:%s/%s(db dsn %s --reveal 看密码)\n' \
|
||||
@@ -2419,11 +2570,27 @@ info_pg_block() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# info_config_block NAME:单服务 svc.env 内容快照(文件路径+权限+逐行内容)。
|
||||
# info_config_block NAME:单服务 svc.env 内容快照(文件路径+权限+逐行内容),
|
||||
# agent 服务另附其配置文件(conffile)的掩码内容快照。
|
||||
info_config_block() {
|
||||
local name="$1" f="$RUNTIME/$1/svc.env"
|
||||
printf ' [%s] %s(权限 %s,kind=%s)\n' "$name" "$f" "$(info_file_mode "$f")" "$(svc_kind "$name")"
|
||||
info_snapshot_lines "$f"
|
||||
local cf
|
||||
cf="$(env_kv_of "$name" conffile)"
|
||||
[ -z "$cf" ] && cf="$(printf '%s' "$(env_kv_of "$name" exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')"
|
||||
if [ -n "$cf" ]; then
|
||||
if [ -f "$cf" ]; then
|
||||
printf ' ── 配置文件 %s(权限 %s)──\n' "$cf" "$(info_file_mode "$cf")"
|
||||
if [ "$CLI_REVEAL" = 1 ]; then
|
||||
cat "$cf" | sed 's/^/ /'
|
||||
else
|
||||
mask_json "$(cat "$cf")" | sed 's/^/ /'
|
||||
fi
|
||||
else
|
||||
printf ' ── 配置文件 %s:文件不存在(重启将失败,恢复或 %s config --clear)──\n' "$cf" "$name"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -2769,15 +2936,18 @@ main() {
|
||||
# 动作词可省略(省略 = deploy);flags 可直接跟随服务名。
|
||||
local action="deploy"
|
||||
case "${1:-}" in
|
||||
deploy|stop|start|restart|status|logs|db) action="$1"; shift ;;
|
||||
deploy|stop|start|restart|status|logs|db|config) action="$1"; shift ;;
|
||||
esac
|
||||
if [ "$action" = logs ] && [ "${1:-}" != "" ] && [[ "$1" =~ ^[0-9]+$ ]]; then
|
||||
CLI_LOGS_N="$1"; shift
|
||||
fi
|
||||
# db 绑定子命令:自带参数解析(位置参数 <pg名> 不走 parse_args;未知 flags 由其自行拒绝)。
|
||||
if [ "$action" = db ]; then
|
||||
# db / config 子命令:自带参数解析(位置参数不走 parse_args;未知 flags 由其自行拒绝)。
|
||||
if [ "$action" = db ] || [ "$action" = config ]; then
|
||||
env_config "$ENV_NAME"
|
||||
svc_db_bind "$@" || exit $?
|
||||
case "$action" in
|
||||
db) svc_db_bind "$@" || exit $? ;;
|
||||
config) svc_config "$@" || exit $? ;;
|
||||
esac
|
||||
exit 0
|
||||
fi
|
||||
parse_args "$@"
|
||||
|
||||
Reference in New Issue
Block a user