feat(ntp): M22 网络校时 — 复用固件自带 ntpd + hwclock 回写 RTC

真机实测(0.8.47,2026-11-06)定位到的根因不是「没有 NTP 客户端」:
固件自带 ntp-4.2.8p14(/usr/sbin/ntpd)和 /etc/ntp.conf(4 个 pool.ntp.org),
但没有任何 init 脚本/进程启动它 —— 有客户端、没启动。

实测证据:
- RTC 存在且可读(/dev/rtc0,10003000.rtc),内核开机 hctosys=1,
  冷启动不回到 1970 → 掉电保持;RTC 存 UTC(since_epoch == date +%s)。
- 但 RTC 会走慢且无人纠正:实测系统时间比 NTP 慢 49 s;把系统时间人为
  拨错 4 h 后观察 65 s 无人纠正,RTC 也不跟随系统时间。
- UDP/123 出网正常(ntpd -q 一次成功 time set +48.877s);DNS/HTTP 正常。
- udhcpc 的 default.script 不处理 option 42,实测热点也不下发。
- ntpd -q 不会回写 RTC(步进后 sys-rtc 仍差 49 s)→ 必须 hwclock -w -u。
- ntpd 没有超时参数,无服务器时永久存活 → 必须自带看门狗。
- -p 形式的 server 首个 poll 要 ~64 s;配置里加 iburst 后 8 s 内返回。

实现(新增,不改任何基线文件):
- /usr/bin/meow_ntpsync:一次性校时器。复用 ntpd -q -g + 运行时生成的
  iburst 配置;内置 IP 兜底(阿里/腾讯/Cloudflare)→ DNS 名兜底;
  单次 15 s 看门狗、默认路由轮询、1 h 短路、进程锁、失败静默。
  成功严格校时后 hwclock -w -u 回写 RTC(RTC 存 UTC,必须 -u)。
- /etc/init.d/S45meow_ntpsync:开机只 setsid 后台拉起并立即返回
  (关键路径 +0 秒;后台最长约 150 s 自行退出),失败不阻断 init。
- /usr/share/udhcpc/default.script.d/50meow_ntpsync:只在 bound/renew
  触发(设备上唯一可靠的「网络就绪」事件),后台拉起后立即 exit 0;
  有 $ntpsrv(option 42)就优先用。基线 default.script 本就有 HOOK_DIR
  循环,目录原本为空。
- tests/run_tests.sh:宿主沙箱单测(假 ntpd/hwclock/网络探测)。

