@@ -1,7 +1,8 @@
#!/usr/bin/env bash
# svc.sh —— 本机服务管理(服务模型 v2):按提交/版本部署并启动 后端(agent) / 前端(static)
# / 数据库(postgres) 三类服务,统一登记(svc.env + history.jsonl)、标签、配置分层、
# 生命周期(start/stop/restart/rm) ,全程不触碰主检出与其他已启动服务。
# 生命周期(start/stop/restart/rm) 与数据库运维(换端口/备份/恢复/克隆/搬库),
# 全程不触碰主检出与其他已启动服务。
#
# 用法:
# scripts/svc.sh <name> [action] [参数...] # 服务级操作(name 见命名规则)
@@ -9,6 +10,13 @@
# scripts/svc.sh list [--tag T] # 全服务总览(status 等价)
# scripts/svc.sh db create <name> [参数] # 创建 PostgreSQL 服务(initdb)
# scripts/svc.sh db dsn <name> [--reveal] [--db X] # 打印数据库连接串(默认掩码密码)
# scripts/svc.sh db backup <name> [--db X] [--out F] # pg_dump 备份(自动自检归档)
# scripts/svc.sh db backups <name> # 列出已有备份
# scripts/svc.sh db restore <name> --file F [--db X] [--force] # 恢复(覆盖非空库须 --force)
# scripts/svc.sh db clone <src> <dst> [--port P] [--password PW] [--online] # 克隆实例
# scripts/svc.sh db copy <src> <dst> [--db X] [--force] # 跨实例搬库(双方须运行)
# scripts/svc.sh db ls <name> # 列出实例内数据库
# scripts/svc.sh set <name> [--port N --apply] [--tag T] # 变更登记(换端口/标签)
# scripts/svc.sh rm <name> [--purge] # 注销服务(--purge 连数据一起删)
# scripts/svc.sh commits [dev|release] [N] # 列出可部署提交(默认 dev,N=20)
# scripts/svc.sh history [name] [N] # 事件历史(默认全部,最近 20 条)
@@ -51,6 +59,11 @@
# scripts/svc.sh ui-qa deploy --kind static --dir ../some/dist
# scripts/svc.sh list --tag core && scripts/svc.sh history
# scripts/svc.sh pg1 restart && scripts/svc.sh rm tmp1 --purge
# scripts/svc.sh set pg1 --port 8895 --apply # 换端口并重启生效(登记/DSN 联动)
# scripts/svc.sh db backup pg1 && scripts/svc.sh db backups pg1
# scripts/svc.sh db clone pg1 pg2 --port 8896 # 物理克隆一套到新端口
# scripts/svc.sh db copy pg1 pg2 --db app # 把 pg1 的 app 库搬到 pg2
# scripts/svc.sh db restore pg2 --file <dump> --db app --force
#
# 安全约束:
# - 只写 runtime/<name>/ 与 runtime/.worktrees/;不改主检出 git 状态、不动他人现场;
@@ -59,7 +72,10 @@
# - 只停自己启动的进程:pidfile + /proc/<pid>/cmdline 标记双校验;postgres 走
# pg_ctl -m fast 优雅停机,SIGKILL 不用于 postmaster;
# - 端口被非本脚本进程占用时报错并给出占用者,不抢不杀;
# - svc.env 含密码时权限 600; DSN 默认掩码密码(--reveal 才显示)。
# - svc.env 含密码时权限 600; DSN 默认掩码密码(--reveal 才显示);
# - 备份/克隆只增不删;restore/copy 覆盖非空库必须显式 --force(drop 重建再恢复);
# 备份文件在 runtime/<name>/backups/( 600),随 rm --purge 一并删除;
# - 物理克隆期间源实例短暂停止(一致性要求),完成后自动复启;--online 不停源。
# <<usage-end>>
set -euo pipefail
@@ -105,6 +121,7 @@ CLI_MBASE=""; CLI_MKEY=""; CLI_MNAME=""
CLI_LBASE = "" ; CLI_LKEY = "" ; CLI_LNAME = "" ; CLI_TAVILY = ""
CLI_FROM = "" ; CLI_PICK = "" ; CLI_DRYRUN = 0
CLI_KIND = "" ; CLI_DIR = "" ; CLI_PURGE = 0; CLI_REVEAL = 0
CLI_APPLY = 0; CLI_FORCE = 0; CLI_ONLINE = 0; CLI_FILE = "" ; CLI_OUT = ""
CLI_DB_USER = "" ; CLI_DB_PASSWORD = "" ; CLI_DB_NAME = "" ; CLI_DB_AUTH = ""
CLI_TAGS = ( )
@@ -137,6 +154,11 @@ parse_args() {
--tag) need_value " $@ " ; CLI_TAGS += ( " $2 " ) ; shift 2 ; ;
--purge) CLI_PURGE = 1; shift ; ;
--reveal) CLI_REVEAL = 1; shift ; ;
--apply) CLI_APPLY = 1; shift ; ;
--force) CLI_FORCE = 1; shift ; ;
--online) CLI_ONLINE = 1; shift ; ;
--file) need_value " $@ " ; CLI_FILE = " $2 " ; shift 2 ; ;
--out) need_value " $@ " ; CLI_OUT = " $2 " ; shift 2 ; ;
--user) need_value " $@ " ; CLI_DB_USER = " $2 " ; shift 2 ; ;
--password) need_value " $@ " ; CLI_DB_PASSWORD = " $2 " ; shift 2 ; ;
--db) need_value " $@ " ; CLI_DB_NAME = " $2 " ; shift 2 ; ;
@@ -159,7 +181,7 @@ addr_compose() {
}
# ---- 名称与端口 ----
RESERVED_ENVS = " status list add db rm create dsn commits history deploy stop start restart logs help version "
RESERVED_ENVS = " status list add set db rm create dsn commits history deploy stop start restart logs help version "
# valid_env NAME:服务名合法性(小写字母开头、a-z0-9-、≤16 位;保留字不可用)。
valid_env( ) {
@@ -930,9 +952,23 @@ cmd_db() {
local sub = " ${ 1 :- } "
[ $# -gt 0 ] && shift
case " $sub " in
create) db_create " $@ " ; ;
dsn) db_dsn " $@ " ; ;
* ) c_err "用法:scripts/svc.sh db create <name> [--port --user --password --db --auth-mode --tag] | db dsn <name> [--reveal] [--db X]" ; exit 2 ; ;
create) db_create " $@ " ; ;
dsn) db_dsn " $@ " ; ;
backup ) db_backup " $@ " ; ;
backups) db_backups " $@ " ; ;
restore) db_restore " $@ " ; ;
clone) db_clone " $@ " ; ;
copy) db_copy " $@ " ; ;
ls) db_ls " $@ " ; ;
set ) cmd_set " $@ " ; ;
*) c_err "用法:scripts/svc.sh db create|dsn|backup|backups|restore|clone|copy|ls|set <name> [...]
db create <name> [--port --user --password --db --auth-mode --tag]
db dsn <name> [--reveal] [--db X]
db backup <name> [--db X] [--out F] | db backups <name>
db restore <name> --file F [--db X] [--force]
db clone <src> <dst> [--port P] [--password PW] [--tag T] [--online]
db copy <src> <dst> [--db X] [--force]
db ls <name> | db set <name> [--port N --apply]" ; exit 2 ; ;
esac
}
@@ -1093,6 +1129,398 @@ db_dsn() {
return 0
}
# ---- 数据库运维:换端口 / 备份 / 恢复 / 克隆 / 搬库 ----
# check_port_available PORT SKIP_NAME:显式选端口时校验(未被其他登记占用且未在监听)。
check_port_available( ) {
local p = " $1 " skip = " $2 "
if ports_recorded " $p " " $skip " ; then
c_err " [ $ENV_NAME ] 端口 $p 已被其他服务登记占用 "
return 1
fi
if port_listening " $p " ; then
c_err " [ $ENV_NAME ] 端口 $p 正被监听(非本脚本登记的进程) "
return 1
fi
return 0
}
# svc_set_key KEY VALUE:改写当前服务 svc.env 的单键(先读后写,避开重定向截断陷阱)。
svc_set_key( ) {
local key = " $1 " val = " $2 " rest
[ -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 "
return 0
}
# 显式参数版 psql/dump/restore: clone/copy 需要同时操作两个实例,不依赖全局 ENV_*。
pg_run( ) { # HOST PORT USER PASS DB SQL [tA]
local host = " $1 " port = " $2 " user = " $3 " pass = " $4 " db = " $5 " sql = " $6 " ta = " ${ 7 :- } " pgbin
pgbin = " $( pg_bindir) "
PGPASSWORD = " $pass " " $pgbin /psql " -h " $host " -p " $port " -U " $user " -d " $db " ${ ta : +-tA } -c " $sql " 2>/dev/null
}
pg_dump_run( ) { # HOST PORT USER PASS DB OUT
local host = " $1 " port = " $2 " user = " $3 " pass = " $4 " db = " $5 " out = " $6 " pgbin
pgbin = " $( pg_bindir) "
PGPASSWORD = " $pass " " $pgbin /pg_dump " -h " $host " -p " $port " -U " $user " -d " $db " -Fc -f " $out "
}
pg_restore_list( ) { # FILE: exit 0 = 合法 pg_dump 归档
" $( pg_bindir) /pg_restore " --list " $1 " >/dev/null 2>& 1
}
pg_restore_run( ) { # HOST PORT USER PASS DB FILE:出错回显 stderr 尾部
local host = " $1 " port = " $2 " user = " $3 " pass = " $4 " db = " $5 " file = " $6 " pgbin err
pgbin = " $( pg_bindir) "
if ! err = " $( PGPASSWORD = " $pass " " $pgbin /pg_restore " -h " $host " -p " $port " -U " $user " -d " $db " \
--no-owner --no-privileges " $file " 2>& 1) " ; then
printf '%s\n' " $err " | tail -5 >& 2
return 1
fi
return 0
}
# copy_database SH SP SU SPASS SDB DH DP DU DPASS DDB [FORCE]:单库逻辑复制
# ( dump → 建库/覆盖 → restore);目标库非空且无 FORCE 时拒绝。返回 0 = 成功。
copy_database( ) {
local sh = " $1 " sp = " $2 " su = " $3 " spass = " $4 " sdb = " $5 "
local dh = " $6 " dp = " $7 " du = " $8 " dpass = " $9 " ddb = " ${ 10 } " force = " ${ 11 :- 0 } "
local tmp exists objects
tmp = " $( mktemp /tmp/svc-copy-XXXXXXXX.dump) "
if ! pg_dump_run " $sh " " $sp " " $su " " $spass " " $sdb " " $tmp " ; then
rm -f " $tmp " ; c_err " pg_dump $sdb 失败(源 $sh : $sp ) " ; return 1
fi
if ! pg_restore_list " $tmp " ; then
rm -f " $tmp " ; c_err "备份归档自检失败" ; return 1
fi
exists = " $( pg_run " $dh " " $dp " " $du " " $dpass " postgres " SELECT 1 FROM pg_database WHERE datname=' $ddb ' " tA || true ) "
if [ -n " $exists " ] ; then
objects = " $( pg_run " $dh " " $dp " " $du " " $dpass " " $ddb " "SELECT count(*) FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema')" tA || true ) "
case " $objects " in '' | *[ !0-9] *) objects = 0 ; ; esac
if [ " $objects " -gt 0 ] && [ " $force " != 1 ] ; then
rm -f " $tmp "
c_err " 目标库 $ddb 已有 $objects 张表——拒绝覆盖(--force 覆盖) "
return 1
fi
if [ " $force " = 1 ] ; then
pg_run " $dh " " $dp " " $du " " $dpass " postgres " DROP DATABASE IF EXISTS \" $ddb \" WITH (FORCE) " >/dev/null \
|| { rm -f " $tmp " ; c_err " drop $ddb 失败 " ; return 1; }
pg_run " $dh " " $dp " " $du " " $dpass " postgres " CREATE DATABASE \" $ddb \" " >/dev/null \
|| { rm -f " $tmp " ; c_err " create $ddb 失败 " ; return 1; }
fi
else
pg_run " $dh " " $dp " " $du " " $dpass " postgres " CREATE DATABASE \" $ddb \" " >/dev/null \
|| { rm -f " $tmp " ; c_err " create $ddb 失败 " ; return 1; }
fi
if ! pg_restore_run " $dh " " $dp " " $du " " $dpass " " $ddb " " $tmp " ; then
rm -f " $tmp " ; return 1
fi
rm -f " $tmp "
return 0
}
db_backup( ) {
local name = " ${ 1 :- } "
[ -n " $name " ] || { c_err "用法:scripts/svc.sh db backup <name> [--db X] [--out FILE]" ; exit 2; }
shift
parse_args " $@ "
env_config " $name "
[ " $SVC_KIND " = postgres ] || { c_err " [ $ENV_NAME ] kind= $SVC_KIND 不是 postgres " ; exit 2; }
if [ -z " $( read_pid) " ] || ! pid_alive " $( read_pid) " ; then
c_err " [ $ENV_NAME ] 未运行——备份走 TCP,请先 scripts/svc.sh $ENV_NAME start "
exit 1
fi
local db = " ${ CLI_DB_NAME :- $( svc_get dbname) } "
local host = " ${ ENV_ADDR % : * } " port = " ${ ENV_ADDR ##* : } "
local user pass; user = " $( svc_get user) " ; pass = " $( svc_get password) "
local out = " ${ CLI_OUT :- $ENV_DIR /backups/ $( date -u +%Y%m%dT%H%M%SZ) - $db .dump } "
mkdir -p " $( dirname " $out " ) " ; chmod 700 " $( dirname " $out " ) "
( umask 077; : > " $out " )
c_info " [ $ENV_NAME ] pg_dump $db → $out "
if ! pg_dump_run " $host " " $port " " $user " " $pass " " $db " " $out " ; then
rm -f " $out "
hist_event backup fail " $( read_pid) " " db= $db "
c_err " [ $ENV_NAME ] 备份失败(上方输出) "
exit 1
fi
if ! pg_restore_list " $out " ; then
rm -f " $out "
hist_event backup fail " $( read_pid) " " 归档自检失败 db= $db "
c_err " [ $ENV_NAME ] 备份归档自检失败(pg_restore --list 不通过),已删除 "
exit 1
fi
hist_event backup ok " $( read_pid) " " db= $db file= $( basename " $out " ) size= $( du -h " $out " | cut -f1) "
c_ok " [ $ENV_NAME ] 备份完成: $out ( $( du -h " $out " | cut -f1) ) "
}
db_backups( ) {
local name = " ${ 1 :- } "
[ -n " $name " ] || { c_err "用法:scripts/svc.sh db backups <name>" ; exit 2; }
env_config " $name "
[ " $SVC_KIND " = postgres ] || { c_err " [ $ENV_NAME ] kind= $SVC_KIND 不是 postgres " ; exit 2; }
local dir = " $ENV_DIR /backups " f
if [ ! -d " $dir " ] || [ -z " $( ls " $dir " /*.dump 2>/dev/null || true ) " ] ; then
c_info " [ $ENV_NAME ] 暂无备份(scripts/svc.sh db backup $ENV_NAME ) "
return 0
fi
c_info " [ $ENV_NAME ] 备份列表( $dir ) "
for f in " $dir " /*.dump; do
[ -f " $f " ] || continue
printf ' %s %6s %s\n' " $( stat -c %y " $f " | cut -d. -f1) " " $( du -h " $f " | cut -f1) " " $( basename " $f " ) "
done
return 0
}
db_restore( ) {
local name = " ${ 1 :- } "
[ -n " $name " ] || { c_err "用法:scripts/svc.sh db restore <name> --file F [--db X] [--force]" ; exit 2; }
shift
parse_args " $@ "
[ -n " $CLI_FILE " ] || { c_err "需要 --file <备份文件>" ; exit 2; }
[ -f " $CLI_FILE " ] || { c_err " 备份文件不存在: $CLI_FILE " ; exit 2; }
env_config " $name "
[ " $SVC_KIND " = postgres ] || { c_err " [ $ENV_NAME ] kind= $SVC_KIND 不是 postgres " ; exit 2; }
if [ -z " $( read_pid) " ] || ! pid_alive " $( read_pid) " ; then
c_err " [ $ENV_NAME ] 未运行——恢复走 TCP,请先 scripts/svc.sh $ENV_NAME start "
exit 1
fi
pg_restore_list " $CLI_FILE " || { c_err " 不是有效的 pg_dump 归档: $CLI_FILE " ; exit 2; }
local db = " ${ CLI_DB_NAME :- $( svc_get dbname) } "
local host = " ${ ENV_ADDR % : * } " port = " ${ ENV_ADDR ##* : } "
local user pass; user = " $( svc_get user) " ; pass = " $( svc_get password) "
local exists objects
exists = " $( pg_run " $host " " $port " " $user " " $pass " postgres " SELECT 1 FROM pg_database WHERE datname=' $db ' " tA || true ) "
if [ -n " $exists " ] ; then
objects = " $( pg_run " $host " " $port " " $user " " $pass " " $db " "SELECT count(*) FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema')" tA || true ) "
case " $objects " in '' | *[ !0-9] *) objects = 0 ; ; esac
if [ " $objects " -gt 0 ] && [ " $CLI_FORCE " != 1 ] ; then
c_err " [ $ENV_NAME ] 目标库 $db 已存在且含 $objects 张表——拒绝覆盖;确认覆盖加 --force "
exit 1
fi
if [ " $CLI_FORCE " = 1 ] ; then
c_warn " [ $ENV_NAME ] --force:删除并重建库 $db "
pg_run " $host " " $port " " $user " " $pass " postgres " DROP DATABASE IF EXISTS \" $db \" WITH (FORCE) " >/dev/null \
|| { c_err " [ $ENV_NAME ] drop $db 失败 " ; exit 1; }
pg_run " $host " " $port " " $user " " $pass " postgres " CREATE DATABASE \" $db \" " >/dev/null \
|| { c_err " [ $ENV_NAME ] create $db 失败 " ; exit 1; }
fi
else
pg_run " $host " " $port " " $user " " $pass " postgres " CREATE DATABASE \" $db \" " >/dev/null \
|| { c_err " [ $ENV_NAME ] 建库 $db 失败 " ; exit 1; }
fi
c_info " [ $ENV_NAME ] 恢复 $db ← $CLI_FILE "
if ! pg_restore_run " $host " " $port " " $user " " $pass " " $db " " $CLI_FILE " ; then
hist_event restore fail " $( read_pid) " " db= $db file= $( basename " $CLI_FILE " ) "
c_err " [ $ENV_NAME ] 恢复失败(上方输出) "
exit 1
fi
local tables
tables = " $( pg_run " $host " " $port " " $user " " $pass " " $db " "SELECT count(*) FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema')" tA || true ) "
hist_event restore ok " $( read_pid) " " db= $db file= $( basename " $CLI_FILE " ) tables= ${ tables :- ? } "
c_ok " [ $ENV_NAME ] 恢复完成:库 $db ( ${ tables :- ? } 张表)← $( basename " $CLI_FILE " ) "
}
db_clone( ) {
local src = " ${ 1 :- } " dst = " ${ 2 :- } "
[ -n " $src " ] && [ -n " $dst " ] || {
c_err "用法:scripts/svc.sh db clone <src> <dst> [--port P] [--host H] [--password PW] [--tag T] [--online]"
exit 2
}
shift 2
parse_args " $@ "
local req_port = " ${ CLI_PORT :- } " req_host = " ${ CLI_HOST :- } "
CLI_PORT = "" ; CLI_HOST = "" ; CLI_ADDR = "" # 克隆不消费地址覆盖——防泄漏到源复启与目标解析
valid_env " $dst " || { c_err " 非法目标服务名: $dst " ; exit 2; }
[ " $dst " != " $src " ] || { c_err "目标不能与源相同" ; exit 2; }
env_config " $src "
[ " $SVC_KIND " = postgres ] || { c_err " [ $src ] kind= $SVC_KIND 不是 postgres " ; exit 2; }
local src_name = " $ENV_NAME " src_addr = " $ENV_ADDR " src_dir = " $ENV_DIR "
local src_user pass db auth pgdata ver tags
src_user = " $( svc_get user) " ; pass = " $( svc_get password) " ; db = " $( svc_get dbname) "
auth = " $( svc_get authmode) " ; pgdata = " $( svc_get pgdata) " ; ver = " $( svc_get version) " ; tags = " $( svc_get tags) "
[ -n " $pgdata " ] && [ -d " $pgdata " ] || { c_err " [ $src ] 无数据目录 " ; exit 1; }
if [ " $CLI_ONLINE " = 1 ] ; then
# 逻辑克隆:不停源;目标须已 create+start,复制源登记库。
[ -f " $RUNTIME / $dst /svc.env " ] || {
c_err " [ $dst ] 未登记——--online 要求目标先 db create + start(或省略 --online 走物理克隆) "
exit 2
}
env_config " $dst "
[ " $SVC_KIND " = postgres ] || { c_err " [ $dst ] kind= $SVC_KIND 不是 postgres " ; exit 2; }
[ -n " $( read_pid) " ] && pid_alive " $( read_pid) " || { c_err " [ $dst ] 未运行——先 start " ; exit 1; }
local dh = " ${ ENV_ADDR % : * } " dp = " ${ ENV_ADDR ##* : } " du dpass dname
du = " $( svc_get user) " ; dpass = " $( svc_get password) " ; dname = " $ENV_NAME "
c_info " [ $src ]. $db → [ $dst ]. $db 在线克隆(不停源) "
if copy_database " ${ src_addr % : * } " " ${ src_addr ##* : } " " $src_user " " $pass " " $db " \
" $dh " " $dp " " $du " " $dpass " " $db " " $CLI_FORCE " ; then
ENV_NAME = " $dname " ; ENV_DIR = " $RUNTIME / $dname " ; ENV_ADDR = " $dh : $dp " ; SVC_KIND = postgres; SRC_LABEL = " clone@ $src_name "
hist_event clone ok "" " online db= $db from $src_name "
ENV_NAME = " $src_name " ; ENV_DIR = " $src_dir " ; ENV_ADDR = " $src_addr "
hist_event clone ok "" " online db= $db to $dst "
c_ok " [ $src ] → [ $dst ] 在线克隆完成(库 $db ) "
return 0
fi
ENV_NAME = " $src_name " ; ENV_DIR = " $src_dir " ; ENV_ADDR = " $src_addr "
hist_event clone fail "" " online db= $db to $dst "
c_err "在线克隆失败(上方输出)"
return 1
fi
# 物理克隆(默认):短暂停源 → cp pgdata → 登记目标 → 复启源;目标不自动启动。
[ -e " $RUNTIME / $dst " ] && { c_err " [ $dst ] 目录已存在: $RUNTIME / $dst (换名或 rm) " ; exit 2; }
local saved_name = " $ENV_NAME "
ENV_NAME = " $dst " # 端口校验以目标视角提示
local dst_port = " ${ req_port :- } "
if [ -n " $dst_port " ] ; then
check_port_available " $dst_port " " $dst " || exit 1
else
dst_port = " $( derive_port " $dst " ) " || { c_err " [ $dst ] 无空闲派生端口,请 --port 指定 " ; exit 1; }
fi
ENV_NAME = " $saved_name "
local dst_host = " ${ req_host :- 127 .0.0.1 } "
local dst_pass = " ${ CLI_DB_PASSWORD :- $pass } "
local was_running = 0
if [ -n " $( read_pid) " ] && pid_alive " $( read_pid) " ; then was_running = 1; fi
if [ " $was_running " = 1 ] ; then
c_info " [ $src ] 物理克隆需短暂停止源实例(一致性),完成后自动复启 "
pg_do_stop || { c_err " [ $src ] 停止失败,克隆中止(数据未动) " ; return 1; }
fi
mkdir -p " $RUNTIME / $dst " ; chmod 700 " $RUNTIME / $dst "
c_info " [ $dst ] 复制数据目录( $pgdata → $RUNTIME / $dst /pgdata) "
if ! cp -a " $pgdata " " $RUNTIME / $dst /pgdata " ; then
c_err " [ $dst ] 数据目录复制失败 "
[ " $was_running " = 1 ] && { env_config " $src_name " ; pg_do_start; }
return 1
fi
rm -f " $RUNTIME / $dst /pgdata/postmaster.pid " " $RUNTIME / $dst /pgdata/postmaster.opts "
local t out_tags = "" seen = " "
for t in $( printf '%s' " $tags " | tr ',' ' ' ) ${ CLI_TAGS [@]+ " ${ CLI_TAGS [@] } " } ; do
[ -n " $t " ] || continue
case " $seen " in *" $t " *) continue ; ; esac
seen = " $seen $t " ; out_tags = " ${ out_tags : + $out_tags , } $t "
done
{
printf 'kind=postgres\n'
printf 'env=%s\n' " $dst "
printf 'pid=\n'
printf 'addr=%s:%s\n' " $dst_host " " $dst_port "
printf 'tags=%s\n' " $out_tags "
printf 'source=clone@%s\n' " $src_name "
printf 'version=%s\n' " $ver "
printf 'pidcheck=%s\n' " $RUNTIME / $dst /pgdata "
printf 'user=%s\n' " $src_user "
printf 'password=%s\n' " $dst_pass "
printf 'dbname=%s\n' " $db "
printf 'authmode=%s\n' " $auth "
printf 'pgdata=%s\n' " $RUNTIME / $dst /pgdata "
} > " $RUNTIME / $dst /svc.env "
chmod 600 " $RUNTIME / $dst /svc.env "
ENV_NAME = " $dst " ; ENV_DIR = " $RUNTIME / $dst " ; ENV_ADDR = " $dst_host : $dst_port " ; SVC_KIND = postgres
SRC_LABEL = " clone@ $src_name "
hist_event create ok "" " physical clone from $src_name port= $dst_port "
if [ " $was_running " = 1 ] ; then
env_config " $src_name "
pg_do_start || c_warn " [ $src ] 复启失败(克隆已成功;查看 $ENV_DIR /pg.log) "
fi
c_ok " [ $dst ] 克隆完成(未启动):scripts/svc.sh $dst start; DSN: scripts/svc.sh db dsn $dst --reveal "
return 0
}
db_copy( ) {
local src = " ${ 1 :- } " dst = " ${ 2 :- } "
[ -n " $src " ] && [ -n " $dst " ] || { c_err "用法:scripts/svc.sh db copy <src> <dst> [--db X] [--force]" ; exit 2; }
shift 2
parse_args " $@ "
CLI_PORT = "" ; CLI_HOST = "" ; CLI_ADDR = "" # copy 不消费地址覆盖
[ " $src " != " $dst " ] || { c_err "源与目标不能相同" ; exit 2; }
env_config " $src "
[ " $SVC_KIND " = postgres ] || { c_err " [ $src ] kind= $SVC_KIND 不是 postgres " ; exit 2; }
[ -n " $( read_pid) " ] && pid_alive " $( read_pid) " || { c_err " [ $src ] 未运行——copy 走 TCP,双方都需 start " ; exit 1; }
local sh = " ${ ENV_ADDR % : * } " sp = " ${ ENV_ADDR ##* : } " su pass sdb sname sdir
su = " $( svc_get user) " ; pass = " $( svc_get password) " ; sname = " $ENV_NAME " ; sdir = " $ENV_DIR "
sdb = " ${ CLI_DB_NAME :- $( svc_get dbname) } "
env_config " $dst "
[ " $SVC_KIND " = postgres ] || { c_err " [ $dst ] kind= $SVC_KIND 不是 postgres " ; exit 2; }
[ -n " $( read_pid) " ] && pid_alive " $( read_pid) " || { c_err " [ $dst ] 未运行——copy 走 TCP,双方都需 start " ; exit 1; }
local dh = " ${ ENV_ADDR % : * } " dp = " ${ ENV_ADDR ##* : } " du dpass dname
du = " $( svc_get user) " ; dpass = " $( svc_get password) " ; dname = " $ENV_NAME "
c_info " [ $sname ]. $sdb → [ $dname ]. $sdb (逻辑复制) "
if copy_database " $sh " " $sp " " $su " " $pass " " $sdb " " $dh " " $dp " " $du " " $dpass " " $sdb " " $CLI_FORCE " ; then
ENV_NAME = " $dname " ; ENV_DIR = " $RUNTIME / $dname " ; ENV_ADDR = " $dh : $dp " ; SVC_KIND = postgres
hist_event copy ok "" " from $sname db= $sdb "
ENV_NAME = " $sname " ; ENV_DIR = " $sdir " ; ENV_ADDR = " $sh : $sp "
hist_event copy ok "" " to $dname db= $sdb "
c_ok " 复制完成: $sname / $sdb → $dname / $sdb "
return 0
fi
ENV_NAME = " $sname " ; ENV_DIR = " $sdir " ; ENV_ADDR = " $sh : $sp "
hist_event copy fail "" " to $dname db= $sdb "
c_err "复制失败(上方输出)"
return 1
}
db_ls( ) {
local name = " ${ 1 :- } "
[ -n " $name " ] || { c_err "用法:scripts/svc.sh db ls <name>" ; exit 2; }
env_config " $name "
[ " $SVC_KIND " = postgres ] || { c_err " [ $ENV_NAME ] kind= $SVC_KIND 不是 postgres " ; exit 2; }
[ -n " $( read_pid) " ] && pid_alive " $( read_pid) " || { c_err " [ $ENV_NAME ] 未运行——先 start " ; exit 1; }
pg_run " ${ ENV_ADDR % : * } " " ${ ENV_ADDR ##* : } " " $( svc_get user) " " $( svc_get password) " postgres \
"SELECT datname FROM pg_database WHERE NOT datistemplate ORDER BY 1" tA || true
return 0
}
cmd_set( ) {
local name = " ${ 1 :- } "
[ -n " $name " ] || { c_err "用法:scripts/svc.sh set <name> [--port N] [--host H] [--apply] [--tag T...]" ; exit 2; }
shift
parse_args " $@ "
env_config " $name "
[ -f " $ENV_DIR /svc.env " ] || { c_err " [ $ENV_NAME ] 无登记(先 db create / deploy) " ; exit 2; }
# tags:给了 --tag 才改(合并去重;postgres/agent/static 通用)。
if [ " ${# CLI_TAGS [@] } " -gt 0 ] ; then
local merged; merged = " $( svc_merge_tags) "
svc_set_key tags " $merged "
c_ok " [ $ENV_NAME ] 标签更新: $merged "
fi
case " $SVC_KIND " in
postgres)
if [ -n " $CLI_ADDR $CLI_HOST $CLI_PORT " ] ; then
local old; old = " $( svc_get addr) " ; [ -n " $old " ] || old = " $ENV_ADDR "
local running = 0
if [ -n " $( read_pid) " ] && pid_alive " $( read_pid) " ; then running = 1; fi
if [ " $running " = 1 ] && [ " $CLI_APPLY " != 1 ] ; then
c_err " [ $ENV_NAME ] 运行中——端口/地址变更需重启生效:加 --apply(停止→改登记→重启),或先 stop 再 set "
exit 1
fi
check_port_available " ${ ENV_ADDR ##* : } " " $ENV_NAME " || exit 1
if [ " $running " = 1 ] ; then
pg_do_stop
svc_set_key addr " $ENV_ADDR "
pg_do_start || { c_err " [ $ENV_NAME ] 新地址重启失败(登记已是 $ENV_ADDR );查看 $ENV_DIR /pg.log " ; return 1; }
else
svc_set_key addr " $ENV_ADDR "
fi
hist_event set ok " $( read_pid) " " addr $old → $ENV_ADDR "
c_ok " [ $ENV_NAME ] 地址已更新: $old → $ENV_ADDR "
elif [ " ${# CLI_TAGS [@] } " -eq 0 ] ; then
c_warn " [ $ENV_NAME ] 未指定任何变更(--port/--host/--addr/--tag) "
fi
; ;
agent| static)
[ -n " $CLI_ADDR $CLI_HOST $CLI_PORT " ] && \
c_warn " [ $ENV_NAME ] agent/static 换端口请用 restart --port N(进程与登记同步更新);set 只改登记 "
[ " ${# CLI_TAGS [@] } " -eq 0 ] && c_warn " [ $ENV_NAME ] 未指定任何变更(--tag) "
; ;
*) c_err " [ $ENV_NAME ] 未知 kind: $SVC_KIND " ; return 1 ; ;
esac
return 0
}
# ---- 生命周期分派 ----
do_start( ) {
@@ -1313,6 +1741,7 @@ main() {
exit 0
; ;
db) shift; cmd_db " $@ " ; exit 0 ; ;
set ) shift; cmd_set " $@ " ; exit 0 ; ;
rm) shift; cmd_rm " $@ " ; exit 0 ; ;
commits) shift; cmd_commits " $@ " ; exit 0 ; ;
history ) shift; cmd_history " $@ " ; exit 0 ; ;