agent-exec transport 03522b193f49679a69e784a2cc3fd118/431f463cce7f
This commit is contained in:
@@ -31,20 +31,20 @@ fi
|
|||||||
id dev >/dev/null 2>&1 || fail_expect "用户 dev 存在" "用户 dev 不存在"
|
id dev >/dev/null 2>&1 || fail_expect "用户 dev 存在" "用户 dev 不存在"
|
||||||
|
|
||||||
sudo_list="$(sudo -l -U dev 2>&1 || true)"
|
sudo_list="$(sudo -l -U dev 2>&1 || true)"
|
||||||
echo "${sudo_list}" | grep -q "NOPASSWD" || fail_expect "sudo -l -U dev 含 NOPASSWD" "${sudo_list}"
|
grep -q "NOPASSWD" <<<"${sudo_list}" || fail_expect "sudo -l -U dev 含 NOPASSWD" "${sudo_list}"
|
||||||
|
|
||||||
if [ "${PLAYWRIGHT_BROWSERS_PATH:-}" != "/opt/ms-playwright" ]; then
|
if [ "${PLAYWRIGHT_BROWSERS_PATH:-}" != "/opt/ms-playwright" ]; then
|
||||||
fail_expect "PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright" "${PLAYWRIGHT_BROWSERS_PATH:-<未设置>}"
|
fail_expect "PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright" "${PLAYWRIGHT_BROWSERS_PATH:-<未设置>}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chromium_path="$(
|
chromium_path="$(
|
||||||
find /opt/ms-playwright -type f \( -name headless_shell -o -name chrome \) 2>/dev/null | head -n 1
|
find /opt/ms-playwright -type f \( -name headless_shell -o -name chrome \) 2>/dev/null | sed -n 1p
|
||||||
)"
|
)"
|
||||||
|
|
||||||
[ -n "${chromium_path}" ] || fail_expect "在 /opt/ms-playwright 下存在 headless_shell 或 chrome" "未找到可执行文件"
|
[ -n "${chromium_path}" ] || fail_expect "在 /opt/ms-playwright 下存在 headless_shell 或 chrome" "未找到可执行文件"
|
||||||
[ -x "${chromium_path}" ] || fail_expect "chromium 可执行文件具有执行权限" "${chromium_path} 不可执行"
|
[ -x "${chromium_path}" ] || fail_expect "chromium 可执行文件具有执行权限" "${chromium_path} 不可执行"
|
||||||
|
|
||||||
chromium_dom="$("${chromium_path}" --headless --no-sandbox --disable-gpu --dump-dom about:blank 2>&1 || true)"
|
chromium_dom="$("${chromium_path}" --headless --no-sandbox --disable-gpu --dump-dom about:blank 2>&1 || true)"
|
||||||
echo "${chromium_dom}" | grep -q "<html" || fail_expect "chromium --dump-dom 输出包含 <html" "${chromium_dom}"
|
grep -q "<html" <<<"${chromium_dom}" || fail_expect "chromium --dump-dom 输出包含 <html" "${chromium_dom}"
|
||||||
|
|
||||||
echo "verify-env: PASS"
|
echo "verify-env: PASS"
|
||||||
|
|||||||
+1
-1
@@ -208,7 +208,7 @@ log "seed: load cockpit doc-tree into $PID_MAIN"
|
|||||||
"$OUT/agent-system-rdm" -data "$DATA" \
|
"$OUT/agent-system-rdm" -data "$DATA" \
|
||||||
-structure "$DEV_ROOT/docs/structure/cockpit-e2e.yaml" -project "$PID_MAIN" \
|
-structure "$DEV_ROOT/docs/structure/cockpit-e2e.yaml" -project "$PID_MAIN" \
|
||||||
>"$OUT/structure-cockpit.log" 2>&1 || { tail -5 "$OUT/structure-cockpit.log"; die "cockpit structure load failed"; }
|
>"$OUT/structure-cockpit.log" 2>&1 || { tail -5 "$OUT/structure-cockpit.log"; die "cockpit structure load failed"; }
|
||||||
grep -o '装载(.*)' "$OUT/structure.log" | head -1 || true
|
grep -m1 -o '装载(.*)' "$OUT/structure.log" || true
|
||||||
|
|
||||||
# 阶段③:重启服务,切片入库挂载到已装载骨架(API)。
|
# 阶段③:重启服务,切片入库挂载到已装载骨架(API)。
|
||||||
start_serve
|
start_serve
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ for dir in deps/agent-intent deps/agent-native deps/agent-squad; do
|
|||||||
deps_out=$(cd "$root/$dir" && GOWORK=off go list -deps ./...) || fail=1
|
deps_out=$(cd "$root/$dir" && GOWORK=off go list -deps ./...) || fail=1
|
||||||
for sib in "${siblings[@]}"; do
|
for sib in "${siblings[@]}"; do
|
||||||
[ "$sib" = "$mod" ] && continue
|
[ "$sib" = "$mod" ] && continue
|
||||||
if printf '%s\n' "$deps_out" | grep -E -q "^(${sib}|${sib}/)"; then
|
if grep -E -q "^(${sib}|${sib}/)" <<<"$deps_out"; then
|
||||||
echo "boundary: $mod core depends on $sib" >&2
|
echo "boundary: $mod core depends on $sib" >&2
|
||||||
fail=1
|
fail=1
|
||||||
fi
|
fi
|
||||||
|
|||||||
+27
-27
@@ -897,13 +897,13 @@ derive_port() {
|
|||||||
# svc_get KEY:读当前服务 svc.env 的键值(无则空)。
|
# svc_get KEY:读当前服务 svc.env 的键值(无则空)。
|
||||||
svc_get() {
|
svc_get() {
|
||||||
[ -f "$ENV_DIR/svc.env" ] || return 0
|
[ -f "$ENV_DIR/svc.env" ] || return 0
|
||||||
sed -n "s/^$1=//p" "$ENV_DIR/svc.env" | head -1
|
sed -n "/^$1=/{s///p;q;}" "$ENV_DIR/svc.env"
|
||||||
}
|
}
|
||||||
|
|
||||||
# svc_kind NAME:服务类型(svc.env kind=,无登记或无 kind 键 → agent,兼容旧登记)。
|
# svc_kind NAME:服务类型(svc.env kind=,无登记或无 kind 键 → agent,兼容旧登记)。
|
||||||
svc_kind() {
|
svc_kind() {
|
||||||
local k=""
|
local k=""
|
||||||
[ -f "$RUNTIME/$1/svc.env" ] && k="$(sed -n 's/^kind=//p' "$RUNTIME/$1/svc.env" | head -1)"
|
[ -f "$RUNTIME/$1/svc.env" ] && k="$(sed -n '/^kind=/{s///p;q;}' "$RUNTIME/$1/svc.env")"
|
||||||
[ -n "$k" ] && printf '%s' "$k" || printf agent
|
[ -n "$k" ] && printf '%s' "$k" || printf agent
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1064,7 +1064,7 @@ port_owner_pid() {
|
|||||||
local line pid
|
local line pid
|
||||||
line="$(port_owner)"
|
line="$(port_owner)"
|
||||||
[ -n "$line" ] || return 1
|
[ -n "$line" ] || return 1
|
||||||
pid="$(printf '%s' "$line" | sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' | head -1)"
|
pid="$(sed -n '/.*pid=\([0-9][0-9]*\).*/{s//\1/p;q;}' <<<"$line")"
|
||||||
[ -n "$pid" ] || return 1
|
[ -n "$pid" ] || return 1
|
||||||
printf '%s' "$pid"
|
printf '%s' "$pid"
|
||||||
}
|
}
|
||||||
@@ -1473,7 +1473,7 @@ PY
|
|||||||
iid="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["instance_id"])' "$ENV_DIR/manifest.json")"
|
iid="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["instance_id"])' "$ENV_DIR/manifest.json")"
|
||||||
[ "$CLI_CONFIRM" = "$iid" ] || { c_err "[$ENV_NAME] --migrate-schema 需要 --confirm $iid"; return 1; }
|
[ "$CLI_CONFIRM" = "$iid" ] || { c_err "[$ENV_NAME] --migrate-schema 需要 --confirm $iid"; return 1; }
|
||||||
backup="$(agent_backup_root)"
|
backup="$(agent_backup_root)"
|
||||||
if ! find "$backup" -mindepth 1 -maxdepth 1 -type d -print -quit | grep -q .; then
|
if ! grep -q . < <(find "$backup" -mindepth 1 -maxdepth 1 -type d -print -quit); then
|
||||||
c_err "[$ENV_NAME] forward-only 迁移必须先 backup"
|
c_err "[$ENV_NAME] forward-only 迁移必须先 backup"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -1700,7 +1700,7 @@ adjust_args_for_bin() {
|
|||||||
case "$w" in
|
case "$w" in
|
||||||
--auth|--config)
|
--auth|--config)
|
||||||
flag="${w#--}"
|
flag="${w#--}"
|
||||||
if printf '%s\n' "$out" | grep -Eq -- "(^|[[:space:]])-{1,2}${flag}([[:space:]]|=|$)"; then
|
if grep -Eq -- "(^|[[:space:]])-{1,2}${flag}([[:space:]]|=|$)" <<<"$out"; then
|
||||||
filtered="$filtered $w"
|
filtered="$filtered $w"
|
||||||
else
|
else
|
||||||
drop_next=1
|
drop_next=1
|
||||||
@@ -2429,7 +2429,7 @@ materialize_release() {
|
|||||||
fi
|
fi
|
||||||
if [ -f "$mf" ]; then
|
if [ -f "$mf" ]; then
|
||||||
local want have
|
local want have
|
||||||
want="$(sed -n 's/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/\1/p' "$mf" | head -1)"
|
want="$(sed -n '/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/{s//\1/p;q;}' "$mf")"
|
||||||
have="$(sha256sum "$bin" | cut -d' ' -f1)"
|
have="$(sha256sum "$bin" | cut -d' ' -f1)"
|
||||||
if [ -n "$want" ] && [ "$want" != "$have" ]; then
|
if [ -n "$want" ] && [ "$want" != "$have" ]; then
|
||||||
c_err "[$ENV_NAME] sha256 校验失败:制品与 manifest 不符($bin)——拒绝部署"
|
c_err "[$ENV_NAME] sha256 校验失败:制品与 manifest 不符($bin)——拒绝部署"
|
||||||
@@ -3317,7 +3317,7 @@ agent_do_deploy() {
|
|||||||
fi
|
fi
|
||||||
local mf="$REL_ROOT/versions/$REL_VERSION/manifest.json" want have
|
local mf="$REL_ROOT/versions/$REL_VERSION/manifest.json" want have
|
||||||
if [ -f "$mf" ]; then
|
if [ -f "$mf" ]; then
|
||||||
want="$(sed -n 's/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/\1/p' "$mf" | head -1)"
|
want="$(sed -n '/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/{s//\1/p;q;}' "$mf")"
|
||||||
have="$(sha256sum "$relbin" | cut -d' ' -f1)"
|
have="$(sha256sum "$relbin" | cut -d' ' -f1)"
|
||||||
if [ -n "$want" ] && [ "$want" != "$have" ]; then
|
if [ -n "$want" ] && [ "$want" != "$have" ]; then
|
||||||
c_err "[$ENV_NAME] sha256 校验失败:制品与 manifest 不符($relbin)——拒绝部署"
|
c_err "[$ENV_NAME] sha256 校验失败:制品与 manifest 不符($relbin)——拒绝部署"
|
||||||
@@ -3574,7 +3574,7 @@ for u in users:
|
|||||||
"$ENV_BIN" apikey revoke ${seed_dbf[@]+"${seed_dbf[@]}"} --owner "$seed_owner" --name svc-seed >/dev/null 2>&1 \
|
"$ENV_BIN" apikey revoke ${seed_dbf[@]+"${seed_dbf[@]}"} --owner "$seed_owner" --name svc-seed >/dev/null 2>&1 \
|
||||||
|| c_warn "[$ENV_NAME] 临时播种密钥回收失败:请手动吊销 owner=$seed_owner name=svc-seed"
|
|| c_warn "[$ENV_NAME] 临时播种密钥回收失败:请手动吊销 owner=$seed_owner name=svc-seed"
|
||||||
fi
|
fi
|
||||||
if echo "$resp" | grep -q '"ok": *true'; then
|
if grep -q '"ok": *true' <<<"$resp"; then
|
||||||
local did=""
|
local did=""
|
||||||
[ -n "${CLI_MBASE:-${SVC_MODEL_BASE:-}}${CLI_MKEY:-${SVC_MODEL_KEY:-}}${CLI_MNAME:-${SVC_MODEL_NAME:-}}" ] && did="主模型"
|
[ -n "${CLI_MBASE:-${SVC_MODEL_BASE:-}}${CLI_MKEY:-${SVC_MODEL_KEY:-}}${CLI_MNAME:-${SVC_MODEL_NAME:-}}" ] && did="主模型"
|
||||||
[ -n "${CLI_LBASE:-${SVC_LIGHT_BASE:-}}${CLI_LKEY:-${SVC_LIGHT_KEY:-}}${CLI_LNAME:-${SVC_LIGHT_NAME:-}}" ] && did="${did:+$did、}轻量模型"
|
[ -n "${CLI_LBASE:-${SVC_LIGHT_BASE:-}}${CLI_LKEY:-${SVC_LIGHT_KEY:-}}${CLI_LNAME:-${SVC_LIGHT_NAME:-}}" ] && did="${did:+$did、}轻量模型"
|
||||||
@@ -3940,7 +3940,7 @@ bin_env_dsn_supported() {
|
|||||||
# 「支持」(回归教训:test_svc 的 legacy 桩即脚本形态)。
|
# 「支持」(回归教训:test_svc 的 legacy 桩即脚本形态)。
|
||||||
[ "$(head -c 4 "$1" 2>/dev/null)" = $'\x7fELF' ] || return 1
|
[ "$(head -c 4 "$1" 2>/dev/null)" = $'\x7fELF' ] || return 1
|
||||||
command -v strings >/dev/null 2>&1 || return 1
|
command -v strings >/dev/null 2>&1 || return 1
|
||||||
strings "$1" 2>/dev/null | grep -q 'AGENT_SYSTEM_DSN' && return 0
|
grep -q 'AGENT_SYSTEM_DSN' < <(strings "$1" 2>/dev/null) && return 0
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4412,7 +4412,7 @@ pg_do_start() {
|
|||||||
# 首次启动:建默认库(已存在则跳过)。
|
# 首次启动:建默认库(已存在则跳过)。
|
||||||
local dbname; dbname="$(svc_get dbname)"
|
local dbname; dbname="$(svc_get dbname)"
|
||||||
if [ -n "$dbname" ]; then
|
if [ -n "$dbname" ]; then
|
||||||
if ! pg_psql "SELECT 1 FROM pg_database WHERE datname='$dbname'" tA | grep -q 1; then
|
if ! grep -q 1 < <(pg_psql "SELECT 1 FROM pg_database WHERE datname='$dbname'" tA); then
|
||||||
if pg_psql "CREATE DATABASE \"$dbname\""; then
|
if pg_psql "CREATE DATABASE \"$dbname\""; then
|
||||||
c_ok "[$ENV_NAME] 已建库:$dbname"
|
c_ok "[$ENV_NAME] 已建库:$dbname"
|
||||||
else
|
else
|
||||||
@@ -4920,7 +4920,7 @@ EOF
|
|||||||
[ -n "$target" ] && { c_err "--clear 不接受文件参数"; return 2; }
|
[ -n "$target" ] && { c_err "--clear 不接受文件参数"; return 2; }
|
||||||
local exec_old new_exec
|
local exec_old new_exec
|
||||||
exec_old="$(svc_get exec)"
|
exec_old="$(svc_get exec)"
|
||||||
if printf '%s' "$exec_old" | grep -q -- '--config '; then
|
if grep -q -- '--config ' <<<"$exec_old"; then
|
||||||
new_exec="$(printf '%s' "$exec_old" | sed -E 's/(^| )--config [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
new_exec="$(printf '%s' "$exec_old" | sed -E 's/(^| )--config [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
||||||
svc_set_key exec "$new_exec"
|
svc_set_key exec "$new_exec"
|
||||||
fi
|
fi
|
||||||
@@ -4946,13 +4946,13 @@ EOF
|
|||||||
c_err "(键与取值以 config explain 为准;先修正文件再切换)"
|
c_err "(键与取值以 config explain 为准;先修正文件再切换)"
|
||||||
return 2
|
return 2
|
||||||
fi
|
fi
|
||||||
if ! "$ENV_BIN" serve -h 2>&1 | grep -Eq -- "(^|[[:space:]])-{1,2}config([[:space:]]|=|$)"; then
|
if ! grep -Eq -- "(^|[[:space:]])-{1,2}config([[:space:]]|=|$)" < <("$ENV_BIN" serve -h 2>&1); then
|
||||||
c_warn "[$ENV_NAME] 当前产物不识别 --config(旧版本二进制)——绑定已登记,但重启将忽略该参数"
|
c_warn "[$ENV_NAME] 当前产物不识别 --config(旧版本二进制)——绑定已登记,但重启将忽略该参数"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
local exec_old new_exec
|
local exec_old new_exec
|
||||||
exec_old="$(svc_get exec)"
|
exec_old="$(svc_get exec)"
|
||||||
if printf '%s' "$exec_old" | grep -q -- '--config '; then
|
if grep -q -- '--config ' <<<"$exec_old"; then
|
||||||
new_exec="$(printf '%s' "$exec_old" | sed -E "s|--config [^ ]+|--config $abs|")"
|
new_exec="$(printf '%s' "$exec_old" | sed -E "s|--config [^ ]+|--config $abs|")"
|
||||||
else
|
else
|
||||||
new_exec="${exec_old:+$exec_old }--config $abs"
|
new_exec="${exec_old:+$exec_old }--config $abs"
|
||||||
@@ -5037,7 +5037,7 @@ EOF
|
|||||||
[ -n "$target" ] && { c_err "--clear 不接受服务名参数"; return 2; }
|
[ -n "$target" ] && { c_err "--clear 不接受服务名参数"; return 2; }
|
||||||
local exec_old new_exec
|
local exec_old new_exec
|
||||||
exec_old="$(svc_get exec)"
|
exec_old="$(svc_get exec)"
|
||||||
if printf '%s' "$exec_old" | grep -q -- '--dsn '; then
|
if grep -q -- '--dsn ' <<<"$exec_old"; then
|
||||||
new_exec="$(printf '%s' "$exec_old" | sed -E 's/(^| )--dsn [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
new_exec="$(printf '%s' "$exec_old" | sed -E 's/(^| )--dsn [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
||||||
svc_set_key exec "$new_exec"
|
svc_set_key exec "$new_exec"
|
||||||
fi
|
fi
|
||||||
@@ -5084,7 +5084,7 @@ EOF
|
|||||||
# 实例运行中:确保目标库存在(缺则建,幂等)。
|
# 实例运行中:确保目标库存在(缺则建,幂等)。
|
||||||
st="$(svc_pid_state "$target")"
|
st="$(svc_pid_state "$target")"
|
||||||
if [ "${st%% *}" = alive ]; then
|
if [ "${st%% *}" = alive ]; then
|
||||||
if ! pg_run "$thost" "$tport" "$tuser" "$tpass" postgres "SELECT 1 FROM pg_database WHERE datname='$tdb'" tA | grep -q 1; then
|
if ! grep -q 1 < <(pg_run "$thost" "$tport" "$tuser" "$tpass" postgres "SELECT 1 FROM pg_database WHERE datname='$tdb'" tA); then
|
||||||
if pg_run "$thost" "$tport" "$tuser" "$tpass" postgres "CREATE DATABASE \"$tdb\"" >/dev/null; then
|
if pg_run "$thost" "$tport" "$tuser" "$tpass" postgres "CREATE DATABASE \"$tdb\"" >/dev/null; then
|
||||||
c_ok "[$target] 已建库:$tdb"
|
c_ok "[$target] 已建库:$tdb"
|
||||||
else
|
else
|
||||||
@@ -5097,7 +5097,7 @@ EOF
|
|||||||
dsn="postgresql://$tuser:$tpass@$thost:$tport/$tdb"
|
dsn="postgresql://$tuser:$tpass@$thost:$tport/$tdb"
|
||||||
# 写持久化配置(托管绑定形态):dbref= + db=pg;清除直接 dsn=;剥离 exec 残留 --dsn。
|
# 写持久化配置(托管绑定形态):dbref= + db=pg;清除直接 dsn=;剥离 exec 残留 --dsn。
|
||||||
# DSN 不落 exec=——启动时由实例登记重建(实例端口/密码变更自动跟随,密码不重复保存)。
|
# DSN 不落 exec=——启动时由实例登记重建(实例端口/密码变更自动跟随,密码不重复保存)。
|
||||||
if printf '%s' "$exec_now" | grep -q -- '--dsn '; then
|
if grep -q -- '--dsn ' <<<"$exec_now"; then
|
||||||
local new_exec
|
local new_exec
|
||||||
new_exec="$(printf '%s' "$exec_now" | sed -E 's/(^| )--dsn [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
new_exec="$(printf '%s' "$exec_now" | sed -E 's/(^| )--dsn [^ ]+//; s/ +/ /g; s/^ //; s/ $//')"
|
||||||
svc_set_key exec "$new_exec"
|
svc_set_key exec "$new_exec"
|
||||||
@@ -5153,7 +5153,7 @@ agent_backup_snapshot() { # $1=dest:svc.env + history.jsonl + data/(*.db 一
|
|||||||
[ -f "$ENV_DATA/$rel" ] || continue
|
[ -f "$ENV_DATA/$rel" ] || continue
|
||||||
case "$rel" in
|
case "$rel" in
|
||||||
*"'"*|*'"'*)
|
*"'"*|*'"'*)
|
||||||
if head -c 15 "$ENV_DATA/$rel" | grep -qa '^SQLite format 3$'; then
|
if grep -qa '^SQLite format 3$' < <(head -c 15 "$ENV_DATA/$rel"); then
|
||||||
c_err "[$ENV_NAME] 路径含引号,无法走 sqlite 一致性导出:$rel"
|
c_err "[$ENV_NAME] 路径含引号,无法走 sqlite 一致性导出:$rel"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -5166,7 +5166,7 @@ agent_backup_snapshot() { # $1=dest:svc.env + history.jsonl + data/(*.db 一
|
|||||||
rm -f "$dest/data/$rel-wal" "$dest/data/$rel-shm" # .backup 已并入 WAL 内容
|
rm -f "$dest/data/$rel-wal" "$dest/data/$rel-shm" # .backup 已并入 WAL 内容
|
||||||
else
|
else
|
||||||
rm -f "$dest/data/$rel.bk" # 失败尝试的半成品不留在快照里
|
rm -f "$dest/data/$rel.bk" # 失败尝试的半成品不留在快照里
|
||||||
if head -c 15 "$ENV_DATA/$rel" | grep -qa '^SQLite format 3$'; then
|
if grep -qa '^SQLite format 3$' < <(head -c 15 "$ENV_DATA/$rel"); then
|
||||||
c_err "[$ENV_NAME] SQLite 一致性备份/自检失败:$rel"
|
c_err "[$ENV_NAME] SQLite 一致性备份/自检失败:$rel"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -5446,7 +5446,7 @@ svc_pid_state() { # NAME → "alive <pid>" | "dead"
|
|||||||
[ -f "$d/svc.pid" ] || { printf dead; return 0; }
|
[ -f "$d/svc.pid" ] || { printf dead; return 0; }
|
||||||
pid="$(cat "$d/svc.pid" 2>/dev/null || true)"
|
pid="$(cat "$d/svc.pid" 2>/dev/null || true)"
|
||||||
[ -n "$pid" ] || { printf dead; return 0; }
|
[ -n "$pid" ] || { printf dead; return 0; }
|
||||||
mark="$(sed -n 's/^pidcheck=//p' "$d/svc.env" 2>/dev/null | head -1)"
|
mark="$(sed -n '/^pidcheck=/{s///p;q;}' "$d/svc.env" 2>/dev/null)"
|
||||||
[ -z "$mark" ] && mark="agent-system"
|
[ -z "$mark" ] && mark="agent-system"
|
||||||
if pid_alive_mark "$pid" "$mark"; then printf 'alive %s' "$pid"; else printf dead; fi
|
if pid_alive_mark "$pid" "$mark"; then printf 'alive %s' "$pid"; else printf dead; fi
|
||||||
return 0
|
return 0
|
||||||
@@ -5503,10 +5503,10 @@ global_list() {
|
|||||||
while IFS= read -r name; do names+=("$name"); done < <(registered_names)
|
while IFS= read -r name; do names+=("$name"); done < <(registered_names)
|
||||||
for name in ${names[@]+"${names[@]}"}; do
|
for name in ${names[@]+"${names[@]}"}; do
|
||||||
d="$RUNTIME/$name"
|
d="$RUNTIME/$name"
|
||||||
kind="$(sed -n 's/^kind=//p' "$d/svc.env" | head -1)"; [ -n "$kind" ] || kind=agent
|
kind="$(sed -n '/^kind=/{s///p;q;}' "$d/svc.env")"; [ -n "$kind" ] || kind=agent
|
||||||
addr="$(sed -n 's/^addr=//p' "$d/svc.env" | head -1)"
|
addr="$(sed -n '/^addr=/{s///p;q;}' "$d/svc.env")"
|
||||||
src="$(sed -n 's/^source=//p' "$d/svc.env" | head -1)"
|
src="$(sed -n '/^source=/{s///p;q;}' "$d/svc.env")"
|
||||||
tags="$(sed -n 's/^tags=//p' "$d/svc.env" | head -1)"
|
tags="$(sed -n '/^tags=/{s///p;q;}' "$d/svc.env")"
|
||||||
if [ -n "${LIST_TAG:-}" ]; then
|
if [ -n "${LIST_TAG:-}" ]; then
|
||||||
case ",$tags," in *",${LIST_TAG},"*) ;; *) continue ;; esac
|
case ",$tags," in *",${LIST_TAG},"*) ;; *) continue ;; esac
|
||||||
fi
|
fi
|
||||||
@@ -5673,7 +5673,7 @@ info_file_mode() {
|
|||||||
# info 遍历多服务时使用;无则空)。
|
# info 遍历多服务时使用;无则空)。
|
||||||
env_kv_of() {
|
env_kv_of() {
|
||||||
[ -f "$RUNTIME/$1/svc.env" ] || return 0
|
[ -f "$RUNTIME/$1/svc.env" ] || return 0
|
||||||
sed -n "s/^$2=//p" "$RUNTIME/$1/svc.env" | head -1
|
sed -n "/^$2=/{s///p;q;}" "$RUNTIME/$1/svc.env"
|
||||||
}
|
}
|
||||||
|
|
||||||
# info_snapshot_lines FILE:逐行打印 KEY=VALUE 内容快照(按 CLI_REVEAL 决定掩码)。
|
# info_snapshot_lines FILE:逐行打印 KEY=VALUE 内容快照(按 CLI_REVEAL 决定掩码)。
|
||||||
@@ -5936,7 +5936,7 @@ cmd_update() {
|
|||||||
# rdm@ 走独立 RDM 源仓。
|
# rdm@ 走独立 RDM 源仓。
|
||||||
local latest_sha="" latest_rel="" latest_rdm="" need_rel=0 need_dev=0 need_rdm=0 e_src
|
local latest_sha="" latest_rel="" latest_rdm="" need_rel=0 need_dev=0 need_rdm=0 e_src
|
||||||
for e in ${targets[@]+"${targets[@]}"}; do
|
for e in ${targets[@]+"${targets[@]}"}; do
|
||||||
e_src="$(sed -n 's/^source=//p' "$RUNTIME/$e/svc.env" | head -1)"
|
e_src="$(sed -n '/^source=/{s///p;q;}' "$RUNTIME/$e/svc.env")"
|
||||||
case "$e_src" in release@*) need_rel=1 ;; local@*|dev@*) need_dev=1 ;; rdm@*) need_rdm=1 ;; esac
|
case "$e_src" in release@*) need_rel=1 ;; local@*|dev@*) need_dev=1 ;; rdm@*) need_rdm=1 ;; esac
|
||||||
done
|
done
|
||||||
local fetch_note="" src_args=""
|
local fetch_note="" src_args=""
|
||||||
@@ -5974,7 +5974,7 @@ cmd_update() {
|
|||||||
for e in ${targets[@]+"${targets[@]}"}; do
|
for e in ${targets[@]+"${targets[@]}"}; do
|
||||||
local kind src cur st_running
|
local kind src cur st_running
|
||||||
kind="$(svc_kind "$e")"
|
kind="$(svc_kind "$e")"
|
||||||
src="$(sed -n 's/^source=//p' "$RUNTIME/$e/svc.env" | head -1)"
|
src="$(sed -n '/^source=/{s///p;q;}' "$RUNTIME/$e/svc.env")"
|
||||||
to=""
|
to=""
|
||||||
latest_disp=""
|
latest_disp=""
|
||||||
case "$kind" in
|
case "$kind" in
|
||||||
@@ -6083,7 +6083,7 @@ cmd_update() {
|
|||||||
# 每个服务独立子进程、清空环境,避免批内 AGENT_SYSTEM_* 串实例。
|
# 每个服务独立子进程、清空环境,避免批内 AGENT_SYSTEM_* 串实例。
|
||||||
# 原样带上 plugins=,避免在终端上再次询问,也避免 ${src##*@} 把插件提交当成基座。
|
# 原样带上 plugins=,避免在终端上再次询问,也避免 ${src##*@} 把插件提交当成基座。
|
||||||
local plug plug_args=()
|
local plug plug_args=()
|
||||||
plug="$(sed -n 's/^plugins=//p' "$RUNTIME/$name/svc.env" | head -1)"
|
plug="$(sed -n '/^plugins=/{s///p;q;}' "$RUNTIME/$name/svc.env")"
|
||||||
if [ -n "$plug" ]; then
|
if [ -n "$plug" ]; then
|
||||||
plug_args=(--with "$plug")
|
plug_args=(--with "$plug")
|
||||||
else
|
else
|
||||||
|
|||||||
+13
-13
@@ -20,13 +20,13 @@ fetch_target() {
|
|||||||
}
|
}
|
||||||
origin_transport_unavailable() { # $1=失败的远端命令 stderr → 仅明确的传输失败(DNS/连接超时/拒绝/重置/无路由,或本地缺失仓库路径)返回 0;认证/授权/权限与未知错误返回 1
|
origin_transport_unavailable() { # $1=失败的远端命令 stderr → 仅明确的传输失败(DNS/连接超时/拒绝/重置/无路由,或本地缺失仓库路径)返回 0;认证/授权/权限与未知错误返回 1
|
||||||
local err=$1 url path
|
local err=$1 url path
|
||||||
if printf '%s\n' "$err" | grep -qiE 'authentication failed|permission denied|unauthori[sz]ed|forbidden|HTTP[^0-9]*40[13]|\b40[13]\b|could not read (username|password)|terminal prompts disabled|invalid credentials|access denied|host key verification failed'; then
|
if grep -qiE 'authentication failed|permission denied|unauthori[sz]ed|forbidden|HTTP[^0-9]*40[13]|\b40[13]\b|could not read (username|password)|terminal prompts disabled|invalid credentials|access denied|host key verification failed' <<<"$err"; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if printf '%s\n' "$err" | grep -qiE 'could not resolve (host|hostname|proxy)|name or service not known|temporary failure in name resolution|nodename nor servname|connection timed out|operation timed out|timed out after|connection refused|connection reset|no route to host|network is unreachable|couldn.t connect to server'; then
|
if grep -qiE 'could not resolve (host|hostname|proxy)|name or service not known|temporary failure in name resolution|nodename nor servname|connection timed out|operation timed out|timed out after|connection refused|connection reset|no route to host|network is unreachable|couldn.t connect to server' <<<"$err"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
printf '%s\n' "$err" | grep -qF 'does not appear to be a git repository' || return 1
|
grep -qF 'does not appear to be a git repository' <<<"$err" || return 1
|
||||||
url=$(git remote get-url origin 2>/dev/null) || return 1
|
url=$(git remote get-url origin 2>/dev/null) || return 1
|
||||||
case "$url" in
|
case "$url" in
|
||||||
file://*) path=${url#file://} ;;
|
file://*) path=${url#file://} ;;
|
||||||
@@ -211,12 +211,12 @@ cmd_precheck() {
|
|||||||
cmd_start() {
|
cmd_start() {
|
||||||
need_repo
|
need_repo
|
||||||
local name="${1:-}"; [ -n "$name" ] || die "usage: task.sh start <feature|fix|hotfix|release>/<slug>"
|
local name="${1:-}"; [ -n "$name" ] || die "usage: task.sh start <feature|fix|hotfix|release>/<slug>"
|
||||||
echo "$name" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "branch name must match (feature|fix|hotfix|release)/<slug>"
|
grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' <<<"$name" || die "branch name must match (feature|fix|hotfix|release)/<slug>"
|
||||||
local kind="${TASK_BRANCH_KIND:-standalone}" initiative="${TASK_INITIATIVE:-}"
|
local kind="${TASK_BRANCH_KIND:-standalone}" initiative="${TASK_INITIATIVE:-}"
|
||||||
case "$kind" in standalone|initiative|child) ;; *) die "TASK_BRANCH_KIND must be standalone, initiative or child" ;; esac
|
case "$kind" in standalone|initiative|child) ;; *) die "TASK_BRANCH_KIND must be standalone, initiative or child" ;; esac
|
||||||
if [ "$kind" = child ]; then
|
if [ "$kind" = child ]; then
|
||||||
[ -n "$initiative" ] || die "child task requires INITIATIVE=<parent initiative branch>"
|
[ -n "$initiative" ] || die "child task requires INITIATIVE=<parent initiative branch>"
|
||||||
echo "$initiative" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "invalid parent initiative branch"
|
grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' <<<"$initiative" || die "invalid parent initiative branch"
|
||||||
case "${name#*/}" in "${initiative#*/}"-?*) ;; *) die "child slug must start with parent initiative slug plus '-'" ;; esac
|
case "${name#*/}" in "${initiative#*/}"-?*) ;; *) die "child slug must start with parent initiative slug plus '-'" ;; esac
|
||||||
fi
|
fi
|
||||||
local slug planned_child=0
|
local slug planned_child=0
|
||||||
@@ -312,8 +312,8 @@ cmd_start() {
|
|||||||
esac
|
esac
|
||||||
if [ "$kind" != child ] && [ -n "${TASK_PARENT_BRANCH:-}${TASK_PARENT_PR:-}" ]; then
|
if [ "$kind" != child ] && [ -n "${TASK_PARENT_BRANCH:-}${TASK_PARENT_PR:-}" ]; then
|
||||||
[ -n "${TASK_PARENT_BRANCH:-}" ] && [ -n "${TASK_PARENT_PR:-}" ] || die "stacked task requires TASK_PARENT_BRANCH and TASK_PARENT_PR"
|
[ -n "${TASK_PARENT_BRANCH:-}" ] && [ -n "${TASK_PARENT_PR:-}" ] || die "stacked task requires TASK_PARENT_BRANCH and TASK_PARENT_PR"
|
||||||
echo "$TASK_PARENT_BRANCH" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "invalid parent task branch"
|
grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' <<<"$TASK_PARENT_BRANCH" || die "invalid parent task branch"
|
||||||
echo "$TASK_PARENT_PR" | grep -qE '^https?://[^[:space:]]+/pulls/[0-9]+$' || die "TASK_PARENT_PR must be a parent PR URL"
|
grep -qE '^https?://[^[:space:]]+/pulls/[0-9]+$' <<<"$TASK_PARENT_PR" || die "TASK_PARENT_PR must be a parent PR URL"
|
||||||
case "${name#*/}" in "${TASK_PARENT_BRANCH#*/}"-?*) ;; *) die "stacked child slug must start with parent slug plus '-'" ;; esac
|
case "${name#*/}" in "${TASK_PARENT_BRANCH#*/}"-?*) ;; *) die "stacked child slug must start with parent slug plus '-'" ;; esac
|
||||||
parent_head=$(git rev-parse --verify "refs/heads/${TASK_PARENT_BRANCH}^{commit}") || die "parent branch missing locally"
|
parent_head=$(git rev-parse --verify "refs/heads/${TASK_PARENT_BRANCH}^{commit}") || die "parent branch missing locally"
|
||||||
git merge-base --is-ancestor "$rmain" "$parent_head" || die "parent branch does not include current origin/${TARGET_BRANCH}; sync it first"
|
git merge-base --is-ancestor "$rmain" "$parent_head" || die "parent branch does not include current origin/${TARGET_BRANCH}; sync it first"
|
||||||
@@ -640,13 +640,13 @@ cmd_sync() {
|
|||||||
# 默认闭合方式是 close --self:任务分支在合入前把本分支的卡改为"已完成(合入前闭合)"并归档,
|
# 默认闭合方式是 close --self:任务分支在合入前把本分支的卡改为"已完成(合入前闭合)"并归档,
|
||||||
# 随本 PR 合入;上述合入后回填只用于遗留卡。
|
# 随本 PR 合入;上述合入后回填只用于遗留卡。
|
||||||
|
|
||||||
card_of_branch() { sed -n 's/^- 工作区:.*分支:\([A-Za-z0-9._/-]\+\).*/\1/p' "$1" | head -1; }
|
card_of_branch() { sed -n '/^- 工作区:.*分支:\([A-Za-z0-9._/-]\+\).*/{s//\1/p;q;}' "$1"; }
|
||||||
card_status() { sed -n 's/^.*;状态:\([^;]*\).*/\1/p' "$1" | head -1; }
|
card_status() { sed -n '/^.*;状态:\([^;]*\).*/{s//\1/p;q;}' "$1"; }
|
||||||
# 两级分支元数据行:'- 分支类型:<kind>;initiative_id:…;root_target:…;immediate_target:…;accepting_requirements:…'。
|
# 两级分支元数据行:'- 分支类型:<kind>;initiative_id:…;root_target:…;immediate_target:…;accepting_requirements:…'。
|
||||||
# 旧卡没有该行:按 standalone、root/immediate 均为目标分支、不接收新需求处理。
|
# 旧卡没有该行:按 standalone、root/immediate 均为目标分支、不接收新需求处理。
|
||||||
card_field() { sed -n "s/^- 分支类型:.*;$2:\([^;]*\).*/\1/p" "$1" 2>/dev/null | head -1; }
|
card_field() { sed -n "/^- 分支类型:.*;$2:\([^;]*\).*/{s//\1/p;q;}" "$1" 2>/dev/null; }
|
||||||
card_r0() { sed -n 's/^- 权威仓:.*;R0(initial_base_sha)= \([0-9a-f]\+\)[[:space:]]*$/\1/p' "$1" 2>/dev/null | head -1; }
|
card_r0() { sed -n '/^- 权威仓:.*;R0(initial_base_sha)= \([0-9a-f]\+\)[[:space:]]*$/{s//\1/p;q;}' "$1" 2>/dev/null; }
|
||||||
card_branch_kind() { local v; v=$(sed -n 's/^- 分支类型:\([^;]*\).*/\1/p' "$1" 2>/dev/null | head -1); printf '%s\n' "${v:-standalone}"; }
|
card_branch_kind() { local v; v=$(sed -n '/^- 分支类型:\([^;]*\).*/{s//\1/p;q;}' "$1" 2>/dev/null); printf '%s\n' "${v:-standalone}"; }
|
||||||
card_initiative_id() { card_field "$1" initiative_id; }
|
card_initiative_id() { card_field "$1" initiative_id; }
|
||||||
card_root_target() { local v; v=$(card_field "$1" root_target); printf '%s\n' "${v:-${TARGET_BRANCH}}"; }
|
card_root_target() { local v; v=$(card_field "$1" root_target); printf '%s\n' "${v:-${TARGET_BRANCH}}"; }
|
||||||
card_immediate_target() { local v; v=$(card_field "$1" immediate_target); printf '%s\n' "${v:-${TARGET_BRANCH}}"; }
|
card_immediate_target() { local v; v=$(card_field "$1" immediate_target); printf '%s\n' "${v:-${TARGET_BRANCH}}"; }
|
||||||
@@ -1067,7 +1067,7 @@ EOF
|
|||||||
-d "{\"head\":\"${headbr}\",\"base\":\"${pr_base}\",\"title\":\"Close merged task cards (${headbr})\",\"body\":\"Auto-generated by task.sh close; see commit message for evidence.\"}" 2>&1) || resp=""
|
-d "{\"head\":\"${headbr}\",\"base\":\"${pr_base}\",\"title\":\"Close merged task cards (${headbr})\",\"body\":\"Auto-generated by task.sh close; see commit message for evidence.\"}" 2>&1) || resp=""
|
||||||
pr_url=$(printf '%s' "$resp" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("html_url","") if isinstance(d,dict) else "")' 2>/dev/null || true)
|
pr_url=$(printf '%s' "$resp" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("html_url","") if isinstance(d,dict) else "")' 2>/dev/null || true)
|
||||||
[ -n "$pr_url" ] && break
|
[ -n "$pr_url" ] && break
|
||||||
echo "[close] 建 PR 第 ${attempt} 次未成功${resp:+($(printf '%s' "$resp" | head -c 120))},退避重试" >&2
|
echo "[close] 建 PR 第 ${attempt} 次未成功${resp:+($(head -c 120 <<<"$resp"))},退避重试" >&2
|
||||||
[ "$attempt" = 4 ] || sleep $((attempt * 3))
|
[ "$attempt" = 4 ] || sleep $((attempt * 3))
|
||||||
done
|
done
|
||||||
if [ -z "$pr_url" ]; then
|
if [ -z "$pr_url" ]; then
|
||||||
|
|||||||
@@ -177,13 +177,13 @@ echo "[pgbench] results $RESULTS"
|
|||||||
df -h "$ROOT"
|
df -h "$ROOT"
|
||||||
pgbench --version
|
pgbench --version
|
||||||
echo "--- heavy processes (comm only) ---"
|
echo "--- heavy processes (comm only) ---"
|
||||||
ps -eo pcpu,pmem,comm --sort=-pcpu | head -12
|
ps -eo pcpu,pmem,comm --sort=-pcpu | sed -n '1,12p'
|
||||||
} >"$RESULTS/host.txt" 2>&1
|
} >"$RESULTS/host.txt" 2>&1
|
||||||
|
|
||||||
echo "[pgbench] creating isolated instance $NAME on 127.0.0.1:$PORT"
|
echo "[pgbench] creating isolated instance $NAME on 127.0.0.1:$PORT"
|
||||||
"$ROOT/scripts/svc.sh" db create "$NAME" --port "$PORT" >"$RESULTS/svc-create.txt" 2>&1
|
"$ROOT/scripts/svc.sh" db create "$NAME" --port "$PORT" >"$RESULTS/svc-create.txt" 2>&1
|
||||||
OWNED=1
|
OWNED=1
|
||||||
addr="$(sed -n 's/^addr=//p' "$ROOT/runtime/$NAME/svc.env" | head -1)"
|
addr="$(sed -n '/^addr=/{s///p;q;}' "$ROOT/runtime/$NAME/svc.env")"
|
||||||
[[ "$addr" == "127.0.0.1:$PORT" ]] || {
|
[[ "$addr" == "127.0.0.1:$PORT" ]] || {
|
||||||
echo "[pgbench] instance addr is $addr, expected 127.0.0.1:$PORT; aborting" >&2
|
echo "[pgbench] instance addr is $addr, expected 127.0.0.1:$PORT; aborting" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
"""托管 shell 脚本:管道末级不得是提前退出的读取器(grep -q/-m、head、sed q)。
|
||||||
|
|
||||||
|
pipefail 下读取器先退出,生产者写满 64 KiB 管道缓冲后收到 SIGPIPE,管道状态报 141,
|
||||||
|
`if producer | grep -q PAT` 与 `if ! ...` 的判定被静默翻转。改用 here-string、进程替换
|
||||||
|
或 sed 打印即退出,读取器的结论才不会丢。
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SCRIPTS = (
|
||||||
|
"scripts/svc.sh",
|
||||||
|
"scripts/task.sh",
|
||||||
|
"scripts/e2e.sh",
|
||||||
|
"scripts/lint-boundaries.sh",
|
||||||
|
"docker/verify-env.sh",
|
||||||
|
"tests/load/collaboration/run_pgbench.sh",
|
||||||
|
)
|
||||||
|
|
||||||
|
HEREDOC = re.compile(r"<<(-?)\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\2")
|
||||||
|
SED_QUIT = re.compile(r"(?:^|[\s;{}'\"/$0-9])[qQ][0-9]*\s*(?:$|[\s;}'\"])")
|
||||||
|
GREP_ARG_OPTS = set("efABCdDX")
|
||||||
|
BOUNDARY = set(" \t\n;&|()<>")
|
||||||
|
|
||||||
|
|
||||||
|
class _Frame:
|
||||||
|
"""一层命令上下文(顶层、$(...)、(...)、<(...))内正在累积的管道。"""
|
||||||
|
|
||||||
|
def __init__(self, line):
|
||||||
|
self.piped, self.stage, self.start, self.cases = False, [], line, []
|
||||||
|
|
||||||
|
|
||||||
|
def pipeline_tails(src):
|
||||||
|
"""词法扫描 bash 源码,返回每条多级管道末级的 (起始行号, 文本);跳过注释与 heredoc 正文。"""
|
||||||
|
tails, frames, modes, heredocs = [], [_Frame(1)], ["cmd"], []
|
||||||
|
line, word, i, n = 1, "", 0, len(src)
|
||||||
|
|
||||||
|
def emit(s):
|
||||||
|
for f in frames:
|
||||||
|
f.stage.append(s)
|
||||||
|
|
||||||
|
def finish(f):
|
||||||
|
if f.piped:
|
||||||
|
tails.append((f.start, "".join(f.stage).strip()))
|
||||||
|
f.piped, f.stage, f.start = False, [], line
|
||||||
|
|
||||||
|
def case_state():
|
||||||
|
cases = frames[-1].cases
|
||||||
|
return cases[-1] if cases else None
|
||||||
|
|
||||||
|
def keyword(w):
|
||||||
|
cases = frames[-1].cases
|
||||||
|
if w == "case":
|
||||||
|
cases.append("in")
|
||||||
|
elif w == "in" and case_state() == "in":
|
||||||
|
cases[-1] = "pattern"
|
||||||
|
elif w == "esac" and cases:
|
||||||
|
cases.pop()
|
||||||
|
|
||||||
|
def push_cmd(opener):
|
||||||
|
emit(opener)
|
||||||
|
modes.append("cmd")
|
||||||
|
frames.append(_Frame(line))
|
||||||
|
|
||||||
|
while i < n:
|
||||||
|
c, nxt = src[i], src[i + 1 : i + 2]
|
||||||
|
mode = modes[-1]
|
||||||
|
if mode in ("sq", "ansi"):
|
||||||
|
if c == "\\" and mode == "ansi":
|
||||||
|
emit(src[i : i + 2])
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
emit(c)
|
||||||
|
line += c == "\n"
|
||||||
|
if c == "'":
|
||||||
|
modes.pop()
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if mode in ("dq", "brace"):
|
||||||
|
if c == "\\":
|
||||||
|
emit(src[i : i + 2])
|
||||||
|
line += nxt == "\n"
|
||||||
|
i += 2
|
||||||
|
elif c == "$" and nxt == "(":
|
||||||
|
push_cmd("$(")
|
||||||
|
i += 2
|
||||||
|
elif c == "$" and nxt == "{":
|
||||||
|
emit("${")
|
||||||
|
modes.append("brace")
|
||||||
|
i += 2
|
||||||
|
elif c == '"':
|
||||||
|
emit(c)
|
||||||
|
if mode == "dq":
|
||||||
|
modes.pop()
|
||||||
|
else:
|
||||||
|
modes.append("dq")
|
||||||
|
i += 1
|
||||||
|
elif c == "}" and mode == "brace":
|
||||||
|
emit(c)
|
||||||
|
modes.pop()
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
emit(c)
|
||||||
|
line += c == "\n"
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if c in BOUNDARY and word:
|
||||||
|
keyword(word)
|
||||||
|
word = ""
|
||||||
|
top = frames[-1]
|
||||||
|
if c == "\\":
|
||||||
|
if nxt == "\n":
|
||||||
|
emit(" ")
|
||||||
|
line += 1
|
||||||
|
else:
|
||||||
|
emit(src[i : i + 2])
|
||||||
|
word += "\0"
|
||||||
|
i += 2
|
||||||
|
elif c == "#" and (i == 0 or src[i - 1] in " \t\n;&|("):
|
||||||
|
j = src.find("\n", i)
|
||||||
|
i = n if j < 0 else j
|
||||||
|
elif c == "'" or (c == "$" and nxt == "'"):
|
||||||
|
emit(src[i : i + 2] if c == "$" else c)
|
||||||
|
modes.append("ansi" if c == "$" else "sq")
|
||||||
|
word += "\0"
|
||||||
|
i += 2 if c == "$" else 1
|
||||||
|
elif c == '"':
|
||||||
|
emit(c)
|
||||||
|
modes.append("dq")
|
||||||
|
word += "\0"
|
||||||
|
i += 1
|
||||||
|
elif c == "$" and nxt == "{":
|
||||||
|
emit("${")
|
||||||
|
modes.append("brace")
|
||||||
|
word += "\0"
|
||||||
|
i += 2
|
||||||
|
elif c == "$" and nxt == "(":
|
||||||
|
push_cmd("$(")
|
||||||
|
word += "\0"
|
||||||
|
i += 2
|
||||||
|
elif c == "(":
|
||||||
|
if case_state() == "pattern":
|
||||||
|
emit(c)
|
||||||
|
else:
|
||||||
|
push_cmd(c)
|
||||||
|
i += 1
|
||||||
|
elif c == ")":
|
||||||
|
if case_state() == "pattern":
|
||||||
|
top.cases[-1] = "body"
|
||||||
|
elif len(frames) > 1:
|
||||||
|
finish(top)
|
||||||
|
frames.pop()
|
||||||
|
modes.pop()
|
||||||
|
emit(c)
|
||||||
|
i += 1
|
||||||
|
elif c == "|":
|
||||||
|
if nxt == "|":
|
||||||
|
finish(top)
|
||||||
|
emit("||")
|
||||||
|
i += 2
|
||||||
|
elif case_state() == "pattern":
|
||||||
|
emit(c)
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
emit(c)
|
||||||
|
top.piped, top.stage, top.start = True, [], line
|
||||||
|
i += 2 if nxt == "&" else 1
|
||||||
|
elif c == ";":
|
||||||
|
if src.startswith(";;", i) or src.startswith(";&", i):
|
||||||
|
if case_state() == "body":
|
||||||
|
top.cases[-1] = "pattern"
|
||||||
|
k = 3 if src.startswith(";;&", i) else 2
|
||||||
|
else:
|
||||||
|
k = 1
|
||||||
|
finish(top)
|
||||||
|
emit(src[i : i + k])
|
||||||
|
i += k
|
||||||
|
elif c == "&":
|
||||||
|
if nxt == "&":
|
||||||
|
finish(top)
|
||||||
|
emit("&&")
|
||||||
|
i += 2
|
||||||
|
elif nxt == ">" or (i > 0 and src[i - 1] in "<>"):
|
||||||
|
emit(c)
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
finish(top)
|
||||||
|
emit(c)
|
||||||
|
i += 1
|
||||||
|
elif c == "<" and src.startswith("<<", i):
|
||||||
|
m = None if src.startswith("<<<", i) else HEREDOC.match(src, i)
|
||||||
|
if m:
|
||||||
|
heredocs.append((m.group(3), m.group(1) == "-"))
|
||||||
|
emit(m.group(0))
|
||||||
|
i = m.end()
|
||||||
|
else:
|
||||||
|
k = 3 if src.startswith("<<<", i) else 2
|
||||||
|
emit(src[i : i + k])
|
||||||
|
i += k
|
||||||
|
elif c == "\n":
|
||||||
|
line += 1
|
||||||
|
i += 1
|
||||||
|
for term, dash in heredocs:
|
||||||
|
while i < n:
|
||||||
|
j = src.find("\n", i)
|
||||||
|
body = src[i : n if j < 0 else j]
|
||||||
|
i = n if j < 0 else j + 1
|
||||||
|
line += 1
|
||||||
|
if (body.lstrip("\t") if dash else body) == term:
|
||||||
|
break
|
||||||
|
heredocs = []
|
||||||
|
if not (top.piped and not "".join(top.stage).strip()):
|
||||||
|
finish(top)
|
||||||
|
emit("\n")
|
||||||
|
else:
|
||||||
|
emit(c)
|
||||||
|
if c not in BOUNDARY:
|
||||||
|
word += c
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if len(frames) != 1 or modes != ["cmd"]:
|
||||||
|
raise ValueError(f"unbalanced shell lexing at EOF: modes={modes}")
|
||||||
|
finish(frames[0])
|
||||||
|
return tails
|
||||||
|
|
||||||
|
|
||||||
|
def _grep_exits_early(words):
|
||||||
|
for w in words[1:]:
|
||||||
|
w = w.strip("'\"")
|
||||||
|
if w == "--":
|
||||||
|
break
|
||||||
|
if w.startswith("--"):
|
||||||
|
if w.split("=")[0] in ("--quiet", "--silent", "--max-count"):
|
||||||
|
return True
|
||||||
|
elif w.startswith("-") and len(w) > 1:
|
||||||
|
for ch in w[1:]:
|
||||||
|
if ch in "qm":
|
||||||
|
return True
|
||||||
|
if ch in GREP_ARG_OPTS:
|
||||||
|
break
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def early_exit_reader(stage):
|
||||||
|
"""末级命令读到结论后是否会提前关闭 stdin(grep -q/-m、head、带 q 命令的 sed)。"""
|
||||||
|
words = stage.split()
|
||||||
|
if not words:
|
||||||
|
return False
|
||||||
|
cmd = words[0]
|
||||||
|
if cmd in ("grep", "egrep", "fgrep"):
|
||||||
|
return _grep_exits_early(words)
|
||||||
|
if cmd == "head":
|
||||||
|
return True
|
||||||
|
if cmd == "sed":
|
||||||
|
return bool(SED_QUIT.search(stage.split(None, 1)[1] if len(words) > 1 else ""))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def hazards(src):
|
||||||
|
return [(line, text) for line, text in pipeline_tails(src) if early_exit_reader(text)]
|
||||||
|
|
||||||
|
|
||||||
|
class ShellPipeHazardTests(unittest.TestCase):
|
||||||
|
def test_detector_flags_synthetic_hazard(self):
|
||||||
|
found = hazards("set -euo pipefail\nseq 1 200000 | grep -q 1\n")
|
||||||
|
self.assertEqual([line for line, _ in found], [2], found)
|
||||||
|
|
||||||
|
def test_detector_flags_hazard_shapes(self):
|
||||||
|
for snippet in (
|
||||||
|
'if ! seq 1 200000 | grep -q 1; then echo miss; fi',
|
||||||
|
'x="$(sed -n \'s/^k=//p\' "$f" | head -1)"',
|
||||||
|
'if ! "$bin" serve -h 2>&1 | grep -Eq -- "(^|[[:space:]])-{1,2}config"; then :; fi',
|
||||||
|
'echo "${r:+($(printf \'%s\' "$r" | head -c 120))}"',
|
||||||
|
'find . -type f \\\n | grep -m1 x',
|
||||||
|
"seq 1 200000 | sed -n '/1/{p;q;}'",
|
||||||
|
'seq 1 200000 | grep -qiE "a|b" || exit 1',
|
||||||
|
):
|
||||||
|
with self.subTest(snippet=snippet):
|
||||||
|
self.assertTrue(hazards(snippet + "\n"), snippet)
|
||||||
|
|
||||||
|
def test_detector_accepts_safe_forms(self):
|
||||||
|
src = "\n".join((
|
||||||
|
"set -euo pipefail",
|
||||||
|
"# seq 1 200000 | grep -q 1",
|
||||||
|
"grep -q 1 < <(seq 1 200000)",
|
||||||
|
'grep -q x <<<"$v"',
|
||||||
|
"x=\"$(sed -n '/^k=/{s///p;q;}' \"$f\")\"",
|
||||||
|
"ps -eo comm | sed -n '1,12p'",
|
||||||
|
"case \"$w\" in --auth|--config) grep -q x <<<\"$w\" ;; esac",
|
||||||
|
"cat <<'EOF'",
|
||||||
|
"seq 1 200000 | grep -q 1",
|
||||||
|
"EOF",
|
||||||
|
"",
|
||||||
|
))
|
||||||
|
self.assertEqual(hazards(src), [])
|
||||||
|
|
||||||
|
def test_managed_scripts_have_no_early_exit_pipe_tail(self):
|
||||||
|
for rel in SCRIPTS:
|
||||||
|
with self.subTest(script=rel):
|
||||||
|
path = ROOT / rel
|
||||||
|
self.assertTrue(path.is_file(), f"{rel} missing")
|
||||||
|
found = hazards(path.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(
|
||||||
|
found, [], "\n".join(f"{rel}:{line}: | {text}" for line, text in found)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user