#!/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 <>"$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" >"$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