Recover svc.sh database config across restarts and refuse on invalid binding
Why: the exec= reuse path stripped --dsn into the process environment (G1-7) and record_state then persisted a DSN-less exec=, while the dbref fallback only existed in the explicit-rebuild branch - the second bare restart after a rebind silently dropped the database (SQLite fallback on loopback). Auth preflight also matched --dsn on the command line only, so env-delivered DSN with auth=auto was misjudged as unauthenticated. Evidence: bash -n OK; make test-governance 115 tests OK (new tests/test_svc.py 11 cases; 10/11 fail against the pre-fix script); make test green; make build OK; isolated real PostgreSQL 18.6 + real binary validation 27/27 (consecutive restarts/deploy keep binding, psql-verified database identity, DB-down restart fails without SQLite fallback or config loss, no credential leakage) - all recorded via make evidence in the task card. Impact: svc.env gains db=/dbref=/dsn= recovery keys (atomic 0600 writes); start/restart/deploy/update/db share one resolver; invalid bindings, config conflicts, mock coexistence and env-DSN-incapable binaries now refuse to start instead of silently downgrading; legacy exec= DSN migrates idempotently; docs/svc.md and script help updated. Refs: task card .task/feature-svc-db-config-recover.md; user-approved plan (2026-10-11 session message); AGENTS.md evidence gates.
This commit is contained in:
+433
-110
@@ -53,10 +53,15 @@
|
||||
# db copy:--db X --force set:--port N --host H --apply --tag T
|
||||
#
|
||||
# ════════════════════ 配置优先级 ════════════════════
|
||||
# 命令行 > --config/--env 文件 > svc.env 持久化(exec=/addr=)> SVC_<NAME>_* 环境变量
|
||||
# (.svc.env 自动加载,同层)> 默认值。裸 start/restart 复用上次启动配置;显式参数
|
||||
# 出现即覆盖。任意环境 X 的变量名:SVC_<X 大写,- → _>_ADDR/DATA/DSN/AUTH/MOCK,
|
||||
# 以及 INSECURE_LISTEN / ALLOWED_HOSTS(传给进程的 AGENT_SYSTEM_*,旧二进制会忽略)。
|
||||
# 命令行 > --config/--env 文件 > svc.env 持久化(exec=/addr=/dbref=/dsn=/db=)
|
||||
# > SVC_<NAME>_* 环境变量(.svc.env 自动加载,同层)> 默认值。裸 start/restart 复用
|
||||
# 上次启动配置;显式参数出现即覆盖。任意环境 X 的变量名:SVC_<X 大写,- → _>_
|
||||
# ADDR/DATA/DSN/AUTH/MOCK,以及 INSECURE_LISTEN / ALLOWED_HOSTS(传给进程的
|
||||
# AGENT_SYSTEM_*,旧二进制会忽略)。
|
||||
# 数据库配置统一解析:托管绑定(dbref=)从实例登记重建 DSN,外部直连存受保护
|
||||
# dsn= 字段;连接串不进 exec=/命令行,经 AGENT_SYSTEM_DSN 环境变量交付。绑定失效
|
||||
# 或配置冲突(直接 DSN 与 dbref 不同库、mock 与绑定并存、db=pg 无可恢复 DSN)时
|
||||
# 拒绝启动,绝不静默回退内嵌 SQLite;显式解绑(db=sqlite)后环境残留不再重新选库。
|
||||
#
|
||||
# ════════════════════ 服务模型与登记 ════════════════════
|
||||
# - 服务名 ^[a-z][a-z0-9-]{0,15}$(保留字不可用);dev=8788 / prod=8787 默认端口,
|
||||
@@ -202,7 +207,11 @@ svc.env 字段:
|
||||
version 产物版本串(agent-system version 输出)
|
||||
started 最近启动时间
|
||||
pidcheck 进程判定标记(agent=二进制路径、postgres=pgdata 路径、static=--directory)
|
||||
exec agent 上次完整启动参数(裸 start/restart 复用;显式参数出现即重建)
|
||||
exec agent 上次完整启动参数(裸 start/restart 复用;显式参数出现即重建)。
|
||||
只存非敏感参数——DSN 不在其中,数据库配置走 db=/dbref=/dsn= 专键
|
||||
db agent 数据库模式:pg | sqlite(显式解绑)| mock;从未配置则无此键
|
||||
dbref agent 托管数据库绑定 <pg名>/<库名>(启动时由实例登记重建 DSN)
|
||||
dsn agent 外部直连 DSN(受保护,600;未命中本机实例时使用)
|
||||
user/password/dbname/authmode/pgdata postgres 专属(密码 600 保存)
|
||||
htdocs static 专属
|
||||
|
||||
@@ -248,7 +257,9 @@ EOF
|
||||
行为细节:
|
||||
- deploy 前自动停止旧实例(只停自己 pidfile+cmdline 校验的进程)
|
||||
- agent 启动后等健康检查(20s),随后按 --model-*/--tavily-keys 播种设置(幂等)
|
||||
- 旧版本产物不认识 --dsn/--auth 时自动省略(探测 serve 用法)并提示
|
||||
- 旧版本产物不认识 --auth/--config 时自动省略(探测 serve 用法)并提示;
|
||||
不消费环境变量 DSN(AGENT_SYSTEM_DSN)的旧产物在需要交付数据库配置时
|
||||
拒绝部署/启动(不静默按内嵌 SQLite 运行)
|
||||
- static 的 release 源不支持(制品内嵌前端);--dir PATH 可直接登记并服务
|
||||
既有目录(不构建、不复制,source=dir@<路径>)
|
||||
- deploy 全程事件入 history.jsonl(begin/ok/fail)
|
||||
@@ -268,9 +279,10 @@ EOF
|
||||
scripts/svc.sh <name> stop 停止(agent/static: TERM→KILL;postgres: pg_ctl -m fast)
|
||||
scripts/svc.sh <name> restart 停止 + 启动(裸 restart 复用上次配置)
|
||||
scripts/svc.sh <name> db [pg名|--clear] [--db X] agent 数据库绑定:缺省查看当前
|
||||
绑定与可用实例;给 pg名=绑定/切换(DSN 写入 exec=,dbref= 登记,
|
||||
实例运行中缺库自动建,服务运行中自动重启生效);--clear 解绑回内嵌
|
||||
SQLite 档;--no-restart 只改登记
|
||||
绑定与可用实例;给 pg名=绑定/切换(dbref= 登记,启动时由实例登记
|
||||
重建 DSN 并经环境变量交付,实例运行中缺库自动建,服务运行中自动
|
||||
重启生效);--clear 显式解绑(db=sqlite,清除直接 DSN,环境残留
|
||||
SVC_*_DSN 不再重新选库);--no-restart 只改登记
|
||||
scripts/svc.sh <name> config [file|--clear] [--no-restart] agent 配置文件:缺省查看当前
|
||||
文件与生效值;给 file=更换(agent 二进制 --config,JSON schema 校验
|
||||
通过才切换,运行中自动重启即时生效);--clear 移除回命令行参数形态。
|
||||
@@ -335,9 +347,9 @@ EOF
|
||||
scripts/svc.sh prod db pg1 --db prod # 绑定 pg1/prod 库(缺库自动建)
|
||||
scripts/svc.sh prod db pg2 --no-restart # 切换实例但暂不重启(下次 start 生效)
|
||||
scripts/svc.sh prod db # 查看当前绑定 + 可用实例
|
||||
scripts/svc.sh prod db --clear # 解绑回内嵌 SQLite 档
|
||||
绑定写入 svc.env(exec= 的 --dsn + dbref=,600);info / list 的 DB 列与数据库节
|
||||
"绑定服务"行展示服务↔实例对应关系。
|
||||
scripts/svc.sh prod db --clear # 显式解绑(db=sqlite,回内嵌档)
|
||||
绑定登记于 svc.env(dbref= + db=pg,600;DSN 启动时由实例登记重建,经环境变量
|
||||
交付);info / list 的 DB 列与数据库节 "绑定服务" 行展示服务↔实例对应关系。
|
||||
|
||||
密码管理:密码明文存 runtime/<name>/svc.env(600);--password 未给时自动生成
|
||||
(head -c 18 /dev/urandom)。auth-mode 默认 scram-sha-256(pg_hba 全线)。
|
||||
@@ -383,7 +395,7 @@ EOF
|
||||
优先级(高 → 低):
|
||||
1. 命令行参数(--port/--dsn/--tag/...)
|
||||
2. --config FILE(或 --env FILE,兼容别名;=FILE 写法亦可)
|
||||
3. svc.env 持久化(addr=、exec=——上次启动的实际参数)
|
||||
3. svc.env 持久化(addr=、exec=、dbref=/dsn=/db=——数据库配置专键)
|
||||
4. SVC_<NAME>_* 环境变量与 .svc.env(自动加载,同层)
|
||||
5. 默认值(dev=8788 / prod=8787 / 其余派生端口)
|
||||
|
||||
@@ -401,11 +413,18 @@ dotenv 格式:KEY=VALUE,支持 # 注释、成对引号、export 前缀;只
|
||||
播种幂等:设置持久化在 agent 数据目录,播过一次后续启动无需再传。
|
||||
|
||||
持久化语义:
|
||||
- deploy/start 成功(健康探测通过)后把当前态写入 svc.env(600);启动失败不覆盖
|
||||
上次配置——裸 start 仍按上次可用参数重试
|
||||
- deploy/start 成功(健康探测通过)后把当前态写入 svc.env(600,临时文件原子替换);
|
||||
启动失败不覆盖上次配置——裸 start 仍按上次可用参数重试
|
||||
- 裸 start/restart 复用 exec=(上次完整参数);任何显式参数(或 --config 文件、
|
||||
SVC_* 变量)出现即整体重建参数——重建时 dbref 绑定的 --dsn 自动从实例登记
|
||||
重建保留(mock 档或显式 --dsn 除外)
|
||||
SVC_* 变量)出现即整体重建参数;两种路径共用同一数据库配置解析——改端口/地址/
|
||||
版本不改变数据库选择,conffile 引用随重建保留
|
||||
- 数据库配置可恢复:托管绑定 dbref= 在启动时由实例登记重建 DSN(实例端口/密码
|
||||
变更自动跟随);外部直连存受保护 dsn= 字段;显式解绑记 db=sqlite(环境残留
|
||||
SVC_*_DSN 不再重新选库)。DSN 经 AGENT_SYSTEM_DSN 环境变量交付,不进 exec=
|
||||
与命令行;存量 exec= 内的 --dsn 启动时自动迁移(幂等)
|
||||
- 绑定失效或配置冲突时拒绝启动(不静默回退 SQLite):dbref 实例缺失/登记不全、
|
||||
直接 DSN 与 dbref 指向不同库、mock 与绑定并存、db=pg 无可恢复 DSN、产物不
|
||||
消费环境 DSN;restart/deploy 在停止旧进程前先做这些静态检查
|
||||
- tags 随每次登记合并去重,跨重启保留
|
||||
|
||||
服务级配置文件(与上述 svc.sh dotenv 层不同——这是 agent 二进制自己的 JSON 配置):
|
||||
@@ -661,21 +680,43 @@ svc_merge_tags() {
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
# record_state EXTRA_KEY=VALUE...:把当前态+持久化配置写入 svc.env(600)。
|
||||
# 基本键:kind/env/pid/addr/tags/source/version/started/pidcheck/exec + dbref/
|
||||
# conffile(跨重启保留的数据库绑定与配置文件);kind 特有键(user/password/dbname/
|
||||
# authmode/pgdata/htdocs...)经参数追加。
|
||||
# record_state EXTRA_KEY=VALUE...:把当前态+持久化配置写入 svc.env(600,临时文件
|
||||
# 原子替换,避免截断/半写)。基本键:kind/env/pid/addr/tags/source/version/started/
|
||||
# pidcheck/exec + db/dbref/dsn/conffile(跨重启保留的数据库配置与配置文件引用);
|
||||
# kind 特有键(user/password/dbname/authmode/pgdata/htdocs...)经参数追加。
|
||||
# agent 服务经统一解析启动后按生效态写 db=(pg/mock;显式 sqlite)与 dbref=/dsn=
|
||||
# (托管绑定不重复存 DSN,外部直连存受保护 dsn=);未走解析的调用保持旧值。
|
||||
record_state() {
|
||||
# 先读完旧文件再开重定向(块内 {} > svc.env 会先截断文件,读旧值必须在其之前)。
|
||||
local pid prev_src prev_dbref prev_conffile merged_tags
|
||||
local pid prev_src prev_dbref prev_conffile prev_db prev_dsn 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)"
|
||||
prev_db="$(svc_get db)"
|
||||
prev_dsn="$(svc_get dsn)"
|
||||
SVC_SOURCE="${SRC_LABEL:-${prev_src:-}}"
|
||||
merged_tags="$(svc_merge_tags)"
|
||||
local db_out="" dbref_out="" dsn_out=""
|
||||
if [ "${SVC_KIND:-agent}" = agent ]; then
|
||||
if [ -n "${SVC_DB_MODE:-}" ]; then
|
||||
case "$SVC_DB_MODE" in
|
||||
pg)
|
||||
# 只在有可恢复来源(dbref=/dsn=/conffile)时记 pg 标记——纯环境层来源
|
||||
# 不落标记,环境撤除后回到原语义,不产生"db=pg 却无可恢复 DSN"的死锁。
|
||||
[ -n "$SVC_DB_REF" ] || [ -n "$SVC_DB_DSN_SAVE" ] || [ "$SVC_DB_SOURCE" = conffile ] && db_out=pg
|
||||
;;
|
||||
mock) db_out=mock ;;
|
||||
sqlite) [ "$SVC_DB_SOURCE" != none ] && db_out=sqlite ;; # 显式解绑才记;从未配置不写
|
||||
esac
|
||||
dbref_out="${SVC_DB_REF:-}"
|
||||
dsn_out="${SVC_DB_DSN_SAVE:-}"
|
||||
else
|
||||
db_out="$prev_db"; dbref_out="$prev_dbref"; dsn_out="$prev_dsn"
|
||||
fi
|
||||
fi
|
||||
mkdir -p "$ENV_DIR"
|
||||
local tmp="$ENV_DIR/svc.env.tmp.$$"
|
||||
{
|
||||
printf 'kind=%s\n' "${SVC_KIND:-agent}"
|
||||
printf 'env=%s\n' "$ENV_NAME"
|
||||
@@ -687,11 +728,14 @@ record_state() {
|
||||
printf 'started=%s\n' "$(date -Is)"
|
||||
printf 'pidcheck=%s\n' "${SVC_PIDCHECK:-}"
|
||||
printf 'exec=%s\n' "${SVC_EXEC:-}"
|
||||
[ -n "$prev_dbref" ] && printf 'dbref=%s\n' "$prev_dbref"
|
||||
[ -n "$db_out" ] && printf 'db=%s\n' "$db_out"
|
||||
[ -n "$dbref_out" ] && printf 'dbref=%s\n' "$dbref_out"
|
||||
[ -n "$dsn_out" ] && printf 'dsn=%s\n' "$dsn_out"
|
||||
[ -n "$prev_conffile" ] && printf 'conffile=%s\n' "$prev_conffile"
|
||||
[ $# -gt 0 ] && printf '%s\n' "$@"
|
||||
} > "$ENV_DIR/svc.env"
|
||||
chmod 600 "$ENV_DIR/svc.env"
|
||||
} > "$tmp"
|
||||
chmod 600 "$tmp"
|
||||
mv -f "$tmp" "$ENV_DIR/svc.env"
|
||||
}
|
||||
|
||||
# ---- pid 管理(泛化:pidfile + cmdline 标记双校验)----
|
||||
@@ -756,6 +800,8 @@ env_config() {
|
||||
ENV_LOG="$ENV_DIR/svc.log"
|
||||
ENV_BIN_DIR="$ENV_DIR/bin"
|
||||
ENV_BIN="$ENV_BIN_DIR/agent-system"
|
||||
# 数据库配置解析态按实例重置——批量 update/restart 逐服务解析时不串用上一服务的 DB_*。
|
||||
SVC_DB_MODE=""; SVC_DB_SOURCE=""; SVC_DB_REF=""; SVC_DB_DSN_SAVE=""
|
||||
local registered=""
|
||||
[ -f "$ENV_DIR/svc.env" ] && registered="$(svc_kind "$name")"
|
||||
SVC_KIND="${registered:-agent}"
|
||||
@@ -810,7 +856,7 @@ agent_has_explicit_cfg() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# adjust_args_for_bin BIN ARGS:按二进制 serve 用法过滤旧产物不认识的 --dsn/--auth。
|
||||
# adjust_args_for_bin BIN ARGS:按二进制 serve 用法过滤旧产物不认识的 --auth/--config。
|
||||
# 匹配按"去横线的旗标名 + 1-2 个横线"(Go flag 帮助打印单横线 -dsn,2026-10-04 实测
|
||||
# 旧实现 grep -- '--dsn' 永不命中,把现役二进制的 --dsn/--auth 误当不支持而丢弃)。
|
||||
adjust_args_for_bin() {
|
||||
@@ -819,7 +865,7 @@ adjust_args_for_bin() {
|
||||
local filtered="" w drop_next=0 flag
|
||||
for w in $args; do
|
||||
case "$w" in
|
||||
--dsn|--auth)
|
||||
--auth|--config)
|
||||
flag="${w#--}"
|
||||
if printf '%s\n' "$out" | grep -Eq -- "(^|[[:space:]])-{1,2}${flag}([[:space:]]|=|$)"; then
|
||||
filtered="$filtered $w"
|
||||
@@ -1087,6 +1133,12 @@ materialize_release() {
|
||||
agent_do_deploy() {
|
||||
resolve_source
|
||||
if [ "$CLI_DRYRUN" = 1 ]; then print_agent_plan; return 0; fi
|
||||
# 停旧之前的静态配置验证:数据库配置可解析(绑定有效/无冲突)、conffile 存在、
|
||||
# 现产物承接能力——配置性问题不制造停机窗口(登记保持可恢复,失败直接返回)。
|
||||
if ! agent_static_preflight; then
|
||||
hist_event deploy fail "$(read_pid)" "预检拒绝:数据库配置/配置文件无效(登记未变更)"
|
||||
return 1
|
||||
fi
|
||||
hist_event deploy begin "$(read_pid)" "from=$SRC_LABEL"
|
||||
do_stop_quiet
|
||||
local wt
|
||||
@@ -1108,6 +1160,13 @@ agent_do_deploy() {
|
||||
materialize_release || { hist_event deploy fail "$(read_pid)" "物化/校验失败($SRC_LABEL)"; return 1; }
|
||||
;;
|
||||
esac
|
||||
# 新产物承接能力复核(环境 DSN 交付):不支持即失败——绑定保持可恢复,不降级启动。
|
||||
if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ] && ! bin_env_dsn_supported "$ENV_BIN"; then
|
||||
c_err "[$ENV_NAME] 新产物不消费环境变量 DSN(AGENT_SYSTEM_DSN)——部署中止;旧绑定保留在 svc.env"
|
||||
c_err " 恢复:换部署版本,或改用携带 dsn 的配置文件(scripts/svc.sh $ENV_NAME config <file.json>)"
|
||||
hist_event deploy fail "$(read_pid)" "新产物不支持环境 DSN($SRC_LABEL)"
|
||||
return 1
|
||||
fi
|
||||
agent_do_start || { hist_event deploy fail "$(read_pid)" "启动失败($SRC_LABEL)"; return 1; }
|
||||
hist_event deploy ok "$(read_pid)" "from=$SRC_LABEL"
|
||||
return 0
|
||||
@@ -1198,15 +1257,253 @@ addr_is_loopback() {
|
||||
esac
|
||||
}
|
||||
|
||||
# agent_args_authed ARGS:按二进制 AuthEnabled 口径判断启动参数是否开启认证
|
||||
# (--auth on;或未显式 off 且带 --dsn——auto+PG 即开)。--mock 只影响存储档不影响认证。
|
||||
agent_args_authed() {
|
||||
case "$1" in
|
||||
*--auth[=\ ]on*) return 0 ;;
|
||||
*--auth[=\ ]off*) return 1 ;;
|
||||
*--dsn\ *) return 0 ;;
|
||||
*) return 1 ;;
|
||||
# dsn_target DSN → host:port/db(配置一致性比较用;解析失败回空串)。
|
||||
dsn_target() {
|
||||
local t="${1%%\?*}"
|
||||
case "$t" in *://*) t="${t#*://}" ;; esac
|
||||
case "$t" in *@*) t="${t#*@}" ;; esac
|
||||
local hostport="${t%%/*}" db="${t#*/}"
|
||||
[ "$db" = "$t" ] && db=""
|
||||
printf '%s/%s' "$hostport" "$db"
|
||||
}
|
||||
|
||||
# dsn_ref_match DSN:DSN 的 host:port/db 命中已登记 postgres 实例时回显 <inst>/<db>(无匹配回空)。
|
||||
dsn_ref_match() {
|
||||
local tgt pg paddr
|
||||
tgt="$(dsn_target "$1")"
|
||||
[ -n "$tgt" ] || return 0
|
||||
while IFS= read -r pg; do
|
||||
[ "$(svc_kind "$pg")" = postgres ] || continue
|
||||
paddr="$(env_kv_of "$pg" addr)"
|
||||
[ -n "$paddr" ] || continue
|
||||
if [ "$tgt" = "$paddr/${tgt#*/}" ]; then
|
||||
printf '%s/%s' "$pg" "${tgt#*/}"
|
||||
return 0
|
||||
fi
|
||||
done < <(registered_names)
|
||||
return 0
|
||||
}
|
||||
|
||||
# auth_pick CLI ENV PERSISTED:生效认证方式(缺省 auto)。
|
||||
auth_pick() {
|
||||
case "${1:-}" in ?*) printf '%s' "$1"; return ;; esac
|
||||
case "${2:-}" in ?*) printf '%s' "$2"; return ;; esac
|
||||
case "${3:-}" in ?*) printf '%s' "$3"; return ;; esac
|
||||
printf auto
|
||||
}
|
||||
|
||||
# migrate_legacy_db_config:存量 exec= 的 --dsn 迁移为可恢复配置(dbref=/dsn=)并从 exec
|
||||
# 剥离——此后 exec= 只存非敏感启动参数,DSN 经环境变量传递(G1-7)。幂等:再次运行时
|
||||
# exec= 已无 --dsn,直接返回。命中已登记实例 → dbref(密码经实例登记存放,不重复保存);
|
||||
# 否则 → 受保护 dsn= 字段。已有 dbref= 时 exec 的 DSN 只作历史残留处理,直接剥离。
|
||||
migrate_legacy_db_config() {
|
||||
local exec_old dsn new_exec ref
|
||||
[ -f "$ENV_DIR/svc.env" ] || return 0
|
||||
exec_old="$(svc_get exec)"
|
||||
case "$exec_old" in *--dsn\ *) ;; *) return 0 ;; esac
|
||||
dsn="$(printf '%s' "$exec_old" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')"
|
||||
new_exec="$(printf '%s' "$exec_old" | sed -E 's/(^| )--dsn [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
||||
svc_set_key exec "$new_exec"
|
||||
if [ -n "$(svc_get dbref)" ]; then
|
||||
c_info "[$ENV_NAME] 存量 exec DSN 与 dbref=$(svc_get dbref) 并存,已剥离 exec 残留(绑定不受影响)"
|
||||
return 0
|
||||
fi
|
||||
ref="$(dsn_ref_match "$dsn")"
|
||||
if [ -n "$ref" ]; then
|
||||
svc_set_key dbref "$ref"
|
||||
svc_set_key db pg
|
||||
c_info "[$ENV_NAME] 存量 exec DSN 已迁移为托管绑定 dbref=$ref(DSN 改由实例登记重建)"
|
||||
else
|
||||
svc_set_key dsn "$dsn"
|
||||
svc_set_key db pg
|
||||
c_info "[$ENV_NAME] 存量 exec DSN 已迁移至受保护 dsn= 字段(未命中本机实例登记)"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- 数据库配置统一解析(start/restart/deploy/update/db 共用单点)----
|
||||
# 核心约束:修改端口、监听地址或部署版本不改变数据库选择;绑定失效或配置冲突时
|
||||
# 拒绝启动(返回 1),绝不静默回退内嵌 SQLite。
|
||||
# 结果全局变量(入口全部重置——批量 update 逐服务重解析,不串用):
|
||||
# DB_MODE pg | sqlite | mock
|
||||
# DB_SOURCE cli | env | dbref | dsn | conffile | db | mock | none
|
||||
# DB_REF <inst>/<db>(命中/保持托管绑定时;否则空)
|
||||
# DB_DSN 有效连接串(sqlite/mock 为空;conffile 来源=文件内值,仅用于判定不外传)
|
||||
# DB_AUTH on | off | auto(生效值,对齐二进制 AuthEnabled 口径)
|
||||
# DB_DSN_SAVE 需持久化到 dsn= 的外部直连 DSN(仅 cli/dsn 来源且未命中实例)
|
||||
resolve_db_config() {
|
||||
DB_MODE="sqlite"; DB_SOURCE="none"; DB_REF=""; DB_DSN=""; DB_AUTH="auto"; DB_DSN_SAVE=""
|
||||
|
||||
migrate_legacy_db_config
|
||||
|
||||
local env_auth env_mock env_dsn
|
||||
env_auth="$(env_var "$ENV_NAME" AUTH)"
|
||||
env_mock="$(env_var "$ENV_NAME" MOCK)"
|
||||
env_dsn="$(env_var "$ENV_NAME" DSN)"
|
||||
|
||||
local p_dbref p_dsn p_db p_conf p_exec p_auth
|
||||
p_dbref="$(svc_get dbref)"
|
||||
p_dsn="$(svc_get dsn)"
|
||||
p_db="$(svc_get db)"
|
||||
p_conf="$(svc_get conffile)"
|
||||
p_exec="$(svc_get exec)"
|
||||
p_auth="$(printf '%s' "$p_exec" | sed -n 's/.*--auth \([^ ]*\).*/\1/p')"
|
||||
[ -n "$p_auth" ] || p_auth="$(svc_get auth)"
|
||||
|
||||
DB_AUTH="$(auth_pick "$CLI_AUTH" "$env_auth" "$p_auth")"
|
||||
case "$DB_AUTH" in on|off|auto) ;; *)
|
||||
c_err "[$ENV_NAME] 拒绝启动:无效 auth 值「$DB_AUTH」(可用 on/off/auto)"
|
||||
return 1 ;;
|
||||
esac
|
||||
|
||||
local mock=0
|
||||
{ [ "$CLI_MOCK" = 1 ] || [ "$env_mock" = "1" ]; } && mock=1
|
||||
[ "$mock" = 0 ] && case "$p_exec" in *--mock*) mock=1 ;; esac
|
||||
|
||||
# conffile 的 dsn 键(二进制自行读文件,连接串不经环境重复传递)。
|
||||
local cf_dsn=""
|
||||
if [ -n "$p_conf" ] && [ -f "$p_conf" ]; then
|
||||
cf_dsn="$(grep -oE '"dsn"[[:space:]]*:[[:space:]]*"[^"]*"' "$p_conf" 2>/dev/null | head -1 | sed -E 's/.*"([^"]*)"$/\1/')"
|
||||
fi
|
||||
|
||||
# mock 档与数据库配置共存 = 冲突,拒绝(行为要明确,不隐式降级)。
|
||||
if [ "$mock" = 1 ]; then
|
||||
if [ -n "$p_dbref" ] || [ -n "$p_dsn" ] || [ -n "$CLI_DSN" ] || { [ -n "$env_dsn" ] && [ "$p_db" != "sqlite" ]; }; then
|
||||
c_err "[$ENV_NAME] 拒绝启动:mock 档与数据库配置冲突(dbref=${p_dbref:-无}、dsn=${p_dsn:+已设、}CLI/环境 DSN=${CLI_DSN:+已设}${env_dsn:+已设})"
|
||||
c_err " 先解绑(scripts/svc.sh $ENV_NAME db --clear)再用 mock,或去掉 --mock / SVC_$(printf '%s' "$ENV_NAME" | tr 'a-z-' 'A-Z_')_MOCK"
|
||||
return 1
|
||||
fi
|
||||
DB_MODE=mock; DB_SOURCE=mock
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 显式解绑(db=sqlite):残留 SVC_*_DSN 不再悄悄重新选库;CLI 显式 --dsn 仍可覆盖。
|
||||
if [ -n "$env_dsn" ] && [ "$p_db" = "sqlite" ] && [ -z "$CLI_DSN" ]; then
|
||||
c_info "[$ENV_NAME] 已显式解绑(db=sqlite),忽略环境层 SVC_*_DSN 残留(显式 --dsn 或重新绑定可覆盖)"
|
||||
env_dsn=""
|
||||
fi
|
||||
|
||||
# 有效 DSN 选择:CLI > 环境 > 持久化(dbref / dsn) > conffile > 无(SQLite)。
|
||||
local eff_dsn="" eff_src="" bt
|
||||
if [ -n "$CLI_DSN" ]; then
|
||||
eff_dsn="$CLI_DSN"; eff_src=cli
|
||||
elif [ -n "$env_dsn" ]; then
|
||||
eff_dsn="$env_dsn"; eff_src=env
|
||||
elif [ -n "$p_dbref" ] && [ -n "$p_dsn" ]; then
|
||||
bt="$(dsn_from_dbref "$p_dbref" 2>/dev/null || true)"
|
||||
if [ -n "$bt" ] && [ "$(dsn_target "$bt")" = "$(dsn_target "$p_dsn")" ]; then
|
||||
eff_dsn="$p_dsn"; eff_src=dsn
|
||||
else
|
||||
c_err "[$ENV_NAME] 拒绝启动:配置冲突——dbref=$p_dbref 与 dsn= 指向不同数据库,不能静默取其一"
|
||||
c_err " 保留其一:重新绑定(scripts/svc.sh $ENV_NAME db <pg实例>)或 db --clear 后重新直连"
|
||||
return 1
|
||||
fi
|
||||
elif [ -n "$p_dbref" ]; then
|
||||
if ! eff_dsn="$(dsn_from_dbref "$p_dbref")"; then
|
||||
c_err "[$ENV_NAME] 拒绝启动:dbref=$p_dbref 无法重建 DSN(实例未登记 / 类型错误 / addr·user·password 登记不全)"
|
||||
c_err " 恢复实例登记或重新绑定(scripts/svc.sh $ENV_NAME db <pg实例>);不回退内嵌 SQLite"
|
||||
return 1
|
||||
fi
|
||||
eff_src=dbref
|
||||
elif [ -n "$p_dsn" ]; then
|
||||
eff_dsn="$p_dsn"; eff_src=dsn
|
||||
elif [ -n "$cf_dsn" ]; then
|
||||
eff_dsn="$cf_dsn"; eff_src=conffile
|
||||
fi
|
||||
|
||||
# 直接 DSN(CLI/环境/持久化)与 dbref 并存:目标库必须一致,否则配置冲突。
|
||||
if [ -n "$eff_dsn" ] && [ -n "$p_dbref" ] && [ "$eff_src" != dbref ]; then
|
||||
if ! bt="$(dsn_from_dbref "$p_dbref")"; then
|
||||
c_err "[$ENV_NAME] 拒绝启动:dbref=$p_dbref 无效(无法重建 DSN)且存在直接 DSN——先修复或解除绑定"
|
||||
return 1
|
||||
fi
|
||||
if [ "$(dsn_target "$bt")" != "$(dsn_target "$eff_dsn")" ]; then
|
||||
c_err "[$ENV_NAME] 拒绝启动:直接 DSN($(mask_dsn "$eff_dsn"))与 dbref=$p_dbref 指向不同数据库,不能静默取其一"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# conffile 一致性:文件内 dsn 与有效来源并存时必须同库。
|
||||
if [ -n "$cf_dsn" ] && [ "$eff_src" != conffile ] && [ -n "$eff_dsn" ] \
|
||||
&& [ "$(dsn_target "$cf_dsn")" != "$(dsn_target "$eff_dsn")" ]; then
|
||||
c_err "[$ENV_NAME] 拒绝启动:配置文件 dsn($(mask_dsn "$cf_dsn"))与数据库绑定指向不同库——先对齐(改文件或换绑定)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 已选 PostgreSQL 但拿不到任何有效 DSN → 拒绝(不降级 SQLite)。
|
||||
if [ -z "$eff_dsn" ] && [ "$p_db" = "pg" ]; then
|
||||
c_err "[$ENV_NAME] 拒绝启动:已登记 db=pg 但无可恢复 DSN(dbref/dsn/conffile 均缺失)"
|
||||
c_err " 重新绑定(scripts/svc.sh $ENV_NAME db <pg实例>)或 db --clear 显式回内嵌 SQLite"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$eff_dsn" ]; then
|
||||
DB_MODE=pg; DB_DSN="$eff_dsn"; DB_SOURCE="$eff_src"
|
||||
local ref=""
|
||||
case "$eff_src" in
|
||||
cli|dsn)
|
||||
if [ -n "$p_dbref" ]; then
|
||||
DB_REF="$p_dbref" # 一致性已验证:保持既有托管绑定
|
||||
else
|
||||
ref="$(dsn_ref_match "$eff_dsn")"
|
||||
if [ -n "$ref" ]; then DB_REF="$ref" # 直连命中本机实例 → 升级托管绑定(密码不重复保存)
|
||||
else DB_DSN_SAVE="$eff_dsn"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
env)
|
||||
[ -n "$p_dbref" ] && DB_REF="$p_dbref" # 环境层本身不持久化;已有绑定/直连保持
|
||||
[ -n "$p_dsn" ] && DB_DSN_SAVE="$p_dsn" # 环境覆盖不丢已持久化的直连配置
|
||||
;;
|
||||
dbref)
|
||||
DB_REF="$p_dbref"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
DB_MODE=sqlite
|
||||
[ "$p_db" = "sqlite" ] && DB_SOURCE=db # 显式解绑的选择跨重启保留
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# db_auth_enabled:启动预检的认证判定,对齐二进制 AuthEnabled:显式 on → 开;
|
||||
# 显式 off → 关;auto → 存在有效 DSN(含环境与配置文件来源)即开。
|
||||
db_auth_enabled() {
|
||||
case "$DB_AUTH" in
|
||||
on) return 0 ;;
|
||||
off) return 1 ;;
|
||||
*) [ -n "$DB_DSN" ] ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# bin_env_dsn_supported BIN:二进制是否消费环境层 AGENT_SYSTEM_DSN(经 config explain
|
||||
# 探测:explain 对 DSN 值脱敏但用户名保留,探测标记用户即可)。旧产物不支持环境层时
|
||||
# 返回 1——调用方拒绝以环境传递 DSN(防静默按内嵌 SQLite 运行)。
|
||||
bin_env_dsn_supported() {
|
||||
local out
|
||||
out="$(AGENT_SYSTEM_DSN='postgresql://svcprobe:svcprobe@127.0.0.1:9/svcprobe' "$1" config explain 2>/dev/null || true)"
|
||||
case "$out" in *svcprobe*) return 0 ;; esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# agent_static_preflight:停止旧进程 / 更换产物之前可静态检查的配置验证——数据库配置
|
||||
# 可解析(含绑定有效性与冲突)、conffile 存在、现产物能承接环境 DSN。失败返回 1(输出
|
||||
# 脱敏原因;除存量迁移外不写登记)。
|
||||
agent_static_preflight() {
|
||||
resolve_db_config || return 1
|
||||
local cf
|
||||
cf="$(svc_get conffile)"
|
||||
if [ -n "$cf" ] && [ ! -f "$cf" ]; then
|
||||
c_err "[$ENV_NAME] 拒绝启动:配置文件不存在:$cf(恢复文件或 config --clear)"
|
||||
return 1
|
||||
fi
|
||||
if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ] && [ -x "$ENV_BIN" ] \
|
||||
&& ! bin_env_dsn_supported "$ENV_BIN"; then
|
||||
c_err "[$ENV_NAME] 拒绝启动:当前产物不消费环境变量 DSN(AGENT_SYSTEM_DSN),无法按绑定非降级交付数据库配置"
|
||||
c_err " 更新产物(deploy 新版本),或改用携带 dsn 的配置文件(scripts/svc.sh $ENV_NAME config <file.json>)"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
agent_do_start() {
|
||||
@@ -1218,52 +1515,34 @@ agent_do_start() {
|
||||
return 1
|
||||
fi
|
||||
SVC_VERSION="$("$ENV_BIN" version 2>/dev/null || true)"
|
||||
# 组装启动参数:显式配置 → 重建;否则复用上次持久化 exec=。
|
||||
# 统一解析数据库配置(复用与重建两路共用;失败拒绝启动,绝不静默回退 SQLite)。
|
||||
# 2026-10-11 修复:复用 exec= 分支此前不查 dbref——G1-7 迁移把 --dsn 剥入环境后
|
||||
# record_state 写回不含 DSN 的 exec=,重新绑定后的第二次裸 restart 即丢库。
|
||||
if ! resolve_db_config; then
|
||||
hist_event start fail "" "数据库配置解析拒绝(绑定失效/配置冲突)"
|
||||
return 1
|
||||
fi
|
||||
SVC_DB_MODE="$DB_MODE"; SVC_DB_SOURCE="$DB_SOURCE"; SVC_DB_REF="$DB_REF"; SVC_DB_DSN_SAVE="$DB_DSN_SAVE"
|
||||
# 组装启动参数:显式配置 → 重建;否则复用上次持久化 exec=(DSN 已由解析迁移出 exec)。
|
||||
local exec_args
|
||||
if ! agent_has_explicit_cfg && [ -n "$(svc_get exec)" ]; then
|
||||
exec_args="$(svc_get exec)"
|
||||
# G1-7:存量 exec= 里的 --dsn 剥离到环境变量(迁移;命令行不再可见明文)。
|
||||
case "$exec_args" in
|
||||
*--dsn\ *)
|
||||
DSN_ENV="$(printf '%s' "$exec_args" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')"
|
||||
exec_args="$(printf '%s' "$exec_args" | sed 's/--dsn [^ ]*//' | sed 's/[[:space:]]\{2,\}/ /g; s/^ //; s/ $//')"
|
||||
c_info "[$ENV_NAME] DSN 已从命令行迁移到环境变量(G1-7)"
|
||||
;;
|
||||
esac
|
||||
c_info "[$ENV_NAME] 复用上次配置(svc.env exec=;显式参数可覆盖):$(mask_dsn "$exec_args")"
|
||||
c_info "[$ENV_NAME] 复用上次配置(svc.env exec=;显式参数可覆盖):$(mask_dsn "$exec_args")"
|
||||
else
|
||||
local args=""
|
||||
if [ "$CLI_MOCK" = 1 ] || [ "$(env_var "$ENV_NAME" MOCK)" = "1" ]; then
|
||||
local args="" explicit_auth="${CLI_AUTH:-$(env_var "$ENV_NAME" AUTH)}"
|
||||
if [ "$DB_MODE" = mock ]; then
|
||||
args="--mock"
|
||||
else
|
||||
local dsn="${CLI_DSN:-$(env_var "$ENV_NAME" DSN)}"
|
||||
local auth="${CLI_AUTH:-$(env_var "$ENV_NAME" AUTH)}"
|
||||
# dbref 兜底:显式参数重建启动参数时,持久化的数据库绑定不再静默丢失
|
||||
#(2026-10-05 dev 事故:restart --addr 走重建分支丢 --dsn → auto 判认证关 → 守卫拒启)。
|
||||
if [ -z "$dsn" ] && [ -n "$(svc_get dbref)" ]; then
|
||||
local bound_dsn
|
||||
if bound_dsn="$(dsn_from_dbref "$(svc_get dbref)")"; then
|
||||
dsn="$bound_dsn"
|
||||
c_info "[$ENV_NAME] 数据库绑定 $(svc_get dbref) 随参数重建保留"
|
||||
else
|
||||
c_warn "[$ENV_NAME] dbref=$(svc_get dbref) 无法重建 DSN(实例缺失或登记不全),按无数据库绑定启动"
|
||||
fi
|
||||
fi
|
||||
# G1-7:DSN 经环境变量传入(AGENT_SYSTEM_DSN),不再进命令行——命令行对
|
||||
# 同机任何进程可见(含智能体生成脚本的 /proc/<pid>/cmdline),2026-10-09
|
||||
# 方案 §3.4 实测。基座 config 优先级 CLI>env,旧 exec= 里的 --dsn 仍生效
|
||||
# (向后兼容),但新建/重建的启动配置从此不带 DSN 明文。
|
||||
DSN_ENV="$dsn"
|
||||
if [ -n "$dsn" ] || [ -n "$auth" ] || [ "$ENV_NAME" != dev ]; then
|
||||
[ -z "$auth" ] && auth=auto
|
||||
args="$args --auth $auth"
|
||||
fi
|
||||
elif [ "$DB_MODE" = pg ] || [ -n "$explicit_auth" ] || [ "$ENV_NAME" != dev ]; then
|
||||
args="--auth $DB_AUTH"
|
||||
fi
|
||||
# 保留既有配置文件引用:显式参数重建时不丢 --config(conffile= 持久化跨重启)。
|
||||
local conf; conf="$(svc_get conffile)"
|
||||
[ -n "$conf" ] && [ -f "$conf" ] && args="$args --config $conf"
|
||||
local adjusted; adjusted="$(adjust_args_for_bin "$ENV_BIN" "$args")"
|
||||
# adjust_args_for_bin 返回值已去首空格——比较前对 args 同样归一,否则每次
|
||||
# 带 --dsn/--auth 的重建都会误报"旧版本二进制已自动省略"(参数实际全保留)。
|
||||
# adjust_args_for_bin 返回值已去首空格——比较前对 args 同样归一,否则每次带
|
||||
# --auth/--config 的重建都会误报"旧版本二进制已自动省略"(参数实际全保留)。
|
||||
if [ "$adjusted" != "${args# }" ]; then
|
||||
c_warn "[$ENV_NAME] 该产物不支持 --dsn/--auth(旧版本二进制),已自动省略:$(mask_dsn "${args:-<无>}") → $(mask_dsn "${adjusted:-<无>}")"
|
||||
c_warn "[$ENV_NAME] 该产物不支持部分启动参数(旧版本二进制),已自动省略:${args:-<无>} → ${adjusted:-<无>}"
|
||||
fi
|
||||
args="$adjusted"
|
||||
exec_args="--addr $ENV_ADDR --data $ENV_DATA --plugins ${CLI_PLUGINS:-$ENV_DIR/plugins} $args $CLI_EXTRA"
|
||||
@@ -1278,9 +1557,12 @@ agent_do_start() {
|
||||
[ -n "$hosts" ] && guard_env+=("AGENT_SYSTEM_ALLOWED_HOSTS=$hosts")
|
||||
# 预检(快速拦截):非回环地址且认证判定为关时提前拒绝,报错可操作——
|
||||
# 不等二进制 listen guard 以"进程即退"暴露;二进制守卫仍是权威。
|
||||
# 认证判定依据统一解析的最终有效 DSN 与 auth(含环境变量与配置文件来源),
|
||||
# 与二进制 AuthEnabled 口径一致(2026-10-11 修复:此前只看命令行 --dsn,
|
||||
# DSN 迁入环境变量后 auto 被误判为未开认证)。
|
||||
local pf_addr; pf_addr="$(printf '%s' "$exec_args" | sed -n 's/.*--addr \([^ ]*\).*/\1/p')"
|
||||
pf_addr="${pf_addr:-$ENV_ADDR}"
|
||||
if ! addr_is_loopback "${pf_addr%:*}" && [ -z "$insecure" ] && ! agent_args_authed "$exec_args"; then
|
||||
if ! addr_is_loopback "${pf_addr%:*}" && [ -z "$insecure" ] && ! db_auth_enabled; then
|
||||
c_err "[$ENV_NAME] 拒绝启动:非回环地址 $pf_addr 且认证判定为关(auto 无 DSN 即关)。"
|
||||
c_err " 任选其一:--addr 127.0.0.1:${pf_addr##*:};数据库绑定 scripts/svc.sh $ENV_NAME db <pg实例>;"
|
||||
c_err " 显式 --auth on;或环境 SVC_$(printf '%s' "$ENV_NAME" | tr 'a-z-' 'A-Z_')_INSECURE_LISTEN=1 显式放行。"
|
||||
@@ -1294,8 +1576,19 @@ agent_do_start() {
|
||||
fi
|
||||
# 直接 nohup 简单命令:$! 即服务进程本身,pidfile 语义才成立。
|
||||
# 守卫变量走环境,不写进 exec=;旧二进制不认识这两个变量,会忽略。
|
||||
# G1-7:DSN 同样走环境(AGENT_SYSTEM_DSN;基座 config 环境档已支持)。
|
||||
[ -n "${DSN_ENV:-}" ] && guard_env+=("AGENT_SYSTEM_DSN=$DSN_ENV")
|
||||
# G1-7:DSN 经环境变量传入(AGENT_SYSTEM_DSN),不进命令行——命令行对同机任何
|
||||
# 进程可见(含智能体生成脚本的 /proc/<pid>/cmdline)。conffile 来源由二进制自行
|
||||
# 读文件,不重复传递。环境层交付前再核一次产物能力(start 可能紧跟 deploy 换产物),
|
||||
# 不支持即拒绝——绝不静默按内嵌 SQLite 运行。
|
||||
if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ]; then
|
||||
if ! bin_env_dsn_supported "$ENV_BIN"; then
|
||||
c_err "[$ENV_NAME] 拒绝启动:产物不消费环境变量 DSN(AGENT_SYSTEM_DSN),无法按绑定交付数据库配置(不回退 SQLite)"
|
||||
c_err " 更新产物(deploy 新版本),或改用携带 dsn 的配置文件(scripts/svc.sh $ENV_NAME config <file.json>)"
|
||||
hist_event start fail "" "产物不支持环境 DSN,拒绝启动"
|
||||
return 1
|
||||
fi
|
||||
guard_env+=("AGENT_SYSTEM_DSN=$DB_DSN")
|
||||
fi
|
||||
if [ "${#guard_env[@]}" -gt 0 ];then
|
||||
nohup env "${guard_env[@]}" "$ENV_BIN" serve $exec_args >> "$ENV_DIR/svc.log" 2>&1 &
|
||||
else
|
||||
@@ -1671,13 +1964,16 @@ check_port_available() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# svc_set_key KEY VALUE:改写当前服务 svc.env 的单键(先读后写,避开重定向截断陷阱)。
|
||||
# svc_set_key KEY VALUE:改写当前服务 svc.env 的单键(先读后写 + 临时文件原子替换,
|
||||
# 避免重定向截断与半写)。
|
||||
svc_set_key() {
|
||||
local key="$1" val="$2" rest
|
||||
local key="$1" val="$2" rest tmp
|
||||
[ -f "$ENV_DIR/svc.env" ] || { c_err "svc.env 不存在:$ENV_DIR"; return 1; }
|
||||
rest="$(grep -v "^$key=" "$ENV_DIR/svc.env" || true)"
|
||||
{ [ -n "$rest" ] && printf '%s\n' "$rest"; printf '%s=%s\n' "$key" "$val"; } > "$ENV_DIR/svc.env"
|
||||
chmod 600 "$ENV_DIR/svc.env"
|
||||
tmp="$ENV_DIR/svc.env.tmp.$$"
|
||||
{ [ -n "$rest" ] && printf '%s\n' "$rest"; printf '%s=%s\n' "$key" "$val"; } > "$tmp"
|
||||
chmod 600 "$tmp"
|
||||
mv -f "$tmp" "$ENV_DIR/svc.env"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -2134,9 +2430,10 @@ EOF
|
||||
# ---- agent 服务的数据库绑定(<name> db ...)----
|
||||
|
||||
# svc_db_bind [pg名] [--db X] [--clear] [--no-restart]:查看 / 绑定切换 / 解除
|
||||
# agent 服务的数据库。绑定 = 目标实例的 DSN 写入持久化 exec=(--dsn,密码 600 存放)
|
||||
# 并登记 dbref=<pg>/<库>;实例运行中时确保目标库存在(缺则建);服务运行中则自动
|
||||
# 重启生效(--no-restart 只改登记)。--clear 解除绑定(回内嵌 SQLite 档)。
|
||||
# agent 服务的数据库。绑定 = dbref=<pg>/<库> + db=pg 登记(600);DSN 不落 exec=,
|
||||
# 启动时由实例登记重建并经 AGENT_SYSTEM_DSN 环境变量交付(实例运行中缺库自动建;
|
||||
# 服务运行中自动重启生效,--no-restart 只改登记)。--clear 显式解绑:db=sqlite、
|
||||
# 清除 dbref 与直接 dsn=,残留 SVC_*_DSN 不再悄悄重新选库。
|
||||
svc_db_bind() {
|
||||
[ "$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; }
|
||||
@@ -2151,7 +2448,7 @@ svc_db_bind() {
|
||||
[$ENV_NAME] 数据库绑定:
|
||||
scripts/svc.sh $ENV_NAME db 查看当前绑定与可用实例
|
||||
scripts/svc.sh $ENV_NAME db <pg名> [--db X] [--no-restart] 绑定/切换(运行中自动重启生效)
|
||||
scripts/svc.sh $ENV_NAME db --clear [--no-restart] 解除绑定(回内嵌 SQLite 档)
|
||||
scripts/svc.sh $ENV_NAME db --clear [--no-restart] 解除绑定(显式回内嵌 SQLite 档)
|
||||
EOF
|
||||
return 0 ;;
|
||||
-*) c_err "[$ENV_NAME] db 未知参数:$1(可用:<pg名> | --clear,--db X,--no-restart)"; return 2 ;;
|
||||
@@ -2162,12 +2459,17 @@ EOF
|
||||
|
||||
# 无参:查看当前绑定 + 可用实例。
|
||||
if [ -z "$target" ] && [ "$clear" = 0 ]; then
|
||||
local ref dsn pg st
|
||||
local ref dsn pg st mode
|
||||
mode="$(svc_get db)"
|
||||
ref="$(db_ref_of "$ENV_NAME")"
|
||||
if [ -n "$ref" ]; then
|
||||
printf ' [%s] 数据库绑定:%s\n' "$ENV_NAME" "$ref"
|
||||
dsn="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')"
|
||||
dsn="$(svc_get dsn)"
|
||||
[ -z "$dsn" ] && dsn="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')"
|
||||
[ -z "$dsn" ] && [ -n "$(svc_get dbref)" ] && dsn="$(dsn_from_dbref "$(svc_get dbref)" 2>/dev/null || true)"
|
||||
[ -n "$dsn" ] && printf ' DSN %s\n' "$(mask_dsn "$dsn")"
|
||||
elif [ "$mode" = "sqlite" ]; then
|
||||
printf ' [%s] 数据库绑定:已显式解绑(db=sqlite,data=%s;残留 SVC_*_DSN 不再自动选库)\n' "$ENV_NAME" "$ENV_DATA"
|
||||
else
|
||||
printf ' [%s] 数据库绑定:无(内嵌 SQLite 档,data=%s)\n' "$ENV_NAME" "$ENV_DATA"
|
||||
fi
|
||||
@@ -2185,7 +2487,7 @@ EOF
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 解除绑定。
|
||||
# 解除绑定(显式选择 SQLite:清除 dbref 与直接 dsn,环境残留不再重新选库)。
|
||||
if [ "$clear" = 1 ]; then
|
||||
[ -n "$target" ] && { c_err "--clear 不接受服务名参数"; return 2; }
|
||||
local exec_old new_exec
|
||||
@@ -2195,8 +2497,10 @@ EOF
|
||||
svc_set_key exec "$new_exec"
|
||||
fi
|
||||
svc_del_key dbref
|
||||
svc_del_key dsn
|
||||
svc_set_key db sqlite
|
||||
hist_event set ok "$(read_pid)" "db unbind"
|
||||
c_ok "[$ENV_NAME] 已解除数据库绑定(内嵌 SQLite 档;数据目录 $ENV_DATA)"
|
||||
c_ok "[$ENV_NAME] 已解除数据库绑定(显式 db=sqlite;数据目录 $ENV_DATA)"
|
||||
if [ "$restart" = 1 ] && [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then
|
||||
do_stop; do_start || { c_err "[$ENV_NAME] 重启失败——解绑已登记,排查后 start"; return 1; }
|
||||
c_ok "[$ENV_NAME] 已重启(内嵌 SQLite 档)"
|
||||
@@ -2217,7 +2521,21 @@ EOF
|
||||
tpass="$(env_kv_of "$target" password)"
|
||||
regdb="$(env_kv_of "$target" dbname)"
|
||||
tdb="${CLI_DB_NAME:-$regdb}"
|
||||
{ [ -n "$tuser" ] && [ -n "$tpass" ]; } || { c_err "[$target] 登记缺 user/password(重新 db create)"; return 2; }
|
||||
{ [ -n "$tuser" ] && [ -n "$tpass" ] && [ -n "$taddr" ]; } || { c_err "[$target] 登记缺 addr/user/password(重新 db create)"; return 2; }
|
||||
# mock 档与绑定冲突:拒绝——绑定后按 mock 运行等于静默丢库。
|
||||
local exec_now mock_now=0
|
||||
exec_now="$(svc_get exec)"
|
||||
{ [ "$CLI_MOCK" = 1 ] || [ "$(env_var "$ENV_NAME" MOCK)" = "1" ]; } && mock_now=1
|
||||
case "$exec_now" in *--mock*) mock_now=1 ;; esac
|
||||
if [ "$mock_now" = 1 ]; then
|
||||
c_err "[$ENV_NAME] 拒绝绑定:当前为 mock 档(--mock)与数据库配置冲突;先去 mock(重新 deploy 不带 --mock)或保持 mock 不绑定"
|
||||
return 2
|
||||
fi
|
||||
# 产物承接能力预检:绑定后 DSN 经环境变量交付,产物须消费 AGENT_SYSTEM_DSN。
|
||||
if [ -x "$ENV_BIN" ] && ! bin_env_dsn_supported "$ENV_BIN"; then
|
||||
c_err "[$ENV_NAME] 拒绝绑定:当前产物不消费环境变量 DSN(AGENT_SYSTEM_DSN),绑定后无法生效;先更新产物再绑定"
|
||||
return 2
|
||||
fi
|
||||
# 实例运行中:确保目标库存在(缺则建,幂等)。
|
||||
st="$(svc_pid_state "$target")"
|
||||
if [ "${st%% *}" = alive ]; then
|
||||
@@ -2232,26 +2550,19 @@ EOF
|
||||
c_warn "[$target] 未运行——库 $tdb 暂未创建(先 start $target 再重跑,或绑定默认库 $regdb)"
|
||||
fi
|
||||
dsn="postgresql://$tuser:$tpass@$thost:$tport/$tdb"
|
||||
# 产物支持预检(旧二进制不识别 --dsn 时提示;重启复用 exec 不会自动过滤)。
|
||||
if [ -x "$ENV_BIN" ]; then
|
||||
local adj
|
||||
adj="$(adjust_args_for_bin "$ENV_BIN" "--dsn $dsn")"
|
||||
[ "$adj" != "--dsn $dsn" ] && \
|
||||
c_warn "[$ENV_NAME] 当前产物不识别 --dsn(旧版本二进制)——绑定已登记,但重启后将按内嵌档运行"
|
||||
# 写持久化配置(托管绑定形态):dbref= + db=pg;清除直接 dsn=;剥离 exec 残留 --dsn。
|
||||
# DSN 不落 exec=——启动时由实例登记重建(实例端口/密码变更自动跟随,密码不重复保存)。
|
||||
if printf '%s' "$exec_now" | grep -q -- '--dsn '; then
|
||||
local new_exec
|
||||
new_exec="$(printf '%s' "$exec_now" | sed -E 's/(^| )--dsn [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
||||
svc_set_key exec "$new_exec"
|
||||
fi
|
||||
# 写持久化配置:exec= 的 --dsn 替换或追加;dbref= 登记。
|
||||
local exec_old new_exec
|
||||
exec_old="$(svc_get exec)"
|
||||
if printf '%s' "$exec_old" | grep -q -- '--dsn '; then
|
||||
new_exec="$(printf '%s' "$exec_old" | sed -E "s|--dsn [^ ]+|--dsn $dsn|")"
|
||||
else
|
||||
new_exec="${exec_old:+$exec_old }--dsn $dsn"
|
||||
fi
|
||||
svc_set_key exec "$new_exec"
|
||||
svc_set_key dbref "$target/$tdb"
|
||||
svc_set_key db pg
|
||||
svc_del_key dsn
|
||||
hist_event set ok "$(read_pid)" "db bind → $target/$tdb"
|
||||
c_ok "[$ENV_NAME] 数据库绑定:$target/$tdb(DSN $(mask_dsn "$dsn");svc.env 600 保存,info 可查)"
|
||||
# 运行中:重启生效(start 复用新 exec=)。
|
||||
c_ok "[$ENV_NAME] 数据库绑定:$target/$tdb(启动时由实例登记重建 DSN:$(mask_dsn "$dsn");svc.env 600 保存,info 可查)"
|
||||
# 运行中:重启生效(start 按新登记重建 DSN 交付)。
|
||||
if [ "$restart" = 1 ] && [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then
|
||||
c_info "[$ENV_NAME] 重启以切换数据库……"
|
||||
do_stop
|
||||
@@ -2370,6 +2681,17 @@ do_stop_quiet() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# do_restart:agent 先完成可静态检查的配置验证(数据库绑定可解析、conffile 存在、
|
||||
# 产物承接能力)再停旧进程——配置性拒绝不制造停机窗口,登记保持可恢复;其余类型
|
||||
# 直接停止后启动。
|
||||
do_restart() {
|
||||
if [ "$SVC_KIND" = agent ]; then
|
||||
agent_static_preflight || { hist_event start fail "" "restart 预检拒绝(数据库配置/配置文件无效)"; return 1; }
|
||||
fi
|
||||
do_stop
|
||||
do_start
|
||||
}
|
||||
|
||||
cmd_rm() {
|
||||
local name="${1:-}"
|
||||
[ -n "$name" ] || { c_err "用法:scripts/svc.sh rm <name> [--purge]"; exit 2; }
|
||||
@@ -2427,6 +2749,7 @@ db_ref_of() {
|
||||
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')"
|
||||
[ -z "$dsn" ] && dsn="$(env_kv_of "$name" dsn)"
|
||||
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')"
|
||||
@@ -2564,13 +2887,13 @@ mask_dsn() {
|
||||
printf '%s' "$1" | sed -E 's#(postgres(ql)?://[^:/@[:space:]]+:)[^@[:space:]]+@#\1***@#g'
|
||||
}
|
||||
|
||||
# mask_kv LINE:KEY=VALUE 行的敏感值掩码——password 键与键名含 KEY/PASS/SECRET/
|
||||
# mask_kv LINE:KEY=VALUE 行的敏感值掩码——password/dsn 键与键名含 KEY/PASS/SECRET/
|
||||
# TOKEN/DSN 的值整段掩为 ***;其余行只掩值内 DSN 密码(exec= 里可能带完整连接串)。
|
||||
mask_kv() {
|
||||
local line="$1" k="${1%%=*}"
|
||||
if [ "$k" != "$1" ]; then
|
||||
case "$k" in
|
||||
password|*KEY*|*PASS*|*SECRET*|*TOKEN*|*DSN*)
|
||||
password|dsn|*KEY*|*PASS*|*SECRET*|*TOKEN*|*DSN*)
|
||||
printf '%s=***' "$k"; return 0 ;;
|
||||
esac
|
||||
fi
|
||||
@@ -3066,7 +3389,7 @@ main() {
|
||||
esac ;;
|
||||
stop) do_stop ;;
|
||||
start) do_start ;;
|
||||
restart) do_stop; do_start ;;
|
||||
restart) do_restart ;;
|
||||
status) env_status_detail ;;
|
||||
logs) [ -f "$ENV_LOG" ] && tail -n "${CLI_LOGS_N:-50}" -f "$ENV_LOG" || c_warn "[$ENV_NAME] 暂无日志" ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user