Files
MeowOS-For-R3P2/patch_v2/ntpsync/meow_ntpsync
T
AskaEth c9f0cbaab6 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 内成功。
2026-11-06 12:49:13 +08:00

314 lines
9.3 KiB
Bash
Executable File
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
#
# 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