验证:dash 与 busybox sh 各 sh -n 通过;单测两套 shell 各 23/23 通过;
真机(/tmp 下运行,未装系统目录)首跑 8 s rc=0、ntpd slew 成功、
RTC 由 1793940395 写成 1793940452(49 s 偏差被纠正)、hwclock -u -r 吻合;
二次运行 0 s 短路;-b -t 3 无网络 5 s 退出 rc=1;
DHCP 灌入不可达服务器 192.0.2.x 仍靠 IP 兜底在 ~10 s 内成功。
This commit is contained in:
AskaEth committed 2026-11-06 12:49:13 +08:00
1 parent 2cfac55b4e
commit c9f0cbaab6
6 files changed
+560 -1

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
#
# 50meow_ntpsync — udhcpc 钩子(M22)
#
# /usr/share/udhcpc/default.script 结尾会执行 default.script.d/*,
# 参数是本轮 DHCP 事件(deconfig|bound|renew|leasefail|nak)。
# 本钩子只在拿到租约(bound/renew)时动作——这是设备上唯一可靠的
# 「网络真的就绪」事件(S40network 阶段 WiFi 还没起来)。
#
# 关键:**绝不能阻塞**。default.script 是 udhcpc 同步调用的,
# 这里把校时丢后台后立即 exit 0,否则会拖住 DHCP 脚本。
#
# $ntpsrv = DHCP option 42(NTP 服务器列表)。
# 实测 2026-11-06 热点不下发(租约环境变量里没有 ntpsrv),企业网可能有;
# 有就优先用,没有就走 /usr/bin/meow_ntpsync 里的内置 IP 兜底。
#
# 重复触发由 meow_ntpsync 自己的 1 h 短路 + 锁兜住:续租每 30 min 一次,
# 最多 1 h 真同步一次,开销可忽略。
#
case "$1" in
bound|renew) ;;
*) exit 0 ;;
esac
[ -x /usr/bin/meow_ntpsync ] || exit 0
if [ -n "$ntpsrv" ]; then
setsid /usr/bin/meow_ntpsync -s "$ntpsrv" >/dev/null 2>&1 &
else
setsid /usr/bin/meow_ntpsync >/dev/null 2>&1 &
fi
exit 0
+40
View File
@@ -0,0 +1,40 @@
#!/bin/sh
#
# S45meow_ntpsync — 开机网络校时(M22)
#
# 只负责把同步器丢到后台并**立即返回**,开机关键路径增加 0 秒。
# 真正的「等网络就绪 + 有限重试 + 总超时」全在 /usr/bin/meow_ntpsync 里。
#
# 为什么排 S45:S40network 只 ifup 了 lo,WiFi 由 S43 之后才起来,租约更晚,
# 所以本脚本启动时大概率还没有网络——同步器自己轮询默认路由(最多 -t 秒)。
# WiFi 在更晚(用户手动连)才就绪的情况,由 udhcpc 钩子
# /usr/share/udhcpc/default.script.d/50meow_ntpsync 在拿到租约时再触发一次。
#
# 失败不阻断 init:所有分支都 exit 0(除非参数错误)。
#
case "$1" in
start)
printf "Starting meow_ntpsync: "
[ -x /usr/bin/meow_ntpsync ] || { echo "SKIP (not installed)"; exit 0; }
setsid /usr/bin/meow_ntpsync -b -t 120 >/dev/null 2>&1 &
echo "OK"
;;
stop)
printf "Stopping meow_ntpsync: "
if [ -f /tmp/meow_ntpsync.lock/pid ]; then
kill "$(cat /tmp/meow_ntpsync.lock/pid 2>/dev/null)" 2>/dev/null
fi
rm -rf /tmp/meow_ntpsync.lock 2>/dev/null
echo "OK"
;;
restart|reload)
"$0" stop
"$0" start
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
exit 0
+313
View File
@@ -0,0 +1,313 @@
#!/bin/sh
#
# meow_ntpsync — 网络校时(一次性,不常驻)M22
#
# ---- 为什么需要它(2026-11-06 固件 0.8.47 实测)----
# * 设备有 RTC:/dev/rtc0(驱动 10003000.rtc),内核开机 hctosys=1,
# 即系统时间来自 RTC;RTC 掉电保持(冷启动后不是 1970)。
# * 但 RTC 走时偏慢且**没有任何东西纠正**:实测系统时间比 NTP 慢 49 s,
# 把系统时间人为拨错 4 h 后观察 65 s,无人纠正,RTC 也不受系统时间影响。
# * 固件其实自带 ntp-4.2.8p14(/usr/sbin/ntpd)和 /etc/ntp.conf
# (4 个 pool.ntp.org),但**没有任何 init 脚本/进程启动它**
# → 根因是「有客户端、没启动」,不是「没有 NTP 客户端」。
# * udhcpc 的 default.script 不处理 option 42,实测热点也不下发。
#
# ---- 三条路线的取舍 ----
# ① 自写轻量 SNTP:要新增/交叉编译一个二进制,且 UDP 123 收发、超时、
# 时间戳换算都要自己写;收益与 ntpd 相同 → 放弃。
# ② 复用固件自带 ntpd(本脚本):零新增依赖、零体积增长,协议实现成熟,
# -g 允许开机大步进跳时。唯一坑是 ntpd 没有超时参数(无响应会一直等),
# 所以本脚本自带看门狗 kill -9。
# ③ 依赖 DHCP option 42:实测热点不下发,且 udhcpc 的 default.script
# 根本不读 ntpsrv → 不能作为唯一来源;但企业网可能有,
# 所以 dhcp 钩子把它当「网络就绪事件 + 可选服务器源」,而非唯一依赖。
#
# ---- 设计约束 ----
# * 不阻塞开机:调用方(S45 / udhcpc 钩子)一律后台拉起本脚本后立即返回。
# * 严格有界:单次 ntpd 上限 ATTEMPT_TIMEOUT 秒,-b 模式总时长受 -t 限制,
# 进程生命周期最长 ≈ -t + 2×ATTEMPT_TIMEOUT,之后自杀。
# * 网络未就绪不硬等:先轮询默认路由,出现才尝试校时。
# * 失败静默:只写 /var/log(tmpfs,不磨 NAND),不占 CPU、不留常驻进程。
# * 成功必须回写 RTC(hwclock -w -u),否则下次开机电又错了。
#
# 用法:
# meow_ntpsync -b [-t 秒] [-s "服务器 ..."] 开机模式:等默认路由 + 重试
# meow_ntpsync [-s "服务器 ..."] 立即试一次(udhcpc bound/renew)
# meow_ntpsync -S 打印上次同步记录
# meow_ntpsync -f ... 忽略「1 h 内已同步」短路
#
# 退出码: 0 = 时间可信(本次同步成功,或 1 h 内已同步过);1 = 失败/超时;2 = 用法错误
#
# 以下变量可由环境覆盖——仅供宿主单测注入,设备上不设置,走真实路径:
# MEOW_NTPD / MEOW_HWCLOCK / MEOW_NETCHECK / MEOW_NTPSYNC_ATTEMPT_TIMEOUT
# MEOW_NTPSYNC_LOG / MEOW_NTPSYNC_STATE / MEOW_NTPSYNC_LOCK
NTPD="${MEOW_NTPD:-/usr/sbin/ntpd}"
HWCLOCK="${MEOW_HWCLOCK:-/sbin/hwclock}"
NETCHECK="${MEOW_NETCHECK:-}"
LOG_FILE="${MEOW_NTPSYNC_LOG:-/var/log/meow_ntpsync.log}"
STATE_FILE="${MEOW_NTPSYNC_STATE:-/tmp/meow_ntpsync.state}"
LOCK_DIR="${MEOW_NTPSYNC_LOCK:-/tmp/meow_ntpsync.lock}"
# 内置 IP 兜底(不依赖 DNS,国内可达):阿里 / 腾讯 / Cloudflare
FALLBACK_IP="203.107.6.88 120.25.115.20 162.159.200.1"
# DNS 名兜底:内置 IP 全废(被墙/下线)时最后再试一次
FALLBACK_DNS="0.pool.ntp.org 1.pool.ntp.org"
SYNC_TTL=3600 # 1 h 内已同步过就短路
ATTEMPT_TIMEOUT="${MEOW_NTPSYNC_ATTEMPT_TIMEOUT:-15}" # 单次 ntpd 上限(实测 iburst 8 s 内回)
BOOT_TOTAL=120 # -b 模式总时长上限
usage() {
cat <<EOF
用法: meow_ntpsync [-b] [-t 秒] [-s "服务器 ..."] [-f] [-q]
meow_ntpsync -S
-b 开机模式:先等默认路由出现,再校时,总时长受 -t 限制(默认 ${BOOT_TOTAL}s)
-t 开机模式总时长(秒)
-s DHCP option 42 下发的 NTP 服务器(空格分隔),优先于内置 IP
-f 强制同步,忽略「1 h 内已同步」短路
-q 不写日志
-S 打印上次同步记录
EOF
}
log() {
[ -n "$QUIET" ] && return 0
printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$LOG_FILE" 2>/dev/null
return 0
}
# 网络就绪 = 存在默认路由。不 ping、不解析 DNS,只查内核路由表(零网络开销)。
net_ready() {
if [ -n "$NETCHECK" ]; then
$NETCHECK >/dev/null 2>&1
return $?
fi
if [ -r /proc/net/route ]; then
awk 'NR>1 && $2=="00000000" {found=1; exit} END{exit !found}' \
/proc/net/route 2>/dev/null && return 0
fi
if command -v ip >/dev/null 2>&1; then
ip route 2>/dev/null | grep -q '^default ' && return 0
fi
return 1
}
synced_recently() {
[ -f "$STATE_FILE" ] || return 1
s_epoch=$(sed -n 's/^epoch=//p' "$STATE_FILE" 2>/dev/null | head -n 1)
case "$s_epoch" in
''|*[!0-9]*) return 1 ;;
esac
now=$(date +%s)
age=$((now - s_epoch))
# 时钟被往回拨时 age<0,视为未同步
[ "$age" -ge 0 ] && [ "$age" -lt "$SYNC_TTL" ]
}
lock_acquire() {
if mkdir "$LOCK_DIR" 2>/dev/null; then
chmod 700 "$LOCK_DIR" 2>/dev/null
echo $$ >"$LOCK_DIR/pid" 2>/dev/null
return 0
fi
other=$(cat "$LOCK_DIR/pid" 2>/dev/null)
case "$other" in
''|*[!0-9]*) other=0 ;;
esac
if [ "$other" -gt 0 ] && kill -0 "$other" 2>/dev/null; then
return 1
fi
# 锁的持有者已经不在了 → 清掉重来
rm -rf "$LOCK_DIR" 2>/dev/null
mkdir "$LOCK_DIR" 2>/dev/null || return 1
chmod 700 "$LOCK_DIR" 2>/dev/null
echo $$ >"$LOCK_DIR/pid" 2>/dev/null
return 0
}
clock_sane() {
y=$(date +%Y 2>/dev/null)
case "$y" in
''|*[!0-9]*) return 1 ;;
esac
[ "$y" -ge 2020 ] && [ "$y" -le 2100 ]
}
record_success() {
now=$(date +%s)
{
echo "epoch=$now"
echo "date=$(date '+%Y-%m-%d %H:%M:%S')"
echo "servers=$1"
} >"$STATE_FILE" 2>/dev/null
}
# RTC 存的是 UTC(实测 /sys/class/rtc/rtc0/since_epoch == date +%s),必须 -u
persist_rtc() {
if "$HWCLOCK" -w -u >>"$LOG_FILE" 2>&1; then
log "rtc written (hwclock -w -u)"
return 0
fi
log "rtc write FAILED (hwclock -w -u)"
return 1
}
# 生成 ntpd 临时配置。iburst 是必须的:实测不加 iburst 时首个 poll 要等 ~64 s,
# -q 根本来不及取到时间(加 iburst 后 8 s 内返回)。
gen_conf() {
for s in $1; do
echo "server $s iburst"
done >"$LOCK_DIR/ntp.conf" 2>/dev/null
}
# 单次尝试。ntpd 无响应时不会自己退出(实测无服务器时永久存活),
# 所以用轮询 + kill -9 兜底;成功与否只看退出码。
attempt() {
servers="$1"
[ -n "$servers" ] || return 1
gen_conf "$servers" || return 1
"$NTPD" -n -q -g -c "$LOCK_DIR/ntp.conf" </dev/null >>"$LOG_FILE" 2>&1 &
pid=$!
waited=0
while [ "$waited" -lt "$ATTEMPT_TIMEOUT" ]; do
kill -0 "$pid" 2>/dev/null || break
sleep 1
waited=$((waited + 1))
done
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
log "attempt timeout after ${waited}s (servers: $servers)"
return 1
fi
wait "$pid" 2>/dev/null
rc=$?
if [ "$rc" -ne 0 ]; then
log "attempt failed rc=$rc (servers: $servers)"
return 1
fi
return 0
}
trim_log() {
[ -f "$LOG_FILE" ] || return 0
sz=$(wc -c <"$LOG_FILE" 2>/dev/null)
case "$sz" in
''|*[!0-9]*) return 0 ;;
esac
# /var/log 是 tmpfs,只防无限增长;保留最后 200 行
[ "$sz" -gt 65536 ] && tail -n 200 "$LOG_FILE" >"$LOG_FILE.tmp" 2>/dev/null &&
mv -f "$LOG_FILE.tmp" "$LOG_FILE" 2>/dev/null
return 0
}
# 一轮 = 先打「DHCP 服务器 + 内置 IP」(不需要 DNS),再打 DNS 名
sync_round() {
servers="$EXTRA $FALLBACK_IP"
trim_log
if attempt "$servers"; then
if clock_sane; then
record_success "$servers"
log "sync OK (servers: $servers)"
persist_rtc
return 0
fi
log "sync reported ok but clock still insane, ignore"
fi
if attempt "$FALLBACK_DNS"; then
if clock_sane; then
record_success "$FALLBACK_DNS"
log "sync OK (dns: $FALLBACK_DNS)"
persist_rtc
return 0
fi
log "dns sync reported ok but clock still insane, ignore"
fi
return 1
}
MODE=single
TOTAL=$BOOT_TOTAL
EXTRA=""
FORCE=0
QUIET=""
while [ $# -gt 0 ]; do
case "$1" in
-b) MODE=boot ;;
-t) TOTAL="$2"; shift ;;
-s) EXTRA="$2"; shift ;;
-f) FORCE=1 ;;
-q) QUIET=1 ;;
-S) MODE=status ;;
-h|--help) usage; exit 0 ;;
*)
echo "meow_ntpsync: unknown option: $1" >&2
usage >&2
exit 2
;;
esac
shift
done
case "$TOTAL" in
''|*[!0-9]*)
echo "meow_ntpsync: -t 必须是整数秒" >&2
exit 2
;;
esac
if [ "$MODE" = status ]; then
if [ -f "$STATE_FILE" ]; then
cat "$STATE_FILE"
else
echo "never synced"
fi
exit 0
fi
if [ "$FORCE" -eq 0 ] && synced_recently; then
log "skip: synced within ${SYNC_TTL}s"
exit 0
fi
if ! lock_acquire; then
log "skip: another meow_ntpsync is running"
exit 0
fi
trap 'rm -rf "$LOCK_DIR" 2>/dev/null' EXIT INT TERM HUP
# 已经有常驻 ntpd 在跑(用户/第三方装的)→ 它会自己校时,别抢 123 端口
if pidof ntpd >/dev/null 2>&1; then
log "skip: an ntpd is already running"
exit 0
fi
if [ "$MODE" = boot ]; then
deadline=$(( $(date +%s) + TOTAL ))
while :; do
if net_ready; then
log "network ready, syncing"
sync_round && exit 0
fi
[ "$(date +%s)" -ge "$deadline" ] && break
sleep 5
done
log "give up: no sync within ${TOTAL}s"
exit 1
fi
# 单次模式(udhcpc 钩子):此刻租约刚拿到,路由已装好,直接试
if ! net_ready; then
log "single: network not ready, skip"
exit 1
fi
sync_round && exit 0
exit 1
+162
View File
@@ -0,0 +1,162 @@
#!/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 决定 ----
cat >"$W/ntpd" <<'EOF'
#!/bin/sh
echo "invoked $(date +%s)" >> "$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) sleep 30; 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
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
if [ "$el" -ge 2 ] && [ "$el" -le 6 ]; then ok "看门狗在 2~6s 内掐断 (实测 ${el}s)"
else bad "看门狗时长异常 (实测 ${el}s, 期望 2~6s)"; fi
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 2 ]; 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 8 ]; then ok "开机模式在 -t 内退出 (${el}s)"
else bad "开机模式退出时长异常 (${el}s)"; fi
# 6) 开机模式:网络晚到 → 仍能同步成功
cat >"$W/latecheck" <<'EOF'
#!/bin/sh
n=$(cat "$LATE_FILE" 2>/dev/null || echo 0)
n=$((n + 1)); echo "$n" >"$LATE_FILE"
[ "$n" -gt 2 ]
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 20; 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
echo
echo "== 结果: PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ] || exit 1