From c9f0cbaab64d5ea2c23c85245ea461787af82df4 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 12:49:13 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(ntp):=20M22=20=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E6=A0=A1=E6=97=B6=20=E2=80=94=20=E5=A4=8D=E7=94=A8=E5=9B=BA?= =?UTF-8?q?=E4=BB=B6=E8=87=AA=E5=B8=A6=20ntpd=20+=20hwclock=20=E5=9B=9E?= =?UTF-8?q?=E5=86=99=20RTC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真机实测(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 内成功。 --- patch_v2/ntpsync/50meow_ntpsync | 33 +++ patch_v2/ntpsync/S45meow_ntpsync | 40 ++++ patch_v2/ntpsync/meow_ntpsync | 313 ++++++++++++++++++++++++++++ patch_v2/ntpsync/tests/run_tests.sh | 162 ++++++++++++++ scripts/build/README.md | 2 +- scripts/build/fw_content.conf | 11 + 6 files changed, 560 insertions(+), 1 deletion(-) create mode 100755 patch_v2/ntpsync/50meow_ntpsync create mode 100755 patch_v2/ntpsync/S45meow_ntpsync create mode 100755 patch_v2/ntpsync/meow_ntpsync create mode 100644 patch_v2/ntpsync/tests/run_tests.sh diff --git a/patch_v2/ntpsync/50meow_ntpsync b/patch_v2/ntpsync/50meow_ntpsync new file mode 100755 index 0000000..fbf8d93 --- /dev/null +++ b/patch_v2/ntpsync/50meow_ntpsync @@ -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 diff --git a/patch_v2/ntpsync/S45meow_ntpsync b/patch_v2/ntpsync/S45meow_ntpsync new file mode 100755 index 0000000..5f23f2e --- /dev/null +++ b/patch_v2/ntpsync/S45meow_ntpsync @@ -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 diff --git a/patch_v2/ntpsync/meow_ntpsync b/patch_v2/ntpsync/meow_ntpsync new file mode 100755 index 0000000..210c01b --- /dev/null +++ b/patch_v2/ntpsync/meow_ntpsync @@ -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 <>"$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 diff --git a/patch_v2/ntpsync/tests/run_tests.sh b/patch_v2/ntpsync/tests/run_tests.sh new file mode 100644 index 0000000..d654b32 --- /dev/null +++ b/patch_v2/ntpsync/tests/run_tests.sh @@ -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 diff --git a/scripts/build/README.md b/scripts/build/README.md index abeae2f..91a73d1 100644 --- a/scripts/build/README.md +++ b/scripts/build/README.md @@ -66,7 +66,7 @@ bash scripts/build/build_fw_apps.sh -v 0.7.68 -o r3proii_m2_73.upt -b fw_ssh/squ 规则: - `#` 开头为注释,空行忽略。 -- 目标目录自动创建;`usr/bin` / `etc/init.d` / `etc/profile.d` 一律 `chmod 755`。 +- 目标目录自动创建;**每条**条目装入后一律 `chmod 755`(`usr/bin` / `etc/init.d` / `etc/profile.d` 之外同理,例如 udhcpc 钩子 `usr/share/udhcpc/default.script.d/*` 必须可执行才会被调用)。 - 多重配置并存:复制 `fw_content.conf` 改名,用 `-c` 指定,互不干扰。 ### 覆盖基线已有文件(清单 `` 与基线同名) diff --git a/scripts/build/fw_content.conf b/scripts/build/fw_content.conf index 3c66acc..b6b1532 100644 --- a/scripts/build/fw_content.conf +++ b/scripts/build/fw_content.conf @@ -44,6 +44,7 @@ patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_powermgr|1ef2f8afc07acc7aa09d patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_power|1ef2f8afc07acc7aa09dee9f7149aba8|meow_powermgr 的 CLI 形态(同二进制双形态,argv[0]=meow_power 即 CLI:status/estimate/model/history/top/events,policy 期3桩) patch_v2/meow_netmgrd/meow_netmgrd|/usr/bin/meow_netmgrd|20243ea1a800b29badefebb395603aaf|网络管理服务期6 M6.28(蓝牙P1 BT_CTL权威收口:hcitool con判连接/paired-devices补名/a2dp详情/connect-disconnect-pair-unpair-scan) patch_v2/meow_netmgrd/meow_netmgrd|/usr/bin/meow_net|20243ea1a800b29badefebb395603aaf|meow_netmgrd 的 CLI 形态(同二进制双形态,argv[0]=meow_net 即 CLI:status/iface/wifi/bt/rfkill/apps/conns/top/dns/policy/help;M6.28 蓝牙P1 BT_CTL收口) +patch_v2/ntpsync/meow_ntpsync|/usr/bin/meow_ntpsync|ad324e3ffffa7ab156fcd9d646981d33|网络校时 M22(复用固件自带 ntp-4.2.8p14 一次性 -q 校时 + hwclock -w -u 回写 RTC;自带看门狗/总超时/默认路由轮询,不自常驻、不阻塞调用方) # ---- 开机 init 脚本(etc/init.d,数字决定启动顺序,按文件名自然序)---- # M21 开机亮度链(顺序依赖:S11 < S21mount_ubifs < S22): @@ -54,6 +55,10 @@ patch_v2/meow_netmgrd/meow_netmgrd|/usr/bin/meow_net|20243ea1a800b29badefebb3956 # S11 是**覆盖基线同名文件**(基线已有,md5 249220f2…);S22 是**新增**文件。 patch_v2/bootmenu/S11jpeg_display_shell|/etc/init.d/S11jpeg_display_shell|3ad33301fb9f0884102a47e17a1985ed|覆盖基线 S11(基线 md5 249220f2…):背光写入从「画完 logo 后写 60」改为「画 logo 前写 20(只降不升;cur=0/空 照写,仅 1..20 的有效值跳过)」,删掉写死 60;用户亮度交给 S22 patch_v2/bootmenu/S22brightness|/etc/init.d/S22brightness|1a0aa33d6f806f3f1648168cc1d8b953|新增:S21mount_ubifs 之后按 /usr/data/meow/settings.conf 的 display.brightness 恢复背光(clamp [brightness_min, max_brightness];读不到不改、失败不阻断 init) +# M22 网络校时:S45 排在 S40network/S43wifi 之后。S40 只 ifup 了 lo,WiFi 租约更晚, +# 所以 S45 只把校时器丢后台(开机关键路径 +0 秒),由 /usr/bin/meow_ntpsync 自己轮询 +# 默认路由(-t 120)+ 有限重试;WiFi 更晚才连上的情况由 udhcpc 钩子再触发一次。 +patch_v2/ntpsync/S45meow_ntpsync|/etc/init.d/S45meow_ntpsync|25454a3febf112c2cbedcc0a87fcc3d8|新增:开机校时启动器(后台 setsid 拉起 meow_ntpsync -b -t 120,立即返回;失败/未安装不阻断 init) patch_v2/meow_sysui/S70meow_sysui|/etc/init.d/S70meow_sysui|e435affcb56499e5229728c6ae2524b9|sysui 自启 M2.85(launcher.conf enabled=1+迁移) patch_v2/meow_svcd/S89meow_svcd|/etc/init.d/S89meow_svcd|a1ef9d658d37bb7ed012cc9326840ac4|svcd 自启 M6.2(+amsd.conf 热更 exec→/usr/bin 迁移) patch_v2/meow_amsd/meow_amsd|/usr/bin/meow_amsd|b698f176210b07f760f874ea2bb10a14|活动管理器 M6.46 编入 M7 杀后台修复(am kill 与后台页按钮同通道:MSG_AMS_KILL 先打后台保活表走 meow_bg_terminate,无前台会话也能杀;cmd_kill/cmd_start 失败原因细分 connect failed/timeout,no reply;响应串保留 launcher offline 供 MeowPanel 503 映射);继承 M6.35(注册表自愈+busy 识别+显示名 UTF-8 整序列 name[49];批① FG/BG 收编:handle_bg/handle_fg+AMS_FG/BG 状态供批③后台管理页) @@ -62,5 +67,11 @@ patch_v2/bootmenu/S90meow_bootmenu|/etc/init.d/S90meow_bootmenu|e0ace111ce5ee446 patch_v2/bootmenu/S92_03_start_music_player|/etc/init.d/S92_03_start_music_player|c17257aa3d93f75da72903b57cbe1034|§4.3 兜底 patch(rcS 正常路径被 S90 阻塞不执行) patch_v2/bootmenu/S95shutdown_logo|/etc/init.d/S95shutdown_logo|96db7b844d7c26f44835d4681824f578|rcK 关机时显示 /etc/shutdown_logo.jpeg +# ---- udhcpc 钩子(usr/share/udhcpc/default.script.d,default.script 末尾自动执行)---- +# M22:baseline 的 default.script 已有 HOOK_DIR 循环(第 99-103 行),目录原本为空。 +# 钩子只在 bound/renew(拿到租约)时动作——这是设备上唯一可靠的「网络真的就绪」事件; +# 投递后台后立即 exit 0,绝不阻塞 DHCP 脚本。$ntpsrv(DHCP option 42)有就优先用。 +patch_v2/ntpsync/50meow_ntpsync|/usr/share/udhcpc/default.script.d/50meow_ntpsync|4774c08194a2e0232960927252f252f2|新增:udhcpc 租约钩子(bound/renew 时后台触发校时;deconfig/leasefail/nak 直接返回) + # ---- SSH 会话工具 PATH(etc/profile.d)---- patch_v2/meow_launcherd/meow_card_path.sh|/etc/profile.d/meow_card_path.sh|ac1963c75190611f14a2d46d217e640a|SSH 会话 PATH(M2.12 起):指向 TF 卡 MeowOS/opt/bin From 1f692ae2a7b152cc3350b568a54217a2ed2985ac Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 13:34:45 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(ntp):=20M22=20r1=20=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=20P2=C3=973=20=E2=80=94=20=E5=AD=A4=E5=84=BF=20ntpd=20?= =?UTF-8?q?=E6=94=B6=E5=B0=B8=20/=20=E6=97=A5=E5=BF=97=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E4=B8=8D=E5=90=9E=20hwclock=20/=20=E9=94=81=E8=87=AA=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//cmdline 是否含本锁目录来区分「我们自己遗留的孤儿」(收尸) 与「别人的常驻 ntpd」(让路)。崩溃后留下的 ntpd.pid 也据此校验, pid 被复用时不会误判成外部 daemon(不会永久 skip)。 - S45 stop 不再按陈旧 pid 文件盲杀:只从 <锁目录>/ 取持有者, 再核对 /proc//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 有窗口,并发抑制非原子 - 锁的持有者改由 <锁目录>/ 子目录自证(mkdir 原子),不再写 pid 文件; 读到空锁时给 3s 宽限而不是立刻判死。 - 清失效锁改用 mv 到 <锁目录>.stale.$$(rename 原子),并发清锁时只有 一个能成功,避免「抢锁失败方删掉胜方的锁」。 - 退出只删自己的锁:没有自己的 子目录就不动锁目录。 验证 - 新增回归断言 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 不变)。 --- patch_v2/ntpsync/S45meow_ntpsync | 17 ++- patch_v2/ntpsync/meow_ntpsync | 185 ++++++++++++++++++++++++---- patch_v2/ntpsync/tests/run_tests.sh | 135 ++++++++++++++++++-- scripts/build/fw_content.conf | 4 +- 4 files changed, 300 insertions(+), 41 deletions(-) diff --git a/patch_v2/ntpsync/S45meow_ntpsync b/patch_v2/ntpsync/S45meow_ntpsync index 5f23f2e..307567c 100755 --- a/patch_v2/ntpsync/S45meow_ntpsync +++ b/patch_v2/ntpsync/S45meow_ntpsync @@ -21,10 +21,19 @@ case "$1" in ;; 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 + # 持锁者自证在 <锁目录>/ 子目录里;kill 前再核对 cmdline, + # 防止 pid 复用误杀无关进程(P2-1 顺带项) + for d in /tmp/meow_ntpsync.lock/*; do + [ -d "$d" ] || continue + pid=${d##*/} + case "$pid" in + ''|*[!0-9]*) continue ;; + esac + cmd=$(tr '\0' ' ' <"/proc/$pid/cmdline" 2>/dev/null) + case "$cmd" in + *meow_ntpsync*) kill "$pid" 2>/dev/null ;; + esac + done echo "OK" ;; restart|reload) diff --git a/patch_v2/ntpsync/meow_ntpsync b/patch_v2/ntpsync/meow_ntpsync index 210c01b..ecc5453 100755 --- a/patch_v2/ntpsync/meow_ntpsync +++ b/patch_v2/ntpsync/meow_ntpsync @@ -40,7 +40,7 @@ # # 以下变量可由环境覆盖——仅供宿主单测注入,设备上不设置,走真实路径: # MEOW_NTPD / MEOW_HWCLOCK / MEOW_NETCHECK / MEOW_NTPSYNC_ATTEMPT_TIMEOUT -# MEOW_NTPSYNC_LOG / MEOW_NTPSYNC_STATE / MEOW_NTPSYNC_LOCK +# MEOW_NTPSYNC_LOG / MEOW_NTPSYNC_STATE / MEOW_NTPSYNC_LOCK / MEOW_NTPSYNC_PIDOF NTPD="${MEOW_NTPD:-/usr/sbin/ntpd}" HWCLOCK="${MEOW_HWCLOCK:-/sbin/hwclock}" @@ -56,7 +56,8 @@ FALLBACK_IP="203.107.6.88 120.25.115.20 162.159.200.1" 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 内回) +ATTEMPT_TIMEOUT="${MEOW_NTPSYNC_ATTEMPT_TIMEOUT:-15}" # 单次 ntpd 上限(实测 iburst 8 s 内返回) +NTPD_PIDOF="${MEOW_NTPSYNC_PIDOF:-pidof ntpd}" # 找 ntpd 进程(单测可注入) BOOT_TOTAL=120 # -b 模式总时长上限 usage() { @@ -106,27 +107,143 @@ synced_recently() { [ "$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 +LOCK_HELD="" +STALE_NTPD="" +NTPD_CHILD="" + +# 锁的持有者由「锁目录下的 子目录」自证:mkdir 原子,且进程还在就必能 kill -0。 +# 不用 pid 文件——写文件有窗口,读到空 pid 会被误判成「持有者已死」。 +lock_owner() { + for d in "$LOCK_DIR"/*; do + [ -d "$d" ] || continue + echo "${d##*/}" 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 + done + return 1 +} + +lock_take() { mkdir "$LOCK_DIR" 2>/dev/null || return 1 chmod 700 "$LOCK_DIR" 2>/dev/null - echo $$ >"$LOCK_DIR/pid" 2>/dev/null + mkdir "$LOCK_DIR/$$" 2>/dev/null + LOCK_HELD=1 return 0 } +lock_acquire() { + if [ -d "$LOCK_DIR" ]; then + # 对方可能刚 mkdir、还没建 子目录:给 3 s 宽限,期间不抢 + i=0 + other="" + while [ "$i" -lt 3 ]; do + other=$(lock_owner) && break + sleep 1 + i=$((i + 1)) + done + case "$other" in + ''|*[!0-9]*) other=0 ;; + esac + if [ "$other" -gt 0 ] && kill -0 "$other" 2>/dev/null; then + return 1 + fi + # 持有者确实不在了(或宽限期满仍空)→ 用它遗留的 ntpd 子进程记录收尸 + STALE_NTPD=$(cat "$LOCK_DIR/ntpd.pid" 2>/dev/null) + # rename 是原子的:并发清锁时只有一个能 mv 成功,避免「失败方删掉胜方的锁」 + if mv "$LOCK_DIR" "$LOCK_DIR.stale.$$" 2>/dev/null; then + [ -z "$STALE_NTPD" ] && + STALE_NTPD=$(cat "$LOCK_DIR.stale.$$/ntpd.pid" 2>/dev/null) + rm -rf "$LOCK_DIR.stale.$$" 2>/dev/null + fi + fi + lock_take +} + +# 只删自己建立的锁:没有自己的 子目录就说明锁已不是我们的,别动 +lock_release() { + [ "$LOCK_HELD" = 1 ] || return 0 + [ -d "$LOCK_DIR/$$" ] && rm -rf "$LOCK_DIR" 2>/dev/null + LOCK_HELD="" + return 0 +} + +# ntpd 子进程是否由本脚本拉起:cmdline 里的 -c 指向我们的锁目录 +ntpd_is_ours() { + cmd=$(tr '\0' ' ' <"/proc/$1/cmdline" 2>/dev/null) + case "$cmd" in + *"$LOCK_DIR"*) return 0 ;; + esac + return 1 +} + +reap_pid() { + log "reaping stale ntpd child pid=$1" + kill "$1" 2>/dev/null + sleep 1 + kill -9 "$1" 2>/dev/null +} + +# 只对「别人的常驻 ntpd」让路;自己遗留的孤儿必须收尸, +# 否则孤儿会被 pidof 认成「已有 ntpd 在跑」→ 之后每次都静默 skip + rc=0。 +reap_or_skip_ntpd() { + # 先按崩溃时留下的 ntpd.pid 收尸。只认 cmdline 自证:pid 可能已被复用, + # 复用后的进程「不是我们的」并不等于「它是别人的 ntpd」,此时必须忽略它, + # 否则又会退化成永久 skip。 + case "$STALE_NTPD" in + ''|*[!0-9]*) ;; + *) + if kill -0 "$STALE_NTPD" 2>/dev/null && ntpd_is_ours "$STALE_NTPD"; then + reap_pid "$STALE_NTPD" + fi + ;; + esac + + remaining="" + seen=" " + for p in $($NTPD_PIDOF 2>/dev/null); do + case "$p" in + ''|*[!0-9]*) continue ;; + esac + case "$seen" in + *" $p "*) continue ;; + esac + seen="$seen$p " + kill -0 "$p" 2>/dev/null || continue + if ntpd_is_ours "$p"; then + reap_pid "$p" + else + remaining="$remaining $p" + fi + done + if [ -n "$remaining" ]; then + log "skip: a foreign ntpd is already running:$remaining" + exit 0 + fi + return 0 +} + +# 退出清理:先收尸自己的 ntpd 子进程,再只删自己的锁。 +# 顺序很重要——子进程在锁目录里登记了 ntpd.pid,锁删早了就丢线索。 +cleanup() { + if [ -n "$NTPD_CHILD" ] && kill -0 "$NTPD_CHILD" 2>/dev/null; then + kill "$NTPD_CHILD" 2>/dev/null + n=0 + while [ "$n" -lt 3 ] && kill -0 "$NTPD_CHILD" 2>/dev/null; do + sleep 1 + n=$((n + 1)) + done + kill -9 "$NTPD_CHILD" 2>/dev/null + wait "$NTPD_CHILD" 2>/dev/null + fi + NTPD_CHILD="" + lock_release +} + +# 被信号打断时:清理后必须 exit,否则 ash 执行完 trap 会继续往下跑 +on_signal() { + cleanup + exit 143 +} + clock_sane() { y=$(date +%Y 2>/dev/null) case "$y" in @@ -144,9 +261,19 @@ record_success() { } >"$STATE_FILE" 2>/dev/null } +# 日志目录不可写时整体退化为 /dev/null。 +# 关键:重定向失败会让**命令本身不执行**,所以不能让「日志写不进去」 +# 连带把 hwclock / ntpd 也一起废掉(P2-2)。 +# 又:`:` 是 POSIX 特殊内建,它的重定向失败会让 dash 直接退出整个脚本, +# 所以试写必须在子 shell 里做,把失败局限在子 shell 内。 +prepare_log() { + ( : >>"$LOG_FILE" ) 2>/dev/null || LOG_FILE=/dev/null +} + # RTC 存的是 UTC(实测 /sys/class/rtc/rtc0/since_epoch == date +%s),必须 -u persist_rtc() { - if "$HWCLOCK" -w -u >>"$LOG_FILE" 2>&1; then + # hwclock 的 rc 只反映「RTC 写没写成」,与日志能否写入解耦 + if "$HWCLOCK" -w -u >/dev/null 2>>"$LOG_FILE"; then log "rtc written (hwclock -w -u)" return 0 fi @@ -164,13 +291,19 @@ gen_conf() { # 单次尝试。ntpd 无响应时不会自己退出(实测无服务器时永久存活), # 所以用轮询 + kill -9 兜底;成功与否只看退出码。 +# +# NTPD_CHILD 是全局的:本脚本被 SIGTERM/SIGINT 打断时,trap 靠它把子进程杀掉。 +# 否则 ntpd 会孤儿化(ppid 已死),此后 pidof 把它当成「已有 ntpd 在跑」。 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=$! + NTPD_CHILD=$! + # 落盘一份,供崩溃(SIGKILL/掉电)后下一次运行收尸 + echo "$NTPD_CHILD" >"$LOCK_DIR/ntpd.pid" 2>/dev/null + pid=$NTPD_CHILD waited=0 while [ "$waited" -lt "$ATTEMPT_TIMEOUT" ]; do kill -0 "$pid" 2>/dev/null || break @@ -181,11 +314,13 @@ attempt() { if kill -0 "$pid" 2>/dev/null; then kill -9 "$pid" 2>/dev/null wait "$pid" 2>/dev/null + NTPD_CHILD="" log "attempt timeout after ${waited}s (servers: $servers)" return 1 fi wait "$pid" 2>/dev/null rc=$? + NTPD_CHILD="" if [ "$rc" -ne 0 ]; then log "attempt failed rc=$rc (servers: $servers)" return 1 @@ -273,6 +408,8 @@ if [ "$MODE" = status ]; then exit 0 fi +prepare_log + if [ "$FORCE" -eq 0 ] && synced_recently; then log "skip: synced within ${SYNC_TTL}s" exit 0 @@ -282,13 +419,11 @@ 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 +trap 'cleanup' EXIT +trap 'on_signal' INT TERM HUP -# 已经有常驻 ntpd 在跑(用户/第三方装的)→ 它会自己校时,别抢 123 端口 -if pidof ntpd >/dev/null 2>&1; then - log "skip: an ntpd is already running" - exit 0 -fi +# 让路给「别人的常驻 ntpd」;自己遗留的孤儿先收尸(P2-1) +reap_or_skip_ntpd if [ "$MODE" = boot ]; then deadline=$(( $(date +%s) + TOTAL )) diff --git a/patch_v2/ntpsync/tests/run_tests.sh b/patch_v2/ntpsync/tests/run_tests.sh index d654b32..6ffcdf2 100644 --- a/patch_v2/ntpsync/tests/run_tests.sh +++ b/patch_v2/ntpsync/tests/run_tests.sh @@ -21,16 +21,18 @@ 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 决定 ---- +# ---- 假 ntpd:行为由 MODE 决定;第一行记自己的 pid,供「trap 收尸」断言用 ---- cat >"$W/ntpd" <<'EOF' #!/bin/sh -echo "invoked $(date +%s)" >> "$FAKE_NTPD_CALLS" +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) sleep 30; exit 0 ;; + 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 @@ -86,14 +88,17 @@ 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 +# 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 -if [ "$el" -ge 2 ] && [ "$el" -le 6 ]; then ok "看门狗在 2~6s 内掐断 (实测 ${el}s)" -else bad "看门狗时长异常 (实测 ${el}s, 期望 2~6s)"; fi +# 上界放宽到 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 @@ -109,7 +114,7 @@ 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 +if [ "$el" -le 10 ]; then ok "单次模式不做无谓等待 (${el}s)"; else bad "单次模式等了 ${el}s"; fi # 5) 开机模式:网络一直不来,-t 3 → 按时退出 run_case 5 @@ -117,20 +122,21 @@ 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)" +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 2 ] +[ "$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 20; rc=$? +LATE_FILE="$W/late.n" FAKE_NTPD_MODE=ok MEOW_NETCHECK="$W/latecheck" sh "$SUT" -b -t 90; rc=$? check "网络晚到仍返回 0" "$rc" 0 # 7) 非法参数 @@ -157,6 +163,115 @@ 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 文件,读不到当 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 diff --git a/scripts/build/fw_content.conf b/scripts/build/fw_content.conf index b6b1532..be0a897 100644 --- a/scripts/build/fw_content.conf +++ b/scripts/build/fw_content.conf @@ -44,7 +44,7 @@ patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_powermgr|1ef2f8afc07acc7aa09d patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_power|1ef2f8afc07acc7aa09dee9f7149aba8|meow_powermgr 的 CLI 形态(同二进制双形态,argv[0]=meow_power 即 CLI:status/estimate/model/history/top/events,policy 期3桩) patch_v2/meow_netmgrd/meow_netmgrd|/usr/bin/meow_netmgrd|20243ea1a800b29badefebb395603aaf|网络管理服务期6 M6.28(蓝牙P1 BT_CTL权威收口:hcitool con判连接/paired-devices补名/a2dp详情/connect-disconnect-pair-unpair-scan) patch_v2/meow_netmgrd/meow_netmgrd|/usr/bin/meow_net|20243ea1a800b29badefebb395603aaf|meow_netmgrd 的 CLI 形态(同二进制双形态,argv[0]=meow_net 即 CLI:status/iface/wifi/bt/rfkill/apps/conns/top/dns/policy/help;M6.28 蓝牙P1 BT_CTL收口) -patch_v2/ntpsync/meow_ntpsync|/usr/bin/meow_ntpsync|ad324e3ffffa7ab156fcd9d646981d33|网络校时 M22(复用固件自带 ntp-4.2.8p14 一次性 -q 校时 + hwclock -w -u 回写 RTC;自带看门狗/总超时/默认路由轮询,不自常驻、不阻塞调用方) +patch_v2/ntpsync/meow_ntpsync|/usr/bin/meow_ntpsync|f3c3abe4a960560f59f4101e6c444722|网络校时 M22(复用固件自带 ntp-4.2.8p14 一次性 -q 校时 + hwclock -w -u 回写 RTC;自带看门狗/总超时/默认路由轮询/进程锁,不自常驻、不阻塞调用方;r1 修:trap 收尸 ntpd 子进程+孤儿收尸、日志不可写不再吞掉 hwclock、锁用 子目录自证且只删自己的锁) # ---- 开机 init 脚本(etc/init.d,数字决定启动顺序,按文件名自然序)---- # M21 开机亮度链(顺序依赖:S11 < S21mount_ubifs < S22): @@ -58,7 +58,7 @@ patch_v2/bootmenu/S22brightness|/etc/init.d/S22brightness|1a0aa33d6f806f3f164816 # M22 网络校时:S45 排在 S40network/S43wifi 之后。S40 只 ifup 了 lo,WiFi 租约更晚, # 所以 S45 只把校时器丢后台(开机关键路径 +0 秒),由 /usr/bin/meow_ntpsync 自己轮询 # 默认路由(-t 120)+ 有限重试;WiFi 更晚才连上的情况由 udhcpc 钩子再触发一次。 -patch_v2/ntpsync/S45meow_ntpsync|/etc/init.d/S45meow_ntpsync|25454a3febf112c2cbedcc0a87fcc3d8|新增:开机校时启动器(后台 setsid 拉起 meow_ntpsync -b -t 120,立即返回;失败/未安装不阻断 init) +patch_v2/ntpsync/S45meow_ntpsync|/etc/init.d/S45meow_ntpsync|115e759274c8100391e3ede05a0578dc|新增:开机校时启动器(后台 setsid 拉起 meow_ntpsync -b -t 120,立即返回;失败/未安装不阻断 init;stop 只按 <锁目录>/ 自证 + cmdline 核对后 kill,避免陈旧 pid 误杀) patch_v2/meow_sysui/S70meow_sysui|/etc/init.d/S70meow_sysui|e435affcb56499e5229728c6ae2524b9|sysui 自启 M2.85(launcher.conf enabled=1+迁移) patch_v2/meow_svcd/S89meow_svcd|/etc/init.d/S89meow_svcd|a1ef9d658d37bb7ed012cc9326840ac4|svcd 自启 M6.2(+amsd.conf 热更 exec→/usr/bin 迁移) patch_v2/meow_amsd/meow_amsd|/usr/bin/meow_amsd|b698f176210b07f760f874ea2bb10a14|活动管理器 M6.46 编入 M7 杀后台修复(am kill 与后台页按钮同通道:MSG_AMS_KILL 先打后台保活表走 meow_bg_terminate,无前台会话也能杀;cmd_kill/cmd_start 失败原因细分 connect failed/timeout,no reply;响应串保留 launcher offline 供 MeowPanel 503 映射);继承 M6.35(注册表自愈+busy 识别+显示名 UTF-8 整序列 name[49];批① FG/BG 收编:handle_bg/handle_fg+AMS_FG/BG 状态供批③后台管理页)