r1 评审 verdict=p2-3items,三条均已按评审给的修法修掉,且都补了可证伪的回归断言。 P2-1 SIGTERM 后遗留孤儿 ntpd,之后每次静默 skip 且 rc=0 - attempt() 把子进程记进全局 NTPD_CHILD 并落盘 <锁目录>/ntpd.pid; 新增 cleanup()(EXIT trap)先收尸子进程再释放锁;新增 on_signal() (INT/TERM/HUP)清理后 exit 143——旧代码的 TERM trap 只清锁、不 exit, 被杀后还会继续跑完看门狗。 - 取代原来的 "pidof ntpd 就 skip":新增 reap_or_skip_ntpd(),用 /proc/<pid>/cmdline 是否含本锁目录来区分「我们自己遗留的孤儿」(收尸) 与「别人的常驻 ntpd」(让路)。崩溃后留下的 ntpd.pid 也据此校验, pid 被复用时不会误判成外部 daemon(不会永久 skip)。 - S45 stop 不再按陈旧 pid 文件盲杀:只从 <锁目录>/<pid> 取持有者, 再核对 /proc/<pid>/cmdline 含 meow_ntpsync 才 kill。 P2-2 日志重定向失败导致 hwclock 根本不执行 - persist_rtc 改 ">/dev/null 2>>$LOG_FILE":hwclock 的 rc 与日志写入解耦。 - 新增 prepare_log():启动时在子 shell 里试写日志,不可写就整体退化 /dev/null。 必须用子 shell——`:` 是 POSIX 特殊内建,它的重定向失败会让 dash 直接 退出整个脚本(本单自测先踩到了,rc=2)。 P2-3 锁 mkdir→写 pid 有窗口,并发抑制非原子 - 锁的持有者改由 <锁目录>/<pid> 子目录自证(mkdir 原子),不再写 pid 文件; 读到空锁时给 3s 宽限而不是立刻判死。 - 清失效锁改用 mv 到 <锁目录>.stale.$$(rename 原子),并发清锁时只有 一个能成功,避免「抢锁失败方删掉胜方的锁」。 - 退出只删自己的锁:没有自己的 <pid> 子目录就不动锁目录。 验证 - 新增回归断言 11-16(run_tests.sh):SIGTERM 收尸/锁释放、遗留孤儿收尸、 日志不可写仍写 RTC、锁被占用不抢、死锁可接管、pid 复用不阻断。 - 可证伪性:新套件跑旧代码(c9f0cba 的 meow_ntpsync)→ 6 条 FAIL (孤儿 alive、日志不可写 rc=1 且 hwclock 未执行、抢锁并发调 ntpd×3)。 再跑「仅回退 STALE_NTPD 硬化」的变体 → pid 复用那条 FAIL (rc=0 但 RTC 未写,正是评审指出的静默失效)。 - dash 与 busybox ash 各 41/41 PASS,rc=0;dash -n / busybox sh -n 均 0 错。 - 时间断言放宽为负载容忍(宿主是多 agent 共享机,实测 sleep 5 能跑成 85s), 假 ntpd 改为挂 3600s,保证上界仍能证明是看门狗掐断而非自然退出。 fw_content.conf: meow_ntpsync md5 ad324e3f→f3c3abe4, S45meow_ntpsync md5 25454a3f→115e7592(50meow_ntpsync 未改,md5 不变)。
278 lines
11 KiB
Bash
278 lines
11 KiB
Bash
#!/bin/sh
|
||
#
|
||
# run_tests.sh — meow_ntpsync 宿主沙箱单测(M22)
|
||
#
|
||
# 不碰真机、不联网:用假的 ntpd / hwclock / 网络探测注入,
|
||
# 验证「成功 / 超时 / 失败 / 网络未就绪 / 短路 / 返回码」这些分支。
|
||
#
|
||
# 用法: sh patch_v2/ntpsync/tests/run_tests.sh [meow_ntpsync 路径]
|
||
#
|
||
set -u
|
||
|
||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||
SUT=${1:-"$HERE/../meow_ntpsync"}
|
||
W=$(mktemp -d)
|
||
trap 'rm -rf "$W"' EXIT
|
||
|
||
PASS=0
|
||
FAIL=0
|
||
|
||
ok() { PASS=$((PASS + 1)); printf ' ok %s\n' "$1"; }
|
||
bad() { FAIL=$((FAIL + 1)); printf ' FAIL %s\n' "$1"; }
|
||
check(){ if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (want=$3 got=$2)"; fi; }
|
||
|
||
# ---- 假 ntpd:行为由 MODE 决定;第一行记自己的 pid,供「trap 收尸」断言用 ----
|
||
cat >"$W/ntpd" <<'EOF'
|
||
#!/bin/sh
|
||
echo "$$" >> "$FAKE_NTPD_CALLS"
|
||
# 断言调用方必须给 -c <临时配置>
|
||
case " $* " in *" -c "*) : ;; *) echo "no -c" >&2; exit 9 ;; esac
|
||
case "$FAKE_NTPD_MODE" in
|
||
ok) echo "ntpd: time set +1.000000 s"; exit 0 ;;
|
||
fail) echo "ntpd: no server suitable" >&2; exit 3 ;;
|
||
hang) exec sleep 3600 ;;
|
||
# 不 exec:留着 cmdline 供「孤儿自证」;sleep 只挂 120s,万一漏杀也会自己退
|
||
hangkeep) sleep 120; exit 0 ;;
|
||
slew) echo "ntpd: time slew +0.000042 s"; exit 0 ;;
|
||
esac
|
||
exit 1
|
||
EOF
|
||
chmod +x "$W/ntpd"
|
||
|
||
# ---- 假 hwclock ----
|
||
cat >"$W/hwclock" <<'EOF'
|
||
#!/bin/sh
|
||
echo "$*" >> "$FAKE_HWCLOCK_CALLS"
|
||
[ "$FAKE_HWCLOCK_MODE" = fail ] && exit 1
|
||
exit 0
|
||
EOF
|
||
chmod +x "$W/hwclock"
|
||
|
||
# 每种模式都跑在一个干净的沙箱里
|
||
run_case() { # $1=名字 其余=meow_ntpsync 参数;通过环境变量控制
|
||
name=$1
|
||
shift
|
||
CALLS="$W/calls.$name"
|
||
HW="$W/hw.$name"
|
||
: >"$CALLS"
|
||
: >"$HW"
|
||
export FAKE_NTPD_CALLS="$CALLS" FAKE_HWCLOCK_CALLS="$HW"
|
||
export MEOW_NTPD="$W/ntpd" MEOW_HWCLOCK="$W/hwclock"
|
||
export MEOW_NTPSYNC_STATE="$W/state.$name" MEOW_NTPSYNC_LOCK="$W/lock.$name"
|
||
export MEOW_NTPSYNC_LOG="$W/log.$name"
|
||
export MEOW_NTPSYNC_ATTEMPT_TIMEOUT="${CASE_TIMEOUT:-2}"
|
||
}
|
||
|
||
echo "== meow_ntpsync 沙箱单测 ($SUT)"
|
||
|
||
# 1) 正常同步
|
||
rm -f "$W/state.1"
|
||
run_case 1
|
||
FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT"; rc=$?
|
||
check "同步成功返回 0" "$rc" 0
|
||
check "hwclock 被调用一次" "$(wc -l <"$W/hw.1" | tr -d ' ')" 1
|
||
check "hwclock 参数为 -w -u" "$(cat "$W/hw.1")" "-w -u"
|
||
check "写了 state(epoch)" "$(grep -c '^epoch=' "$W/state.1" 2>/dev/null || echo 0)" 1
|
||
|
||
# 1b) 单独验证 gen_conf 的内容(截获 -c 指向的文件)
|
||
cat >"$W/ntpd2" <<'EOF'
|
||
#!/bin/sh
|
||
while [ $# -gt 0 ]; do [ "$1" = "-c" ] && cp "$2" "$FAKE_CONF_DUMP"; shift; done
|
||
exit 0
|
||
EOF
|
||
chmod +x "$W/ntpd2"
|
||
rm -f "$W/state.1b"
|
||
run_case 1b
|
||
FAKE_CONF_DUMP="$W/conf.1b" MEOW_NTPD="$W/ntpd2" FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT"
|
||
check "配置每个 server 都带 iburst" \
|
||
"$(grep -c '^server .* iburst$' "$W/conf.1b" 2>/dev/null)" \
|
||
"$(grep -c '^server ' "$W/conf.1b" 2>/dev/null)"
|
||
|
||
# 2) ntpd 挂死 → 看门狗必须杀掉并返回 1(假 ntpd 挂 3600s,看门狗 2s 必须掐断)
|
||
run_case 2
|
||
t0=$(date +%s)
|
||
FAKE_NTPD_MODE=hang MEOW_NETCHECK=true sh "$SUT"; rc=$?
|
||
t1=$(date +%s); el=$((t1 - t0))
|
||
check "挂死返回 1" "$rc" 1
|
||
# 上界放宽到 90s:宿主是多 agent 共享机,实测负载高时 sleep 5 能跑成 85s;
|
||
# 假 ntpd 自己会挂 3600s,所以 90s 仍能证明是看门狗掐断而不是自然退出。
|
||
if [ "$el" -ge 2 ] && [ "$el" -le 90 ]; then ok "看门狗掐断挂死的 ntpd (实测 ${el}s)"
|
||
else bad "看门狗时长异常 (实测 ${el}s, 期望 2~90s)"; fi
|
||
check "挂死时 ntpd 子进程已死" "$(kill -0 "$(head -n 1 "$W/calls.2" 2>/dev/null)" 2>/dev/null && echo alive || echo dead)" dead
|
||
check "挂死不写 RTC" "$(wc -l <"$W/hw.2" | tr -d ' ')" 0
|
||
|
||
# 3) ntpd 明确失败 → 返回 1,不写 RTC
|
||
run_case 3
|
||
FAKE_NTPD_MODE=fail MEOW_NETCHECK=true sh "$SUT"; rc=$?
|
||
check "失败返回 1" "$rc" 1
|
||
check "失败不写 RTC" "$(wc -l <"$W/hw.3" | tr -d ' ')" 0
|
||
|
||
# 4) 单次模式网络未就绪 → 立刻返回 1,且不去打 ntpd
|
||
run_case 4
|
||
t0=$(date +%s)
|
||
FAKE_NTPD_MODE=ok MEOW_NETCHECK=false sh "$SUT"; rc=$?
|
||
t1=$(date +%s); el=$((t1 - t0))
|
||
check "网络未就绪返回 1" "$rc" 1
|
||
check "网络未就绪不调 ntpd" "$(wc -l <"$W/calls.4" | tr -d ' ')" 0
|
||
if [ "$el" -le 10 ]; then ok "单次模式不做无谓等待 (${el}s)"; else bad "单次模式等了 ${el}s"; fi
|
||
|
||
# 5) 开机模式:网络一直不来,-t 3 → 按时退出
|
||
run_case 5
|
||
t0=$(date +%s)
|
||
FAKE_NTPD_MODE=ok MEOW_NETCHECK=false sh "$SUT" -b -t 3; rc=$?
|
||
t1=$(date +%s); el=$((t1 - t0))
|
||
check "开机模式超时返回 1" "$rc" 1
|
||
if [ "$el" -ge 3 ] && [ "$el" -le 90 ]; then ok "开机模式在 -t 内退出 (${el}s)"
|
||
else bad "开机模式退出时长异常 (${el}s)"; fi
|
||
|
||
# 6) 开机模式:网络晚到 → 仍能同步成功
|
||
# 只在第 2 次探测才就绪(证明「先等、后来到了」这条路径),-t 给足
|
||
cat >"$W/latecheck" <<'EOF'
|
||
#!/bin/sh
|
||
n=$(cat "$LATE_FILE" 2>/dev/null || echo 0)
|
||
n=$((n + 1)); echo "$n" >"$LATE_FILE"
|
||
[ "$n" -gt 1 ]
|
||
EOF
|
||
chmod +x "$W/latecheck"
|
||
rm -f "$W/late.n" "$W/state.6"
|
||
run_case 6
|
||
LATE_FILE="$W/late.n" FAKE_NTPD_MODE=ok MEOW_NETCHECK="$W/latecheck" sh "$SUT" -b -t 90; rc=$?
|
||
check "网络晚到仍返回 0" "$rc" 0
|
||
|
||
# 7) 非法参数
|
||
sh "$SUT" --bogus >/dev/null 2>&1; check "未知参数返回 2" "$?" 2
|
||
sh "$SUT" -t abc >/dev/null 2>&1; check "非数字 -t 返回 2" "$?" 2
|
||
|
||
# 8) 1h 内已同步 → 短路,不调 ntpd
|
||
run_case 8
|
||
echo "epoch=$(date +%s)" >"$W/state.8"
|
||
FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT"; rc=$?
|
||
check "已同步短路返回 0" "$rc" 0
|
||
check "短路不调 ntpd" "$(wc -l <"$W/calls.8" | tr -d ' ')" 0
|
||
|
||
# 9) RTC 写失败:时间已对,返回 0,但日志要留痕
|
||
rm -f "$W/state.9"
|
||
run_case 9
|
||
FAKE_NTPD_MODE=slew FAKE_HWCLOCK_MODE=fail MEOW_NETCHECK=true sh "$SUT"; rc=$?
|
||
check "RTC 写失败仍返回 0" "$rc" 0
|
||
check "日志记录 RTC 写失败" "$(grep -c 'rtc write FAILED' "$W/log.9" 2>/dev/null)" 1
|
||
|
||
# 10) -S 打印状态
|
||
run_case 10
|
||
echo "epoch=12345" >"$W/state.10"
|
||
out=$(sh "$SUT" -S)
|
||
check "-S 打印上次记录" "$(echo "$out" | grep -c '^epoch=12345$')" 1
|
||
|
||
# ---------------------------------------------------------------- 回归(r1 P2)
|
||
# 11) P2-1:SIGTERM 打断时 trap 必须把 attempt() 拉起的 ntpd 子进程一起收尸。
|
||
# 不修的话 ntpd 会孤儿化(后面被 pidof 当成「已有 ntpd 在跑」→ 永久静默 skip)。
|
||
run_case 11
|
||
rm -rf "$W/lock.11"
|
||
# ATTEMPT_TIMEOUT 设 180s:远大于下面的观察窗口,保证「看门狗顺手杀子进程」
|
||
# 不会掩盖孤儿问题(假 ntpd 本身挂 3600s)。
|
||
FAKE_NTPD_MODE=hang MEOW_NETCHECK=true MEOW_NTPSYNC_ATTEMPT_TIMEOUT=180 \
|
||
sh "$SUT" -f >/dev/null 2>&1 &
|
||
sut=$!
|
||
i=0
|
||
while [ "$i" -lt 40 ] && [ ! -s "$W/calls.11" ]; do sleep 0.2; i=$((i + 1)); done
|
||
child=$(head -n 1 "$W/calls.11" 2>/dev/null)
|
||
kill -TERM "$sut" 2>/dev/null
|
||
# 60s 观察窗口:远早于 ATTEMPT_TIMEOUT(180s);上界放宽是因为宿主负载高
|
||
i=0
|
||
while [ "$i" -lt 60 ]; do
|
||
kill -0 "$sut" 2>/dev/null || break
|
||
sleep 1
|
||
i=$((i + 1))
|
||
done
|
||
if kill -0 "$sut" 2>/dev/null; then
|
||
bad "SIGTERM 后主进程 60s 内未退出"
|
||
kill -9 "$sut" 2>/dev/null
|
||
else
|
||
ok "SIGTERM 后主进程已退出"
|
||
fi
|
||
if [ -n "$child" ] && kill -0 "$child" 2>/dev/null; then
|
||
bad "SIGTERM 后 ntpd 子进程成孤儿 (pid=$child)"
|
||
kill -9 "$child" 2>/dev/null
|
||
else
|
||
ok "SIGTERM 后 ntpd 子进程已被收尸"
|
||
fi
|
||
if [ -d "$W/lock.11" ]; then bad "SIGTERM 后锁未释放"; else ok "SIGTERM 后锁已释放"; fi
|
||
|
||
# 12) P2-1:上次遗留的孤儿 ntpd 要被收尸,不能因 pidof 命中就静默 skip + rc=0。
|
||
# 不修的话 hwclock 根本不会被调用(旧代码直接 "skip: an ntpd is already running")。
|
||
run_case 12
|
||
rm -rf "$W/lock.12" "$W/state.12"
|
||
mkdir -p "$W/orphan"
|
||
cp "$W/ntpd" "$W/orphan/ntpd"
|
||
cat >"$W/fakepidof" <<'EOF'
|
||
#!/bin/sh
|
||
cat "$FAKE_PIDOF_OUT" 2>/dev/null
|
||
EOF
|
||
chmod +x "$W/fakepidof"
|
||
# 造一个「上次运行遗留」的 ntpd:cmdline 里带本次锁目录。
|
||
# 用 hangkeep(不 exec)——真 ntpd 是单一 ELF,cmdline 保持不变;
|
||
# 若用 exec 版,cmdline 会变成 "sleep 120",就认不出「是我们的孤儿」了。
|
||
# stdio 全丢 /dev/null:否则它(及其 sleep 子进程)会占住调用方的管道,
|
||
# 让 `run_tests.sh | tail` 永远等不到 EOF。
|
||
FAKE_NTPD_CALLS="$W/orphan.calls" FAKE_NTPD_MODE=hangkeep \
|
||
"$W/orphan/ntpd" -n -q -g -c "$W/lock.12/ntp.conf" >/dev/null 2>&1 &
|
||
orphan=$!
|
||
sleep 1
|
||
echo "$orphan" >"$W/pidof.out"
|
||
FAKE_PIDOF_OUT="$W/pidof.out" MEOW_NTPSYNC_PIDOF="$W/fakepidof" \
|
||
FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT" -f; rc=$?
|
||
check "遗留孤儿不阻断本次同步" "$rc" 0
|
||
check "遗留孤儿被收尸" "$(kill -0 "$orphan" 2>/dev/null && echo alive || echo dead)" dead
|
||
check "孤儿被收尸后仍写了 RTC" "$(wc -l <"$W/hw.12" | tr -d ' ')" 1
|
||
kill -9 "$orphan" 2>/dev/null
|
||
|
||
# 13) P2-2:日志路径不可写时 hwclock 仍必须被执行(重定向失败不得吞掉命令)。
|
||
# 不修的话 hwclock 的 marker 为空,而整轮仍返回 0 → RTC 一小时不更新。
|
||
run_case 13
|
||
rm -f "$W/state.13"
|
||
FAKE_NTPD_MODE=ok MEOW_NETCHECK=true MEOW_NTPSYNC_LOG="/nonexistent_dir_$$/x.log" \
|
||
sh "$SUT" -f; rc=$?
|
||
check "日志不可写仍返回 0" "$rc" 0
|
||
check "日志不可写 hwclock 仍被执行" "$(wc -l <"$W/hw.13" | tr -d ' ')" 1
|
||
|
||
# 14) P2-3:锁里有存活持有者(<pid> 子目录)时不得抢锁。
|
||
# 不修的话(旧代码只读 pid 文件,读不到当 other=0)会 rm -rf 抢锁并并发打 ntpd。
|
||
run_case 14
|
||
rm -rf "$W/lock.14"
|
||
mkdir -p "$W/lock.14/$$"
|
||
FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT" -f; rc=$?
|
||
check "锁被占用时返回 0" "$rc" 0
|
||
check "锁被占用时不并发调 ntpd" "$(wc -l <"$W/calls.14" | tr -d ' ')" 0
|
||
check "锁被占用时不写 RTC" "$(wc -l <"$W/hw.14" | tr -d ' ')" 0
|
||
check "未抢走别人的锁" "$([ -d "$W/lock.14/$$" ] && echo kept || echo stolen)" kept
|
||
rm -rf "$W/lock.14"
|
||
|
||
# 15) P2-3 反向:持有者已死的锁必须能被接管(否则崩溃一次就把校时永久卡死)
|
||
run_case 15
|
||
rm -rf "$W/lock.15"
|
||
mkdir -p "$W/lock.15/999999"
|
||
FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT" -f; rc=$?
|
||
check "死锁可被接管并同步" "$rc" 0
|
||
check "接管后 ntpd 被调用" "$(wc -l <"$W/calls.15" | tr -d ' ')" 1
|
||
rm -rf "$W/lock.15"
|
||
|
||
# 16) P2-1 补充:崩溃记录里的 pid 已被无关进程复用时,不能把它当成「别人的 ntpd」
|
||
# 而永久跳过。cmdline 不匹配 = 不是我们的孤儿 → 忽略它,照常校时。
|
||
# 不修的话 remaining 会收下这个 pid → "skip: a foreign ntpd" → hwclock 不执行。
|
||
run_case 16
|
||
rm -rf "$W/lock.16" "$W/state.16"
|
||
mkdir -p "$W/lock.16/999999" # 持有者已死
|
||
sleep 300 & # 无关进程,占住一个将被「复用」的 pid
|
||
unrelated=$!
|
||
echo "$unrelated" >"$W/lock.16/ntpd.pid"
|
||
FAKE_NTPD_MODE=ok MEOW_NETCHECK=true MEOW_NTPSYNC_PIDOF=false sh "$SUT" -f; rc=$?
|
||
check "pid 复用不阻断同步" "$rc" 0
|
||
check "pid 复用后仍写了 RTC" "$(wc -l <"$W/hw.16" | tr -d ' ')" 1
|
||
check "无关进程未被误杀" "$(kill -0 "$unrelated" 2>/dev/null && echo alive || echo killed)" alive
|
||
kill -9 "$unrelated" 2>/dev/null
|
||
rm -rf "$W/lock.16"
|
||
|
||
echo
|
||
echo "== 结果: PASS=$PASS FAIL=$FAIL"
|
||
[ "$FAIL" -eq 0 ] || exit 1
|