Files
MeowOS-For-R3P2/patch_v2/ntpsync/tests/run_tests.sh
T
AskaEth 1f692ae2a7 fix(ntp): M22 r1 评审 P2×3 — 孤儿 ntpd 收尸 / 日志失败不吞 hwclock / 锁自证
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 不变)。
2026-11-06 13:34:45 +08:00

278 lines
11 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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