diff --git a/SYSTEM_SETTINGS.md b/SYSTEM_SETTINGS.md index 8cfa0ea..1a9e3bf 100644 --- a/SYSTEM_SETTINGS.md +++ b/SYSTEM_SETTINGS.md @@ -57,6 +57,7 @@ | | `audio.output` | string | `hw:0,0` | MeowAPPs 音频输出设备 | | **网络** | `network.wifi_enabled` | bool | true | WiFi 开关(写 wlan0;设置页开关经 `wifi_up.sh`/`ip link set wlan0`,`ensure_wifi()` 尊重此值——关=0 则不再自动连网) | | | `network.bluetooth_enabled` | bool | false | 蓝牙开关(写 `hciconfig hci0 up/down`;默认关=少耗电) | +| **系统** | `system.hostname` | string | `meowos` | 主机名(RFC1035 首标签;bootmenu 的 S89meow_svcd 开机读它 `sethostname`,缺省 `meowos`;供自研 mDNS 应答器 `meow_mdns` 公布 `.local`)[源:M2.54] | > 新增设置项时:在清单加一行 + 存储 schema + API 常量/枚举 + 实体 UI 控件 + web 面板控件(四端同步)。 diff --git a/TF_CARD_LAYOUT.md b/TF_CARD_LAYOUT.md index 9a1d394..006dd0c 100644 --- a/TF_CARD_LAYOUT.md +++ b/TF_CARD_LAYOUT.md @@ -23,7 +23,7 @@ │ └── setting.bak # 设置导出默认路径(SYSTEM_SETTINGS 一致) ├── fonts// # 多字体库(每套一个子目录;font.file 选默认套) ├── logs/ # 统一日志:系统 meow_.log + 应用 apps//logs/_.log - ├── services.d/ # 服务配置(meow_svcd 的 .conf) + ├── services.d/ # 服务配置(meow_svcd 的 .conf;TF 源——与板载 /usr/data/meow/services.d 双源,同名板载优先)`[meow_svcd.md]` ├── opt/ # 外围可选工具(curl/socat/tmux/jq/rsync/nano/htop/ncdu/file/xxd…) ├── lib/ # 可选共享库(opt 工具的依赖 .so) ├── tools/ # 调试/运维(可不设,并入 opt) @@ -33,6 +33,7 @@ ## 2. 固件 rootfs 只保留(稳定框架) - `meow_launcherd`(MeowOS 核心)、`meow_bootmenu`(引导选单)、`meow_watchdog` +- `meow_svcd`(服务管理器,M2.54 进固件)+ `meow_mdns`(自研 IPv4-only mDNS 应答器,M2.54 进固件 `/usr/bin`) - `dropbear`/`dropbearkey`(SSH 必需)、`sftp-server`(小,保留) - 调试三件套:`gdbserver`/`strace`/`tcpdump` + `sqlite3`(DB 调试) - 系统依赖库(libc/libstdc++/libcrypto/glib/asound 等——播放器/服务需要,不能动) diff --git a/docs/MEOWOS_SPEC_REF.md b/docs/MEOWOS_SPEC_REF.md index 7879ca4..bb73160 100644 --- a/docs/MEOWOS_SPEC_REF.md +++ b/docs/MEOWOS_SPEC_REF.md @@ -8,6 +8,8 @@ ### 1.1 板载(NAND rootfs / /data) - 设置持久化:**板载** `/usr/data/meow/settings.conf`(UBIFS,小文件 <10KB,跨换卡保留)`[SYSTEM_SETTINGS §2][MEOWOS_API §3]` +- **板载服务目录**:`/usr/data/meow/services.d/.conf`(M2.54,Data 区,恒定存在、权威优先;首启由 `S89meow_svcd` 预置 `mdnsd.conf`)。与 TF `MeowOS/services.d/` 双源,同名板载优先 + WARN;板载 `trigger=boot` 服务不依赖卡(拔卡保留)`[meow_svcd.md][meow_svcd.c]` +- **主机名**:设置键 `system.hostname`(SET_STR,默认 `meowos`;RFC1035 首标签),`S89meow_svcd` 开机读 settings `sethostname`;供自研 mDNS 应答器公布 `.local` `[SYSTEM_SETTINGS §1][mips-repo/meow_mdns]` - `/etc/meowos_version`:版本唯一来源(launcher 运行时 `load_meowos_version` 读;`MEOWOS_VERSION` 宏仅作 dev 占位兜底)`[DEV_NOTES #4]` - `/tmp`:tmpfs,开机清空;开机模式标记 `/tmp/meow_boot_mode`、电源菜单 `/tmp/meow_power_menu{,_active}`、app 沉浸 `/tmp/meowapp_state` `[BOOT_SELECTOR §1][KEY_CONTROL §9][APPS §3.5]` - 日志启动缓冲 `/tmp/meow_boot.log`(256KB 滚动,卡出现后自动迁卡)`[APPS §4.5]` @@ -23,6 +25,7 @@ ├── MeowOpt/ # 板载外部工具/二进制(如 mdnsd/,卡上加载,拔卡降级不崩)`[DEV_NOTES][meow_svcd.md]` └── MeowOS/ # MeowOS 专属(一切非必要文件) ├── backups/settings/setting.bak # 设置导出默认路径 + ├── services.d/ # 服务配置(meow_svcd 的 .conf;TF 源,与板载 /usr/data/meow/services.d 双源,同名板载优先)`[meow_svcd.md]` ├── fonts// # 多字体库(每套一个子目录,见 §5.2) │ ├── font.png # 预渲染灰度 PNG(bit depth 4|8,gen/gen_font.py 产物) │ ├── font.idx # 索引(MFNI v2,gen/gen_font.py 产物) @@ -39,6 +42,7 @@ - 板载只放**稳定框架+KB 级设置**;日志/字库/媒体等大件一律 TF 卡 `[TF_CARD_LAYOUT §0][SYSTEM_SETTINGS §2]` - 拔卡:launcher/bootmenu/SSH 不死;应用列表"未插卡"、日志回退内存环、字体回退点阵、opt 不可用——均优雅降级 `[TF_CARD_LAYOUT §3]` - 关键配置优先板载(无卡系统能跑);转储/日志可降级 → 卡,无卡 /tmp 回退 `[DEV_NOTES #7]` +- **服务配置双源(M2.54)**:板载 `/usr/data/meow/services.d/` + TF `MeowOS/services.d/`,板载优先;板载服务(`trigger=boot`)随 Data 区恒定存在,拔卡不丢(mDNS 应答器托管于此)`[meow_svcd.md]` - rootfs 体积上限:**45,088,768 B(≈45MB)**,回验 0 diff `[BOOT_SELECTOR §1][APPS §6]` --- @@ -120,13 +124,14 @@ - `meow_media_*`(媒体会话)——**依赖阶段 D 落地,未定** `[WORK_STATE]` - 崩溃捕获/看门狗——**规划中未定** `[WORK_STATE SDK 缺失清单 #5]` - `meow_notify_dbus`(org.freedesktop.Notifications 桥)——**已定,待实现** `[FRAMEWORK_DESIGN 附录 C]` -- **meow_svcd 服务管理器**(`patch_v2/meow_svcd/`,mipsel 静态 ~99KB 进固件,不入卡)`[meow_svcd.md]`: - - 类 systemd/systemctl,由 supervisor(meow_launcherd/bootmenu)`fork/exec` 托管;**接入 supervisor 启动链为后续**,接入用 `-f` 前台便于 `waitpid`。 - - 读 TF 卡 **`MeowOS/services.d/.conf`**(目录式 `key=value`,未知键忽略/前向兼容):`exec`(必填)/`args`/`env`/`enabled`(0=永不自动启动,默认0)/`restart`(`always`/`on-fail`/`never`,默认on-fail)/`backoff_ms`(默认1000)/`max_restart`(默认5)/`cap`(本阶段仅解析不实装)/`trigger`(`boot`/`card-mount`/`wifi-up`,默认card-mount)。 +- **meow_svcd 服务管理器**(`patch_v2/meow_svcd/`,mipsel 静态 ~101KB 进固件,不入卡)`[meow_svcd.md]`: + - 类 systemd/systemctl。**已接入系统启动链(M2.54)**:固件 `etc/init.d/S89meow_svcd` 开机自启(`-f` 前台 + 重挂循环,失败不死;置于 S84ssh 之后、S90meow_bootmenu 之前——因 S90 exec 引导选单阻塞 rcS)。由 bootmenu/launcher `fork/exec` + `waitpid` 托管仍为备选。 + - **双源 services.d**(板载优先):板载 `/usr/data/meow/services.d/.conf` + TF **`MeowOS/services.d/.conf`**,两处都读;同名服务板载优先(TF 同名跳过 + WARN「 板载与 TF 都存在,采用板载」)。目录式 `key=value`,未知键忽略/前向兼容:`exec`(必填)/`args`/`env`/`enabled`(0=永不自动启动,默认0)/`restart`(`always`/`on-fail`/`never`,默认on-fail)/`backoff_ms`(默认1000)/`max_restart`(默认5)/`cap`(本阶段仅解析不实装)/`trigger`(`boot`/`card-mount`/`wifi-up`,默认card-mount)。 + - **板载-trigger=boot 语义**:板载服务恒定存在,boot 触发不依赖卡(拔卡保留);TF 服务 boot 仍需卡。 - 崩溃隔离:服务=svcd **独立子进程**(`fork+execv`),svcd 只 `waitpid`/`SIGCHLD` 感知;服务死不影响 svcd/supervisor;重启受 `backoff_ms` 退避防热循环。 - 状态机:`DISABLED/STOPPED/RUNNING/STOPPING/FAILED`;稳定运行 30s 清零 restart_count。CLI:`status/reload/start|stop|restart /pid`(AF_UNIX 单行文本 `/tmp/meow_svcd.sock`)。 - **卡上外部工具 = `MeowOpt/`**(TF 根下平级目录,如 `MeowOpt/mdnsd/mdnsd`):MeowOS 启动时从卡加载,拔卡降级不崩 `[DEV_NOTES][meow_svcd.md]`。 -- **mDNS 方案A(已拍板)**:用**完整 `mdnsd`**(994KB, mipsel 静态) 放 TF `MeowOpt/mdnsd/mdnsd`,由 meow_svcd 托管(需 root / setcap `cap_net_bind_service,cap_net_raw`);**SAResponder**(885KB) 因"单服务注册型"(只回单服务查询,无法按名访问设备)被排除。⚠️ 手机热点组播常在客户端间丢失 → mDNS 有风险,主机 DNS 兜底 `[DEV_NOTES 检查点]`。 +- **mDNS 应答器 = `meow_mdns`(自研,已拍板替代完整 mdnsd)**:`mips-repo/meow_mdns/`,**IPv4-only**(内核无 IPv6,完整 mDNSResponder/SAResponder 均 `socket(AF_INET6)` 失败),mipsel 静态 ~79KB(md5 b23de7ce)。**进固件 `/usr/bin/meow_mdns`**(不入卡),由 meow_svcd 托管(板载 `/usr/data/meow/services.d/mdnsd.conf`,`trigger=boot`、root 即可绑 5353/组播,`cap=` 空)。功能:join `224.0.0.251:5353`,应答 `.local` A(wlan0 IPv4)/`_ssh._tcp` PTR/SRV/TXT/`_services._dns-sd` PTR;支持 `MEOW_MDNS_HOST`/`_ADDR`/`_PORT`/`_TTL`/`_CF`/`_QD`/`_MCAST`/`_MIF`/`_DBG`。⚠️ **手机热点隔离**:设备→主机组播被热点丢弃,avahi 只收组播应答 → 该热点下 `avahi-resolve .local` 超时(网络层限制,非应答器缺陷),主机需 DNS 兜底 `[mips-repo/meow_mdns/manifest.md][meow_svcd.md]`。 - `meow_settings_*`(设置后端,已定)/`meowcli`/`meow_sys_devattr`/`meow_devattr_set` `[MEOWOS_API]` - 后台权限:`MSG_HELLO/MSG_REGISTER.flags` 扩展 `MF_BACKGROUND`/`MF_PLAYBACK`;`PERF_BACKGROUND`/`PERF_AUDIO_BACKGROUND`(规划)`[FRAMEWORK_DESIGN 附录 A]` @@ -164,6 +169,7 @@ - 刷新率:`display.refresh_rate` enum 60/30/15(默认 60);`display.refresh_always_max` bool false(关=允许 60→30→15 降档)。governor:连续 12 帧超预算降一档,连续 4 批≈48 帧远低预算升一档 `[SYSTEM_SETTINGS §1][PERF_SCHEDULER][MIGRATION_AC §12.1]` - 息屏:`display.screen_off_timeout` int 秒 默认 **30**(0=常亮);`display.screen_off_mode` enum `panel`(面板下电+背光0+FBIOPBLANK)/`black`(背光0全黑+双击亮屏) `[SYSTEM_SETTINGS §1]` - 亮度 clamp:`display.brightness` 0-255 默认 101(clamp ≥ `display.brightness_min`);`display.brightness_min` 默认 **1**(M2.39 由 25 改 1)——**与旧文档 25 冲突见 §A** `[SYSTEM_SETTINGS §1]` +- **主机名(M2.54)**:`system.hostname`(SET_STR 默认 `meowos`,RFC1035 首标签);`S89meow_svcd` 开机读板载 `settings.conf` 应用 `sethostname`,缺省 `meowos`/系统名;供 `meow_mdns` 公布 `.local`。settings 无该键 → 默认 `meowos`。`[SYSTEM_SETTINGS §1][mips-repo/meow_mdns/manifest.md]` ### 5.5 MeowAPPs 包结构 / .mpk / 数据 - `/` = 程序主体+app.json+资源;`MeowData//{data,cache}` = 数据;首次启动自动建 data/cache 并经 env 注入,exFAT 无软链 `[APPS 附录 §1/§2]` @@ -288,7 +294,7 @@ - OPEN_QUESTIONS.md 待定 ❓:#1 背景图加载时机/缓存、#2 非 480×720 图如何处(拉伸/裁剪/回退)、#3 叠加层颜色联动、#4 背景色板具体枚举、#5 浅色主题文字对比度、#7 首期是否做应用图标、#8 通知来源(**最大空白**,M2 已定为 系统+IPC+D-Bus 三路)、#9 下拉手势 vs 左滑返回区分、#10 通知存储上限/FIFO、#11 媒体活跃会话判定、#12 多 app 音频混音/焦点、#13 web 实时画面 JPEG/WS、#14 远程输入注入仲裁、#16 主页左滑=侧边栏 vs 返回 `[OPEN_QUESTIONS]` - DEVELOPMENT_PLAN:阶段 0(bootmenu 双系统)**待实现**(M0-M4);阶段 B/E/F;M2-M6 阶段 D 里程碑未做 `[DEVELOPMENT_PLAN]` - 未定/待拍板:`meow_config_*`/`meow_media_*`/崩溃捕获(见 §4);MeowOS 音频输出设置(只读还是可配);OTA 依赖 MeowPanel `[WORK_STATE][OPEN_QUESTIONS #17/#18]` -- **meow_svcd 待办**:接入 supervisor 启动链(`-f` 前台 + waitpid,改固件有风险需另确认);`cap` setcap 真机实装(cap_net_bind_service,cap_net_raw);写 `MeowOS/services.d/mdnsd.conf` 落地 mDNS。**services.d/svcd 尚未接入 supervisor,勿当作已跑起来。** `[WORK_STATE][meow_svcd.md]` +- **meow_svcd 后续(M2.54 已落地大部分)**:~~接入 supervisor 启动链~~(已改 `etc/init.d/S89meow_svcd` 开机自启;bootmenu/launcher `fork+exec`+`waitpid` 仍为备选);~~写 `MeowOS/services.d/mdnsd.conf` 落地 mDNS~~(已改**板载** `/usr/data/meow/services.d/mdnsd.conf` 预置,`trigger=boot`)。剩余:`cap` setcap 真机实装(cap_net_bind_service,cap_net_raw);`wifi-up` 判定收紧。**services.d/svcd 已接入开机链(S89)**,但真机行为待验证。 `[WORK_STATE][meow_svcd.md]` --- diff --git a/mips-repo/README.md b/mips-repo/README.md index ae054c1..c6b2c88 100644 --- a/mips-repo/README.md +++ b/mips-repo/README.md @@ -18,7 +18,8 @@ mips-repo/ - **mdnsd/**:Apple mDNSResponder-2881.0.25 交叉编译产物(`mipsel-linux-muslsf` 静态)。两种变体同在 `bin/`: - `bin/mdnsd` = **SAResponder**(mDNSResponderPosix,单服务注册型,`-n/-f` 只发布/回指定服务)strip 后 **885.6 KB**; - `bin/mdnsd.bin` = **完整 mdnsd** 守护进程(通用宿主响应器,回任意 `.local` 查询)strip 后 **994 KB**。 - 均远超固件 <200KB 阈值 → 按 mDNS 方案A 用**完整版**部署到 TF `MeowOpt/mdnsd/mdnsd`,由 meow_svcd 托管(需 root / setcap `cap_net_bind_service,cap_net_raw`)。详见 `mdnsd/manifest.md`。 + ⚠️ 实测(内核无 IPv6)**两者均失败**:`socket AF_INET6: Address family not supported by protocol`;且即便 `make HAVE_IPV6=0` 编译通过、daemon 能跑,**也不发布主机名 A 记录**(IPC 服务型守护进程,等客户端注册)。故 mDNS 方案A 判为**不达标**,由 `meow_mdns/` 替代(见下)。 +- **meow_mdns/**:自研极简 IPv4-only mDNS 应答器(~77.6 KB,`mipsel-linux-muslsf` 静态)。join `224.0.0.251:5353`,应答 `.local` A + `_ssh._tcp.local` PTR/SRV/TXT;零 IPv6 依赖。部署到 TF `MeowOS/opt/mdnsd/mdnsd`(95 KB < 阈值,可考虑进固件)。详见 `meow_mdns/manifest.md`。⚠️ 手机热点**丢弃客户端间组播**→ 设备能应答、主机收不到(avahi 只认组播应答),`avahi-resolve *.local` 仍超时(网络层热点隔离,非应答器缺陷),需主机 DNS 兜底。 ## 目标架构与工具链 - 架构:`mipsel`(MIPS32r2, little-endian) diff --git a/mips-repo/meow_mdns/bin/meow_mdns b/mips-repo/meow_mdns/bin/meow_mdns new file mode 100755 index 0000000..02bbf36 Binary files /dev/null and b/mips-repo/meow_mdns/bin/meow_mdns differ diff --git a/mips-repo/meow_mdns/build.sh b/mips-repo/meow_mdns/build.sh new file mode 100644 index 0000000..2d3017d --- /dev/null +++ b/mips-repo/meow_mdns/build.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# +# build.sh — 自研极简 IPv4-only mDNS 应答器 meow_mdns(mipsel-linux-muslsf,静态) +# +# 背景: 完整 mDNSResponder / SAResponder 两者均需 IPv6(socket AF_INET6 在内核无 IPv6 时 fatal), +# 且完整版 mdnsd 是 IPC 服务型守护进程, 自身不公布主机名 A 记录。 +# 故自研本应答器: join 224.0.0.251:5353, 应答 .local A + _ssh._tcp.local PTR/SRV/TXT。 +# +# 目标架构 : mipsel (MIPS32r2 little-endian), musl, 静态 +# 交叉编译器: ~/Project/R3ProII/.tools/zig-mipsel-cc +# 产物 : bin/meow_mdns +# +# 用法 : ./build.sh # 构建 mipsel 静态(部署版) +# 宿主单测 gcc -O2 -o meow_mdns meow_mdns.c && MEOW_MDNS_PORT=55353 MEOW_MDNS_HOST=ingenic MEOW_MDNS_ADDR=127.0.0.1 ./meow_mdns -v +set -euo pipefail + +DIR="$(cd "$(dirname "$0")" && pwd)" +ZIGCC="${ZIGCC:-$HOME/Project/R3ProII/.tools/zig-mipsel-cc}" + +echo "==> [1/2] 交叉编译 meow_mdns (zig: mipsel-linux-musleabi, -Os -static)" +"$ZIGCC" -Os -static -o "$DIR/meow_mdns.tmp" "$DIR/meow_mdns.c" + +echo "==> [2/2] 放置产物 $DIR/bin/meow_mdns" +mkdir -p "$DIR/bin" +install -m 755 "$DIR/meow_mdns.tmp" "$DIR/bin/meow_mdns" +rm -f "$DIR/meow_mdns.tmp" + +file "$DIR/bin/meow_mdns" +echo "字节数: $(stat -c %s "$DIR/bin/meow_mdns")" +echo "MD5 : $(md5sum "$DIR/bin/meow_mdns" | cut -d' ' -f1)" +echo "构建完成: $DIR/bin/meow_mdns" diff --git a/mips-repo/meow_mdns/manifest.md b/mips-repo/meow_mdns/manifest.md new file mode 100644 index 0000000..fc41bc2 --- /dev/null +++ b/mips-repo/meow_mdns/manifest.md @@ -0,0 +1,69 @@ +# meow_mdns — 自研极简 IPv4-only mDNS 应答器(mipsel-linux-muslsf 静态) + +> 目的:让 R3 Pro II / MeowOS 设备在**纯 IPv4**(内核无 IPv6)下应答 `*.local`, +> 使主机 `avahi-resolve .local` / `avahi-browse` 能识别设备。 +> 产出为自研 C 代码,零 IPv6 依赖,覆盖路径 A(mDNSResponder HAVE_IPV6=0)无法满足的功能缺口。 + +## 为什么不用 mDNSResponder / SAResponder +| 原因 | 说明 | +|---|---| +| 需 IPv6 | 两者都在 `socket(AF_INET6)` 处失败:`Address family not supported by protocol`。mDNSResponder 无 `NOT_HAVE_IPV6` 宏(0 处),IPv6 是运行时硬编码;即便 `make HAVE_IPV6=0` 编译通过(见下“路径 A 试验”),仍不解决下一条 | +| 不公布主机名 A 记录 | 完整版 `mdnsd` 是 **IPC 服务型守护进程**:它等客户端经 UDS(`/var/run/mdnsd`) 注册名字,自身**不自动登记主机名**。`-debug` 日志证实它只注册接口 wlan0 + `_dns-sd` 浏览记录,从无 `ingenic.local A`。应答记录需客户端调用 `DNSServiceRegister`,没有现成客户端 | +| SAResponder 单服务 | 只用 `mDNS_RegisterService` 注册**单一服务**,不注册主机 A 记录,`ingenic.local` 无法按名解析 | + +## 路径 A 试验结论(编译层面 MVP,但功能不达标) +- 源码无 `NOT_HAVE_IPV6`;开关是 Makefile Linux 分支的 `-DHAVE_IPV6`(第 113 行)+ 第 196-200 行 `HAVE_IPV6=0/1` 可覆盖。 +- `make os=linux tls=no HAVE_IPV6=0 CC=zig-mipsel-cc CFLAGS="-Os -static -Wno-date-time" LD=... LINKOPTS="-s" Daemon` **能编译出 1015792B 的 mdnsd**(md5 `7ee68d626d718d0572d4087c5a584d7f`), + `SetupSocket`/接口枚举/unicast v6 socket 均被 `#if HAVE_IPV6` 移除;仅硬件地址那段 `socket(AF_INET6)` 被 `if(sockfd>=0)` 守护(无 IPv6 内核返回 -1,无害)。 +- 该 mdnsd 在设备上**可运行**(绑 0.0.0.0:5353 + 加入 224.0.0.251),但**不发布主机名**(见上),故不满足 `ingenic.local` 解析。→ 转路径 B。 + +## 功能 +- join IPv4 组播 `224.0.0.251:5353`(`IP_ADD_MEMBERSHIP`/`SO_REUSEADDR`/`SO_REUSEPORT`)。 +- 应答(QNAME 大小写不敏感): + - `.local` **A** → 设备 wlan0 IPv4。 + - `_ssh._tcp.local` **PTR** → `._ssh._tcp.local`。 + - `._ssh._tcp.local` **SRV**(port 22,target `.local`)+ **TXT**(空)。 + - `_services._dns-sd._udp.local` **PTR** → `_ssh._tcp.local`(便于 avahi-browse)。 + - 支持 QTYPE=ANY(255)。 +- RFC 1035 名压缩(owner 名若已在响应中出现则回写 `0xC0` 指针)。 +- IP_PKTINFO 判断查询到组播还是单播:**组播查询→组播应答 + 单播冗余回源**;单播查询→单播回源。 +- 线程/信号安全;SIGINT/SIGTERM 优雅退出(留组并关 socket)。 + +## 环境覆写 +| 变量 | 默认 | 说明 | +|---|---|---| +| `MEOW_MDNS_HOST` | `gethostname()` 首标签 | 公布的主机名(设备上=内核 hostname `meowos`,`/etc/hostname` 残留 `ingenic`) | +| `MEOW_MDNS_ADDR` | 首块非回环 UP 接口 IPv4 | 应答的 A 记录 IP(设备=wlan0 10.109.49.50) | +| `MEOW_MDNS_PORT` | 5353 | 监听端口(通常仅宿主单测用) | +| `MEOW_MDNS_TTL` | 255 | IP_MULTICAST_TTL | +| `MEOW_MDNS_CF` | 0 | 唯一记录(A/SRV/TXT)是否置 cache-flush 位 | +| `MEOW_MDNS_QD` | 1 | 应答是否回显问题段(QDCOUNT) | +| `MEOW_MDNS_MCAST`| 1 | 组播查询是否同时回组播应答(0=只单播回源) | +| `MEOW_MDNS_MIF` | 0 | 是否显式设 IP_MULTICAST_IF 为设备接口 IP | +| `MEOW_MDNS_DBG` | - | (调试用) | + +## ARCH / 运行库 +- ARCH: mipsel (MIPS32r2 little-endian) +- 目标: mipsel-linux-musleabi (soft-float) +- 静态链接 musl,无 glibc 依赖。 + +## file 结果(bin/meow_mdns) + ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), statically linked, stripped + +## 体积 +- 部署版:**79,468 B ≈ 77.6 KB**(strip 后) +- MD5 :**b23de7ce54873e926e7601a3b21b66a9** + +## 构建命令 +见同目录 build.sh(宿主 gcc `-O2` 可编译并在本机组播/单播自测;交叉 `zig-mipsel-cc -Os -static`)。 + +## 部署 +- 设备:`/usr/data/mnt/sd_0/MeowOS/opt/mdnsd/mdnsd`(已部署, 755)。启动 `./mdnsd -v`(或经 meow_svcd 托管)。 +- 验证:设备 `netstat -uln | grep 5353`(0.0.0.0:5353)、`cat /proc/net/igmp`(wlan0 含 `FB0000E0`=224.0.0.251)、主机单播 `dig @<设备IP> -p 5353 .local A` 或 python 探测。 + +## 已知限制(重要) +- **手机热点丢弃客户端间组播**:设备能收到主机发来的组播查询并组播+单播应答,但**设备→主机组播被热点丢弃**(主机加入 224.0.0.251 组后 5s 收不到设备组播应答)。avahi 又**只接受组播应答**(对组播查询的单播应答, avahi 拒绝, 已实测)。故此刻 `avahi-resolve .local` **超时**——这是**网络层热点隔离**,非应答器缺陷。主机需 DNS 兜底(如 `getent hosts meowos.local` 走库, 或主机 `/etc/hosts` 静态条目, 或设备侧起一个 UDP:53 应答器)。 +- 本机(多接口 phone-Linux host)自测时, 应答器组播发送在同机环回也偶发不达(与接口/多默认路由相关),但单播回源始终正确;单接口设备上的组播发送不受此影响。 + +## bin/ 内容 +- `bin/meow_mdns` = 部署版(mipsel 静态, 79,468 B, md5 b23de7ce…) diff --git a/mips-repo/meow_mdns/meow_mdns.c b/mips-repo/meow_mdns/meow_mdns.c new file mode 100644 index 0000000..1f2320c --- /dev/null +++ b/mips-repo/meow_mdns/meow_mdns.c @@ -0,0 +1,483 @@ +/* + * meow_mdns.c — 极简 IPv4-only mDNS 应答器(MeowOS / R3 Pro II) + * + * 目标: 让设备在纯 IPv4 下应答 .local (A 记录) 与 _ssh._tcp.local (PTR/SRV/TXT), + * 供主机 avahi-resolve .local 解析到设备 IP。 + * + * 为什么不用 mDNSResponder/SAResponder: 两者均需 IPv6(socket AF_INET6 在内核无 IPv6 时 fatal), + * 且完整版 mdnsd 是 IPC 服务型守护进程,自身不公布主机名 A 记录;SAResponder 只注册单一服务。 + * 本实现为自研,零 IPv6 依赖,线程/信号安全,优雅退。 + * + * 用法: meow_mdns [-v] # 前台运行; -v 打印收到的查询/源地址 + * 环境: MEOW_MDNS_HOST 缺省 = gethostname() 第一标签 (设备为"ingenic") + * MEOW_MDNS_ADDR 缺省 = 第一块非回环 UP 接口的 IPv4 (可为 wlan0 10.109.49.50) + * MEOW_MDNS_IF 缺省 = 自动; 设定接口名, join 组播绑定到该接口 + * MEOW_MDNS_PORT 缺省 = 5353; 可覆写(通常仅宿主单测用) + * + * 构建: 宿主机 cc -O2 -o meow_mdns meow_mdns.c + * 交叉 zig-mipsel-cc -Os -static -o meow_mdns meow_mdns.c + * + * 协议要点(RFC 6762): + * - join 224.0.0.251:5353 (IP_ADD_MEMBERSHIP), SO_REUSEADDR(+SO_REUSEPORT) + * - 应答所有收到的查询(单播/组播), 回发到查询源地址(单播), 规避组播回程丢失 + * - 应答头 flags = 0x8400 (QR=1, AA=1), 回显查询的 Question 段 + * - 记录均带 TTL 120; 唯一记录(A/SRV/TXT)置 cache-flush 位 0x8000, PTR 不置 + * - QNAME 大小写不敏感; 应答中名字不压缩(合法, 由 avahi 解析) + * - 忽略 QR=1 包(避免自响应), 忽略非查询包 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MDNS_PORT 5353 +#define MDNS_GROUP "224.0.0.251" +#define TTL_DEFAULT 120 +#define QCLASS_IN 1 +#define QFLUSH 0x8000 +#define TYPE_A 1 +#define TYPE_PTR 12 +#define TYPE_TXT 16 +#define TYPE_SRV 33 +#define TYPE_ANY 255 +#define MAX_Q 16 /* 每包最多解析的 question 数 */ +#define MAX_NAME 256 + +static volatile sig_atomic_t g_running = 1; +static int g_cacheflush = 0; /* 唯一记录(A/SRV/TXT)是否置 cache-flush 位 (MEOW_MDNS_CF) */ +static void on_signal(int sig) { (void)sig; g_running = 0; } + +static void to_lower(char *s) { for (; *s; s++) *s = (char)tolower((unsigned char)*s); } + +/* 把 "a.b.c" 编码为 DNS 名字(无压缩) ==> abc<0>。返回长度, -1 失败 */ +static int encode_name(uint8_t *buf, size_t buflen, const char *name) +{ + size_t pos = 0; + const char *p = name; + if (!name || !*name) return -1; + while (*p) { + const char *dot = strchr(p, '.'); + size_t len = dot ? (size_t)(dot - p) : strlen(p); + if (len == 0 || len > 63) return -1; + if (pos + 1 + len >= buflen) return -1; + buf[pos++] = (uint8_t)len; + memcpy(buf + pos, p, len); pos += len; + if (!dot) break; + p = dot + 1; + } + if (pos + 1 >= buflen) return -1; + buf[pos++] = 0; + return (int)pos; +} + +/* 解析 DNS 名字(支持 0xC0 压缩指针)。返回解码长度; 把 *off 移到名字结束处 */ +static int parse_name(const uint8_t *msg, size_t msglen, size_t *off, + char *out, size_t outlen) +{ + size_t pos = *off, olen = 0, jumps = 0; + int jumped = 0; + while (1) { + if (pos >= msglen) return -1; + uint8_t c = msg[pos]; + if (c == 0) { pos++; break; } + if ((c & 0xC0) == 0xC0) { + if (pos + 1 >= msglen) return -1; + size_t ptr = ((c & 0x3F) << 8) | msg[pos + 1]; + pos += 2; + if (!jumped) *off = pos; + jumped = 1; + if (++jumps > 64) return -1; + pos = ptr; + continue; + } + if ((c & 0xC0) != 0) return -1; /* 非法标签 */ + pos++; + if (olen > 0) { if (olen + 1 >= outlen) return -1; out[olen++] = '.'; } + if (pos + c > msglen) return -1; + if (olen + c >= outlen) return -1; + memcpy(out + olen, msg + pos, c); olen += c; pos += c; + } + if (!jumped) *off = pos; + out[olen] = 0; + return (int)olen; +} + +/* 小写入工具 */ +static size_t w16(uint8_t *b, size_t o, uint16_t v){ b[o]=v>>8; b[o+1]=v&0xFF; return o+2; } +static size_t w32(uint8_t *b, size_t o, uint32_t v){ b[o]=v>>24; b[o+1]=v>>16; b[o+2]=v>>8; b[o+3]=v&0xFF; return o+4; } + +struct question { + char name[MAX_NAME]; /* 小写 QNAME */ + uint16_t qtype; + uint16_t qclass; + size_t raw_off; /* question 原始字节在消息中的起点 */ + size_t raw_len; /* question 原始字节长度(用于回显) */ +}; + +/* 编码一个 DNS 名字到 resp[*ro..];若能在 resp[0..*ro) 中找到完全相同的名字序列, + * 则改写为压缩指针(RFC 1035 §4.1.4)。返回 0 成功, -1 失败。 */ +static int put_name(uint8_t *resp, size_t *ro, size_t cap, const char *name) +{ + uint8_t tmp[256]; + int tl = encode_name(tmp, sizeof(tmp), name); + if (tl < 0) return -1; + for (size_t i = 0; i + (size_t)tl <= *ro && i < 0x4000; i++) { + if (resp[i] == tmp[0] && memcmp(resp + i, tmp, (size_t)tl) == 0) { + if (*ro + 2 > cap) return -1; + resp[(*ro)++] = (uint8_t)(0xC0 | (i >> 8)); + resp[(*ro)++] = (uint8_t)(i & 0xFF); + return 0; + } + } + if (*ro + (size_t)tl > cap) return -1; + memcpy(resp + *ro, tmp, (size_t)tl); *ro += (size_t)tl; + return 0; +} + +/* 追加一条 answer 记录。owner=记录名(自带大小写, 小写即可), type, unique, + * rdata 指针+长度。返回 0 成功 */ +static int put_answer(uint8_t *resp, size_t *ro, size_t cap, + const char *owner, uint16_t type, int unique, + const uint8_t *rdata, uint16_t rdlen) +{ + int nl = put_name(resp, ro, cap, owner); + if (nl < 0 || (size_t)(*ro + 2 + 2 + 4 + 2 + rdlen) > cap) return -1; + *ro = w16(resp, *ro, type); + *ro = w16(resp, *ro, (uint16_t)(QCLASS_IN | ((unique && g_cacheflush) ? QFLUSH : 0))); + *ro = w32(resp, *ro, TTL_DEFAULT); + size_t rdlen_pos = *ro; *ro += 2; /* 占位 */ + if (rdlen) { memcpy(resp + *ro, rdata, rdlen); *ro += rdlen; } + /* 回填 rdlen(注意:*ro 已越过 rdata,不能用 w16 的返回值覆盖 *ro) */ + resp[rdlen_pos] = (uint8_t)(rdlen >> 8); + resp[rdlen_pos + 1] = (uint8_t)(rdlen & 0xFF); + return 0; +} + +int main(int argc, char **argv) +{ + int verbose = 0; + for (int i = 1; i < argc; i++) + if (!strcmp(argv[i], "-v")) verbose = 1; + uint16_t port = MDNS_PORT; + const char *ep = getenv("MEOW_MDNS_PORT"); + if (ep && *ep) port = (uint16_t)strtoul(ep, NULL, 10); + if (port == 0) port = MDNS_PORT; + int qd_echo = 1; /* 是否回显问题段 (QDCOUNT) */ + const char *eq = getenv("MEOW_MDNS_QD"); + if (eq && *eq) qd_echo = atoi(eq); + g_cacheflush = 0;/* 唯一记录(A/SRV/TXT)是否置 cache-flush 位 */ + const char *ecf = getenv("MEOW_MDNS_CF"); + if (ecf && *ecf) g_cacheflush = atoi(ecf); + int reply_mcast = 1;/* 组播查询是否同时回一条组播应答; 0=只单播回源 */ + const char *emc = getenv("MEOW_MDNS_MCAST"); + if (emc && *emc) reply_mcast = atoi(emc); + int set_mcastif = 0;/* 是否显式设置 IP_MULTICAST_IF (0=走内核默认, 同 zeroconf, 更稳) */ + const char *emi = getenv("MEOW_MDNS_MIF"); + if (emi && *emi) set_mcastif = atoi(emi); + + /* ---- 确定主机名 ---- */ + char host[MAX_NAME]; + const char *eh = getenv("MEOW_MDNS_HOST"); + if (eh && *eh) { snprintf(host, sizeof(host), "%s", eh); } + else { + if (gethostname(host, sizeof(host) - 1) != 0 || !host[0]) + snprintf(host, sizeof(host), "meowos"); + host[sizeof(host) - 1] = 0; + char *d = strchr(host, '.'); if (d) *d = 0; /* 只取第一标签 */ + } + if (!*host) snprintf(host, sizeof(host), "meowos"); + to_lower(host); + + /* ---- 确定 IP ---- */ + char hostA[MAX_NAME]; /* "host.local" */ + snprintf(hostA, sizeof(hostA), "%s.local", host); + char inst[MAX_NAME]; /* "host._ssh._tcp.local" */ + snprintf(inst, sizeof(inst), "%s._ssh._tcp.local", host); + char svc[MAX_NAME] = "_ssh._tcp.local"; + + /* 地址全程保持网络字节序 (struct in_addr) */ + struct in_addr ipin = {0}; + const char *ea = getenv("MEOW_MDNS_ADDR"); + if (ea && *ea) { + if (inet_pton(AF_INET, ea, &ipin) != 1) ipin.s_addr = 0; + } + if (!ipin.s_addr) { + /* 遍历接口取第一块非回环 UP 的 IPv4(sin_addr 已是网络序) */ + struct ifaddrs *ifa, *i; + if (getifaddrs(&ifa) == 0) { + for (i = ifa; i; i = i->ifa_next) { + if (!i->ifa_addr || i->ifa_addr->sa_family != AF_INET) continue; + if (!(i->ifa_flags & IFF_UP) || (i->ifa_flags & IFF_LOOPBACK)) continue; + ipin = ((struct sockaddr_in *)i->ifa_addr)->sin_addr; + break; + } + freeifaddrs(ifa); + } + } + if (!ipin.s_addr) { + fprintf(stderr, "meow_mdns: no IPv4 address (set MEOW_MDNS_ADDR)\n"); + return 2; + } + char ips[32]; inet_ntop(AF_INET, &ipin, ips, sizeof(ips)); + + printf("meow_mdns: host=%s.local addr=%s swc=%s\n", host, ips, svc); + fflush(stdout); + + /* ---- 打开 UDP 套接字 ---- */ + int s = socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) { perror("socket"); return 2; } + int one = 1; + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)); /* 可能失败, 忽略 */ + /* 接收时携带目的地址与接口 (用于判断查询是组播还是单播, 再决定应答方向) */ + setsockopt(s, IPPROTO_IP, IP_PKTINFO, &one, sizeof(one)); + + struct sockaddr_in bindaddr; + memset(&bindaddr, 0, sizeof(bindaddr)); + bindaddr.sin_family = AF_INET; + bindaddr.sin_port = htons(port); + bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(s, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) < 0) { + perror("bind"); return 2; + } + + /* ---- 加入组播组 ---- */ + struct ip_mreq mreq; + memset(&mreq, 0, sizeof(mreq)); + inet_pton(AF_INET, MDNS_GROUP, &mreq.imr_multiaddr); + mreq.imr_interface.s_addr = ipin.s_addr; /* 绑定到设备 IP */ + if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { + perror("IP_ADD_MEMBERSHIP"); + /* 不视为致命: 仍可应答单播查询 */ + } + unsigned char ttl = 255; + const char *ettl = getenv("MEOW_MDNS_TTL"); + if (ettl && *ettl) ttl = (unsigned char)atoi(ettl); + setsockopt(s, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)); + /* IP_MULTICAST_LOOP 保持默认(1): 允许本机其它 socket(如 avahi)收到组播应答; + * 自身收到的应答会是 QR=1, 已被下方 flags&0x8000 过滤, 不会自响应。 */ + + /* 让 recv 有超时以便检查 g_running */ + struct timeval tv = { 1, 0 }; + setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + signal(SIGINT, on_signal); + signal(SIGTERM, on_signal); + signal(SIGPIPE, SIG_IGN); + + struct in_addr mcast_addr; + inet_pton(AF_INET, MDNS_GROUP, &mcast_addr); + + /* 找出持有设备 IP 的接口索引 (组播应答固定从该接口发出, 避免 rcv_ifindex 落到 lo/错接口) */ + int dev_ifindex = 0; + { + struct ifaddrs *ifa, *i; + if (getifaddrs(&ifa) == 0) { + for (i = ifa; i; i = i->ifa_next) { + if (i->ifa_addr && i->ifa_addr->sa_family == AF_INET && + ((struct sockaddr_in *)i->ifa_addr)->sin_addr.s_addr == ipin.s_addr) { + dev_ifindex = (int)if_nametoindex(i->ifa_name); + if (dev_ifindex) break; + } + } + freeifaddrs(ifa); + } + } + + printf("meow_mdns: listening on %s:%d (multicast %s)\n", MDNS_GROUP, port, MDNS_GROUP); + if (verbose) + printf("meow_mdns: ip=%s dev_ifindex=%d\n", ips, dev_ifindex); + fflush(stdout); + + char pkt[2048]; + struct sockaddr_in src; + + while (g_running) { + /* recvmsg + IP_PKTINFO: 拿到查询的目的地址与到达接口, 判断组播/单播 */ + memset(&src, 0, sizeof(src)); + unsigned char ctrl[256]; + struct iovec iov; + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_name = &src; + msg.msg_namelen = sizeof(src); + iov.iov_base = pkt; + iov.iov_len = sizeof(pkt); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = ctrl; + msg.msg_controllen = sizeof(ctrl); + + ssize_t n = recvmsg(s, &msg, 0); + if (n < 0) { + if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue; + if (!g_running) break; + perror("recvmsg"); continue; + } + if (n < 12) continue; + + uint32_t destip = mcast_addr.s_addr; /* 取不到就按组播处理 */ + for (struct cmsghdr *cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) { + if (cm->cmsg_level == IPPROTO_IP && cm->cmsg_type == IP_PKTINFO) { + struct in_pktinfo *pi = (struct in_pktinfo *)CMSG_DATA(cm); + if (pi->ipi_addr.s_addr) destip = pi->ipi_addr.s_addr; + } + } + + uint8_t *m = (uint8_t *)pkt; + size_t ml = (size_t)n; + uint16_t flags = (uint16_t)((m[2] << 8) | m[3]); + if (flags & 0x8000) continue; /* 是应答/响应, 忽略 */ + uint16_t qd = (uint16_t)((m[4] << 8) | m[5]); + if (qd == 0 || qd > MAX_Q) continue; + + struct question qs[MAX_Q]; + size_t off = 12; + int nq = 0; + for (int qi = 0; qi < qd; qi++) { + size_t qstart = off; + if (parse_name(m, ml, &off, qs[nq].name, sizeof(qs[nq].name)) < 0) + { nq = -1; break; } + if (off + 4 > ml) { nq = -1; break; } + qs[nq].qtype = (uint16_t)((m[off] << 8) | m[off + 1]); + qs[nq].qclass = (uint16_t)((m[off + 2] << 8) | m[off + 3]); + off += 4; + qs[nq].raw_off = qstart; + qs[nq].raw_len = off - qstart; + to_lower(qs[nq].name); + nq++; + } + if (nq < 0) continue; + + if (verbose) { + char srcip[64]; + inet_ntop(AF_INET, &src.sin_addr, srcip, sizeof(srcip)); + printf("Q from %s:%d, %d question(s)\n", srcip, ntohs(src.sin_port), nq); + for (int qi = 0; qi < nq; qi++) + printf(" q[%d] %s type=%u class=%u\n", + qi, qs[qi].name, qs[qi].qtype, qs[qi].qclass); + fflush(stdout); + } + + /* ---- 构建应答 ---- + * RFC 6762: 应答 QDCOUNT 通常为 0, 但部分实现(avahi)接受、部分要求回显问题。 + * MEOW_MDNS_QD 控制是否回显; MEOW_MDNS_CF 控制唯一记录 cache-flush 位。 */ + uint8_t resp[2048]; + size_t ro = 0, ancount = 0; + size_t head_ro = ro; ro += 12; /* 预占 header */ + uint16_t qdcount = 0; + if (qd_echo) { + for (int qi = 0; qi < nq; qi++) { + if (ro + qs[qi].raw_len > sizeof(resp)) { ro = 0; ancount = 0; break; } + memcpy(resp + ro, m + qs[qi].raw_off, qs[qi].raw_len); + ro += qs[qi].raw_len; + } + qdcount = (uint16_t)nq; + } + + /* 依据每个 question 的 (qname, qtype) 追加相应记录 */ + for (int qi = 0; qi < nq && ro; qi++) { + const char *qn = qs[qi].name; + uint16_t qt = qs[qi].qtype; + int wantA = (qt == TYPE_A || qt == TYPE_ANY); + int wantPTR = (qt == TYPE_PTR || qt == TYPE_ANY); + int wantSRV = (qt == TYPE_SRV || qt == TYPE_ANY); + int wantTXT = (qt == TYPE_TXT || qt == TYPE_ANY); + + /* host.local -> A */ + if (wantA && !strcmp(qn, hostA)) { + uint8_t rd[4]; + memcpy(rd, &ipin.s_addr, 4); /* 网络序, 直接作为 A rdata */ + if (put_answer(resp, &ro, sizeof(resp), hostA, TYPE_A, 1, rd, 4) == 0) ancount++; + } + /* _ssh._tcp.local -> PTR 实例 */ + if (wantPTR && !strcmp(qn, svc)) { + uint8_t rd[256]; + int rl = encode_name(rd, sizeof(rd), inst); + if (rl > 0 && put_answer(resp, &ro, sizeof(resp), svc, TYPE_PTR, 0, rd, (uint16_t)rl) == 0) ancount++; + } + /* host._ssh._tcp.local -> SRV + TXT */ + if ((wantSRV || wantTXT) && !strcmp(qn, inst)) { + if (wantSRV) { + uint8_t rd[256]; + size_t rp = 0; + rp = w16(rd, rp, 0); rp = w16(rd, rp, 0); /* priority=weight=0 */ + rp = w16(rd, rp, 22); /* SSH port */ + int hl = encode_name(rd + rp, sizeof(rd) - rp, hostA); + if (hl > 0 && put_answer(resp, &ro, sizeof(resp), inst, TYPE_SRV, 1, rd, (uint16_t)(rp + hl)) == 0) ancount++; + } + if (wantTXT) { + uint8_t rd[2] = { 0x00 }; /* 空 TXT */ + if (put_answer(resp, &ro, sizeof(resp), inst, TYPE_TXT, 1, rd, 1) == 0) ancount++; + } + } + /* _services._dns-sd._udp.local -> PTR _ssh._tcp.local (便于 avahi-browse) */ + if (wantPTR && !strcmp(qn, "_services._dns-sd._udp.local")) { + uint8_t rd[256]; + int rl = encode_name(rd, sizeof(rd), svc); + if (rl > 0 && put_answer(resp, &ro, sizeof(resp), "_services._dns-sd._udp.local", TYPE_PTR, 0, rd, (uint16_t)rl) == 0) ancount++; + } + } + if (ro == 0) continue; /* 构建失败或没有记录, 放弃 */ + if (ancount == 0) continue; /* 没有我们需要应答的记录 */ + + /* 组装 header */ + memset(resp + head_ro, 0, 12); + resp[head_ro] = 0x00; resp[head_ro+1] = 0x00; /* ID */ + resp[head_ro+2] = 0x84; resp[head_ro+3] = 0x00; /* QR+AA, no error */ + resp[head_ro+4] = (uint8_t)(qdcount >> 8); resp[head_ro+5] = (uint8_t)qdcount; + resp[head_ro+6] = (uint8_t)(ancount >> 8); resp[head_ro+7] = (uint8_t)ancount; + resp[head_ro+8] = 0; resp[head_ro+9] = 0; /* ANCOUNT of NS */ + resp[head_ro+10] = 0; resp[head_ro+11] = 0; /* ARCOUNT */ + + /* 选择应答方向: + * 查询到达组播组 -> 应答走组播(RFC 6762: 组播查询期待组播应答, avahi 才会接受), + * 并冗余单播一份给查询源(组播被热点丢弃时兜底)。 + * 查询到达单播 -> 直接单播回源。 */ + int to_mcast = (destip == mcast_addr.s_addr); + struct sockaddr_in dst; + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + if (to_mcast && reply_mcast) { + /* 组播应答固定从持有设备 IP 的接口发出 */ + unsigned char mloop = 1; + /* IP_MULTICAST_IF 用设备接口的 IPv4 地址(网络序), 而非接口索引 */ + if (set_mcastif && ipin.s_addr) + setsockopt(s, IPPROTO_IP, IP_MULTICAST_IF, &ipin.s_addr, sizeof(ipin.s_addr)); + setsockopt(s, IPPROTO_IP, IP_MULTICAST_LOOP, &mloop, sizeof(mloop)); + dst.sin_port = htons(MDNS_PORT); + dst.sin_addr = mcast_addr; + ssize_t sn = sendto(s, resp, ro, 0, (struct sockaddr *)&dst, sizeof(dst)); + if (verbose) + printf(" -> mcast sendto ret=%zd errno=%s\n", sn, errno ? strerror(errno) : "ok"); + } + /* 单播回源 (始终发送; 也作为组播应答的兜底/冗余) */ + dst.sin_port = src.sin_port; + dst.sin_addr = src.sin_addr; + ssize_t su = sendto(s, resp, ro, 0, (struct sockaddr *)&dst, sizeof(dst)); + (void)su; + if (verbose) { + printf(" -> replied %zu bytes to %s%s:%d (AN %zu)\n", ro, + to_mcast ? "mcast(224.0.0.251)" : "", + inet_ntop(AF_INET, &src.sin_addr, ips, sizeof(ips)), + ntohs(src.sin_port), ancount); + fflush(stdout); + } + } + + /* 退出: 离开组播组 */ + if (setsockopt(s, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)) == 0) + close(s); + printf("meow_mdns: exiting\n"); + return 0; +} diff --git a/patch_v2/meow_launcherd/meow_settings.c b/patch_v2/meow_launcherd/meow_settings.c index 72a41c2..e941887 100644 --- a/patch_v2/meow_launcherd/meow_settings.c +++ b/patch_v2/meow_launcherd/meow_settings.c @@ -129,6 +129,9 @@ static const meow_setting_def g_defs[] = { * 用户关=0 则 ensure_wifi 不再自动拉起 / 蓝牙默认关=少耗电) */ { "network.wifi_enabled", SET_BOOL, "1", 0, 0, NULL, NULL }, { "network.bluetooth_enabled", SET_BOOL, "0", 0, 0, NULL, NULL }, + /* 系统(M2.54:主机名,bootmenu S89meow_svcd 开机读 system.hostname sethostname; + * 缺省 meowos;RFC1035 首标签,供 meow_mdns 公布 .local) */ + { "system.hostname", SET_STR, "meowos", 0, 0, NULL, NULL }, }; /* ---- 日志(自包含,不依赖 launcher 的 logmsg;前缀 "settings:")---- */ diff --git a/patch_v2/meow_svcd/S89meow_svcd b/patch_v2/meow_svcd/S89meow_svcd new file mode 100755 index 0000000..7e7e42b --- /dev/null +++ b/patch_v2/meow_svcd/S89meow_svcd @@ -0,0 +1,84 @@ +#!/bin/sh +# +# S89meow_svcd - MeowOS 服务管理器开机自启 + 主机名应用 + 板载服务预置 +# +# 位置:S84ssh 之后、S90meow_bootmenu 之前(S80 < S84 < S89 < S90)。 +# * rcS 顺序执行 /etc/init.d/S??*,而 S90meow_bootmenu 里 `exec /usr/bin/meow_bootmenu +# --fork` 会阻塞 rcS(选单后 exec meow_launcherd 或 fork 播放器监督),故任何 S90 +# 之后的 init.d(如 S91/S92)**都不会执行**——svcd 必须排在 S90 之前。 +# * 本脚本只装「稳定框架/KB级设置」:meow_svcd(固件 /usr/bin/)+ 板载服务配置 +# (/usr/data/meow/services.d/,Data 区,首启预置 mdnsd.conf)+ 应用主机名。 +# +# 红线:不 killall、不碰播放器信号、不刷机。svcd 以 -f 前台被本脚本重挂循环拉起 +#(失败降级不死——若 svcd 死,其所托管服务已是独立子进程、被 init 收养继续运行); +# 服务本身(mdnsd)由 svcd 按 restart=on-fail 托管。 +# +case "$1" in + start) + echo -n "Starting meow_svcd (S89): " + + # 1) 应用主机名:读板载设置 system.hostname(settings.conf),缺省 meowos。 + # 须早于 meow_mdns 启动(mdnsd 默认用 gethostname() 公布 .local)。 + host="meowos" + if [ -f /usr/data/meow/settings.conf ]; then + v=$(awk -F= '$1 ~ /^[[:space:]]*system\.hostname[[:space:]]*$/ {gsub(/[[:space:]]/,"",$2); print $2; exit}' /usr/data/meow/settings.conf) + [ -n "$v" ] && host="$v" + fi + # 仅允许 RFC1035 首标签字符(A-Za-z0-9 和 '-'/'.'/'_'),否则回退 meowos + case "$host" in + ''|*[!A-Za-z0-9_.-]*) host="meowos" ;; + esac + sethostname "$host" 2>/dev/null || hostname "$host" 2>/dev/null + echo -n "host=$host " + + # 2) 首启预置板载 mdnsd.conf(Data 区可写才建;已存在不覆盖) + if [ -d /usr/data/meow ] && [ -w /usr/data/meow ]; then + mkdir -p /usr/data/meow/services.d + if [ -f /usr/data/meow/services.d/mdnsd.conf ]; then + echo -n "mdnsd.conf exists " + else + cat > /usr/data/meow/services.d/mdnsd.conf <<'EOF' +exec=/usr/bin/meow_mdns +args= +enabled=1 +restart=on-fail +backoff_ms=2000 +max_restart=5 +trigger=boot +cap= +EOF + chmod 644 /usr/data/meow/services.d/mdnsd.conf + echo -n "provisioned mdnsd.conf " + fi + else + echo -n "skip mdnsd.conf (/usr/data/meow not writable) " + fi + + # 3) 拉起 meow_svcd(-f 前台,重挂循环防死;/tmp/meow_svcd.stop 标记防停循环) + rm -f /tmp/meow_svcd.stop + if [ -x /usr/bin/meow_svcd ]; then + if [ -f /tmp/meow_svcd.pid ] && kill -0 "$(cat /tmp/meow_svcd.pid 2>/dev/null)" 2>/dev/null; then + echo "already running" + else + ( while [ ! -f /tmp/meow_svcd.stop ]; do /usr/bin/meow_svcd -f; sleep 1; done ) & + echo "started (supervisor pid $!)" + fi + else + echo "MISSING /usr/bin/meow_svcd" + fi + ;; + stop) + echo -n "Stopping meow_svcd: " + touch /tmp/meow_svcd.stop + for p in $(ps | grep "[m]eow_svcd" | awk '{print $1}'); do kill "$p" 2>/dev/null; done + echo "OK" + ;; + restart|reload) + ;; + *) + echo "Usage: $0 {start|stop}" + exit 1 + ;; +esac + +exit 0 diff --git a/patch_v2/meow_svcd/meow_svcd b/patch_v2/meow_svcd/meow_svcd index 263db9d..846cadf 100755 Binary files a/patch_v2/meow_svcd/meow_svcd and b/patch_v2/meow_svcd/meow_svcd differ diff --git a/patch_v2/meow_svcd/meow_svcd.c b/patch_v2/meow_svcd/meow_svcd.c index e50f9ca..48cda6b 100644 --- a/patch_v2/meow_svcd/meow_svcd.c +++ b/patch_v2/meow_svcd/meow_svcd.c @@ -8,9 +8,13 @@ * - svcd 本身极简:无 dlopen、无外部依赖,仅依赖 POSIX + Linux 基础接口, * 交叉编译为 mipsel 静态(zig-mipsel-cc),打进固件(不入卡)。 * - * 配置来源(TF 卡,目录式): - * `$CARD/MeowOS/services.d/.conf`(默认 `CARD=/usr/data/mnt/sd_0`)。 - * 卡拔出 → svcd 优雅停止所托管服务并保留运行状态;插卡 → 自动重建服务。 + * 配置来源(双源,目录式;板载优先): + * - 板载 `/usr/data/meow/services.d/.conf`(Data 区,恒定存在,权威源); + * - TF 卡 `$CARD/MeowOS/services.d/.conf`(默认 `CARD=/usr/data/mnt/sd_0`)。 + * 扫描**两处都读**;同名服务**板载优先**(扫 TF 时若 name 已在板载 → 跳过 + WARN + * 「 板载与 TF 都存在,采用板载」)。无双源冲突时行为不变。 + * 卡拔出 → svcd 优雅停止所托管**卡上**服务并保留运行状态(板载服务视 trigger 保留); + * 插卡 → 自动重建。 * * .conf 字段(极简 key = value,'#'/';' 注释,见 meow_svcd.md 文档): * name / exec / args / env / enabled / restart / backoff_ms / max_restart / @@ -60,7 +64,9 @@ /* ---- 默认路径 / 常量 ---- */ #define SVCD_CARD_DEFAULT "/usr/data/mnt/sd_0" /* TF 卡挂载点(SPEC §1.2) */ -#define SVCD_SERVICES_REL "MeowOS/services.d" /* 配置目录(用户定案目录式) */ +#define SVCD_SERVICES_REL "MeowOS/services.d" /* TF 配置目录(用户定案目录式) */ +#define SVCD_SERVICES_BOARD "/usr/data/meow/services.d" /* 板载配置目录(首启预置/Data 区) */ +#define SVCD_PATH_MAX 256 /* 服务源目录路径缓冲上限 */ #define SVCD_SOCK_DEFAULT "/tmp/meow_svcd.sock" #define SVCD_STATUS_DEFAULT "/tmp/meow_svcd.status" #define SVCD_PIDFILE_DEFAULT "/tmp/meow_svcd.pid" @@ -111,6 +117,7 @@ typedef struct { int max_restart; /* <0=无限 */ char cap[SVCD_CAP_MAX]; /* 解析保留;实装为后续任务 */ char trigger[SVCD_TRIG_STR_MAX]; /* boot|card-mount|wifi-up */ + int src; /* 0=TF 卡, 1=板载(同名优先源) */ /* 运行状态 */ pid_t pid; @@ -131,12 +138,14 @@ static int g_nsvc = 0; /* 全局环境(可被 --card/--sock/--status 覆盖,亦读 env) */ static char g_card[256] = SVCD_CARD_DEFAULT; +static char g_board_dir[256] = SVCD_SERVICES_BOARD; /* 板载服务目录(--board/MEOW_SVCD_BOARD 可覆写) */ static char g_sock[SVCD_SOCK_PATH_MAX] = SVCD_SOCK_DEFAULT; static char g_status[256] = SVCD_STATUS_DEFAULT; static char g_pidfile[256] = SVCD_PIDFILE_DEFAULT; static char g_log[256] = ""; /* 可选日志文件 */ static int g_card_present = 0; +static int g_board_online = 0; /* 板载服务目录本帧可读 */ static int g_wifi_up = 0; static int g_shutdown = 0; static int g_fg = 0; /* -f:前台(supervisor 托管) */ @@ -513,9 +522,9 @@ static int trigger_satisfied(const svc_t *s) return g_card_present; case 'w': /* wifi-up */ return g_card_present && g_wifi_up; - case 'b': /* boot:配置在卡 → 随卡存在 */ + case 'b': /* boot:板载服务恒定存在,触发不依赖卡;TF 服务仍需卡 */ default: - return g_card_present; + return (s->src == 1) ? 1 : g_card_present; } } @@ -570,11 +579,23 @@ static int card_present(void) return 0; /* 同文件系统空目录 = 未挂载/未用 */ } -static int scan_directory(char out[][SVCD_NAME_MAX], int max) +static int board_services_exist(void) +{ + struct stat st; + if (stat(g_board_dir, &st) != 0 || !S_ISDIR(st.st_mode)) return 0; + return 1; +} + +static int name_in_list(const char *name, char list[][SVCD_NAME_MAX], int n) +{ + for (int i = 0; i < n; i++) + if (strcmp(list[i], name) == 0) return i; + return -1; +} + +/* 枚举某目录下的 *.conf,去除 .conf 得服务名。返回个数。 */ +static int scan_conf_names(const char *dir, char out[][SVCD_NAME_MAX], int max) { - char dir[512]; - if (snprintf(dir, sizeof dir, "%s/%s", g_card, SVCD_SERVICES_REL) - >= (int)sizeof dir) return 0; DIR *d = opendir(dir); if (!d) return 0; int n = 0; @@ -593,18 +614,48 @@ static int scan_directory(char out[][SVCD_NAME_MAX], int max) return n; } -/* 卡存在时(初始/插入/reload)重新读配置并调和服务表。 */ +/* 双源目录调和:板载(恒定存在、权威优先)先扫,卡上有则叠加扫 TF, + * 同名服务 TF 被板载屏蔽(WARN 记录)。无双源冲突时行为不变。 */ static void scan_and_reconcile(void) { char names[SVCD_MAX_SERVICES][SVCD_NAME_MAX]; - int n = scan_directory(names, SVCD_MAX_SERVICES); + char srcdir[SVCD_MAX_SERVICES][SVCD_PATH_MAX]; /* 每服务实际 .conf 所在目录 */ + int n = 0; + + if (board_services_exist()) { + int nb = scan_conf_names(g_board_dir, names, SVCD_MAX_SERVICES); + for (int i = 0; i < nb; i++) + safe_strcpy(srcdir[i], sizeof srcdir[0], g_board_dir); + n = nb; + } + + if (g_card_present) { + char tfdir[SVCD_PATH_MAX]; + if (snprintf(tfdir, sizeof tfdir, "%s/%s", g_card, SVCD_SERVICES_REL) + < (int)sizeof tfdir) { + char tfnames[SVCD_MAX_SERVICES][SVCD_NAME_MAX]; + int nt = scan_conf_names(tfdir, tfnames, SVCD_MAX_SERVICES); + for (int t = 0; t < nt; t++) { + if (name_in_list(tfnames[t], names, n) >= 0) { + svcd_log("%s 板载与 TF 都存在,采用板载", tfnames[t]); + continue; + } + if (n >= SVCD_MAX_SERVICES) { + svcd_log("service table full, drop %s", tfnames[t]); continue; + } + safe_strcpy(names[n], sizeof names[0], tfnames[t]); + safe_strcpy(srcdir[n], sizeof srcdir[0], tfdir); + n++; + } + } + } for (int i = 0; i < g_nsvc; i++) g_svcs[i].in_config = 0; for (int i = 0; i < n; i++) { char path[512]; - if (snprintf(path, sizeof path, "%s/%s/%s.conf", g_card, SVCD_SERVICES_REL, - names[i]) >= (int)sizeof path) continue; + if (snprintf(path, sizeof path, "%s/%s.conf", srcdir[i], names[i]) + >= (int)sizeof path) continue; int idx = find_svc(names[i]); if (idx < 0) { if (g_nsvc >= SVCD_MAX_SERVICES) { svcd_log("service table full, drop %s", names[i]); continue; } @@ -612,6 +663,7 @@ static void scan_and_reconcile(void) } svc_t tmp; if (load_conf(path, names[i], &tmp) != 0) { continue; } + tmp.src = (strcmp(srcdir[i], g_board_dir) == 0) ? 1 : 0; svc_t *s = &g_svcs[idx]; /* 判定是否需重启(结构字段变化) */ @@ -692,6 +744,8 @@ static void on_card_out(void) for (int i = 0; i < g_nsvc; i++) { svc_t *s = &g_svcs[i]; s->restart_at_ms = 0; + /* 拔卡后 trigger 仍满足的服务(板载-boot)不随卡停止(板载服务保留,MDNS 等常驻) */ + if (s->pid > 0 && trigger_satisfied(s)) continue; if (s->pid > 0) { stop_service(s, 0, 0); } else if (s->state == SV_RUNNING || s->state == SV_STOPPING) s->state = SV_STOPPED; } @@ -822,7 +876,7 @@ static void handle_connection(int fd) } else if (strcmp(line, "status") == 0) { cmd_status(fd); } else if (strcmp(line, "reload") == 0) { - if (g_card_present) scan_and_reconcile(); + scan_and_reconcile(); send_resp(fd, "OK\n"); } else if (sp && strcmp(line, "start") == 0) { send_resp(fd, cmd_start(sp) == 0 ? "OK\n" : "ERR no-such-service\n"); @@ -942,12 +996,13 @@ static int start_daemon(void) if (g_status_fd < 0) svcd_log("status file open fail: %s", strerror(errno)); else set_cloexec(g_status_fd); - /* 初始卡探测 */ + /* 初始卡探测 + 板载服务目录状态 */ g_card_present = card_present(); - if (g_card_present) scan_and_reconcile(); + g_board_online = board_services_exist(); + scan_and_reconcile(); /* 板载服务恒在,无卡也扫;卡上有则叠加扫 TF */ - svcd_log("meow_svcd up: card=%s present=%d sock=%s pid=%d", - g_card, g_card_present, g_sock, (int)getpid()); + svcd_log("meow_svcd up: card=%s present=%d board=%s sock=%s pid=%d", + g_card, g_card_present, g_board_dir, g_sock, (int)getpid()); struct pollfd pfds[2]; nfds_t nfds = 2; @@ -997,6 +1052,12 @@ static int start_daemon(void) if (present) { g_card_present = 1; svcd_log("card mounted, rebuild"); scan_and_reconcile(); } else { g_card_present = 0; on_card_out(); } } + int bonline = board_services_exist(); + if (bonline != g_board_online) { + g_board_online = bonline; + svcd_log("board services dir %s %s, rebuild", g_board_dir, bonline ? "online" : "offline"); + scan_and_reconcile(); + } } tick_evaluate(); build_status_file(); @@ -1048,6 +1109,7 @@ static void usage(const char *argv0) " options:\n" " -f, --foreground stay in foreground (supervisor fork/exec 托管)\n" " --card TF card mount point (default /usr/data/mnt/sd_0)\n" + " --board board services dir (default /usr/data/meow/services.d)\n" " --sock control socket (default /tmp/meow_svcd.sock)\n" " --status status file (default /tmp/meow_svcd.status)\n" " --pidfile pid file (default /tmp/meow_svcd.pid)\n" @@ -1065,6 +1127,7 @@ int main(int argc, char **argv) const char *a = argv[i]; if (strcmp(a, "-f") == 0 || strcmp(a, "--foreground") == 0) g_fg = 1; else if (strcmp(a, "--card") == 0 && i + 1 < argc) safe_strcpy(g_card, sizeof g_card, argv[++i]); + else if (strcmp(a, "--board") == 0 && i + 1 < argc) safe_strcpy(g_board_dir, sizeof g_board_dir, argv[++i]); else if (strcmp(a, "--sock") == 0 && i + 1 < argc) safe_strcpy(g_sock, sizeof g_sock, argv[++i]); else if (strcmp(a, "--status") == 0 && i + 1 < argc) safe_strcpy(g_status, sizeof g_status, argv[++i]); else if (strcmp(a, "--pidfile") == 0 && i + 1 < argc) safe_strcpy(g_pidfile, sizeof g_pidfile, argv[++i]); @@ -1076,6 +1139,7 @@ int main(int argc, char **argv) /* env 覆盖 */ const char *e; if ((e = getenv("MEOW_SVCD_CARD")) && e[0]) safe_strcpy(g_card, sizeof g_card, e); + if ((e = getenv("MEOW_SVCD_BOARD")) && e[0]) safe_strcpy(g_board_dir, sizeof g_board_dir, e); if ((e = getenv("MEOW_SVCD_SOCK")) && e[0]) safe_strcpy(g_sock, sizeof g_sock, e); if ((e = getenv("MEOW_SVCD_STATUS")) && e[0]) safe_strcpy(g_status, sizeof g_status, e); if ((e = getenv("MEOW_SVCD_LOG")) && e[0]) safe_strcpy(g_log, sizeof g_log, e); diff --git a/patch_v2/meow_svcd/meow_svcd.md b/patch_v2/meow_svcd/meow_svcd.md index 87b9993..71428da 100644 --- a/patch_v2/meow_svcd/meow_svcd.md +++ b/patch_v2/meow_svcd/meow_svcd.md @@ -8,9 +8,11 @@ 二进制入口:`patch_v2/meow_svcd/meow_svcd`(mipsel 静态,~98KB,<200KB 固件阈值)。 宿主编译:`patch_v2/meow_svcd/meow_svcd_host`。 -> 本阶段范围:守护 + 宿主测试。**接入 supervisor 启动链** -> (bootmenu/launcher fork+exec 拉起、waitpid)为后续任务——启动改固件有风险, -> 需另行确认。接入时用 `meow_svcd -f`(前台)保证 supervisor 可 `waitpid`。 +> 本阶段(M2.54)范围:守护 + 双源 services.d + 宿主测试。**接入系统启动链**已定: +> 固件预置 `etc/init.d/S89meow_svcd`(开机自启,`myow_svcd -f` 前台 + shell 重挂在 rcS 早期, +> 先于 S90meow_bootmenu——因 S90 exec 引导选单后阻塞 rcS,故须排在 S90 之前)。 +> 直接由 bootmenu/launcher `fork+exec` + `waitpid` 托管的方案仍成立(改固件有风险), +> 作为备选——用 `-f` 前台即可被 waitpid。 --- @@ -19,7 +21,8 @@ | 用途 | 默认值 | 覆盖方式 | |---|---|---| | TF 卡挂载点 | `/usr/data/mnt/sd_0`(SPEC §1.2) | `--card` / `MEOW_SVCD_CARD` | -| 服务配置目录 | `/MeowOS/services.d/` | 随 `--card` | +| 板载服务目录 | `/usr/data/meow/services.d`(首启预置/Data 区) | `--board` / `MEOW_SVCD_BOARD` | +| TF 服务目录 | `/MeowOS/services.d/` | 随 `--card` | | 控制 socket | `/tmp/meow_svcd.sock` | `--sock` / `MEOW_SVCD_SOCK` | | 状态文件 | `/tmp/meow_svcd.status` | `--status` / `MEOW_SVCD_STATUS` | | pid 文件 | `/tmp/meow_svcd.pid` | `--pidfile` | @@ -28,10 +31,18 @@ > 状态文件写板载 `/tmp`(tmpfs,存活期)或可配置;svcd 日志走 stderr 便于 > supervisor 捕获(`-f` 前台时子进程 stdout/stderr 也落到同一 sink)。 -## 2. services.d 目录式配置 +## 2. services.d 目录式配置(**双源**:板载 + TF,板载优先) -卡片上 `MeowOS/services.d/.conf`,一个服务一个 `.conf`。极简 `key = value`, -`#`/`;` 注释,空行忽略;**未知键忽略**(前向兼容)。行内注释可用 `\#` 转义。 +服务配置**两处都读**,一个服务一个 `.conf`: + +- **板载** `/usr/data/meow/services.d/.conf`:Data 区,恒定存在、权威优先; + 首启由固件 `S89meow_svcd` 预置(如 `mdnsd.conf`)。拔卡不丢。 +- **TF** `/MeowOS/services.d/.conf`:随卡存在/消失。 + +**同名遮蔽规则**:先扫板载(权威),再扫 TF;若 TF 出现与板载同名服务 → **跳过 + +WARN**「 板载与 TF 都存在,采用板载」。无双源冲突时行为不变。 + +极简 `key = value`,`#`/`;` 注释,空行忽略;**未知键忽略**(前向兼容)。行内注释可用 `\#` 转义。 | 字段 | 必填 | 类型 | 说明 | 默认 | |---|---|---|---|---| @@ -71,6 +82,23 @@ trigger = card-mount # cap = cap_net_bind_service,cap_net_raw ``` +**板载 mDNS 应答器示例**(固件预置 `/usr/data/meow/services.d/mdnsd.conf`,首启建): + +```ini +# 自研 IPv4-only mDNS 应答器(固件 /usr/bin/meow_mdns;板载服务,拔卡不丢) +exec = /usr/bin/meow_mdns +args = +enabled = 1 +restart = on-fail +backoff_ms = 2000 +max_restart = 5 +trigger = boot # 板载服务:boot 触发不依赖卡(恒定存在);cap 空=root 绑 5353/组播 +cap = +``` + +> `trigger` 为单值枚举(`boot|card-mount|wifi-up`)。板载服务用 `boot`(恒存在即启动); +> `wifi-up` 属扩展点(`/sys/class/net//operstate`),当前平台无无线接口恒 0。 + ## 3. 状态机 ``` @@ -112,7 +140,7 @@ FAILED 崩溃且不再重启(超 max_restart / restart=never) 优雅停所有服务**并保留 `restart_count`/`last_exit` 状态**;插卡 → `scan_and_reconcile()` 重建并按触发拉起。 - **`wifi-up`**:读 `/sys/class/net//operstate`,无无线接口恒 `0`(扩展点)。 -- `boot`:配置在卡 → 随卡存在即启动(拔卡即停)。 +- `boot`:TF 配置在卡 → 随卡存在即启动(拔卡即停);**板载配置**(`src=board`)→ 恒定存在即启动(不依赖卡,拔卡保留,供 mdnsd 等常驻服务)。 ## 6. CLI / 控制协议 @@ -139,7 +167,8 @@ meow_svcd pid ## 后续(不在本阶段) -- 接入 supervisor 启动链(bootmenu/launcher fork+exec、waitpid,`-f` 前台)。 +- ~~接入 supervisor 启动链~~(已改由 `etc/init.d/S89meow_svcd` 开机自启;bootmenu/launcher + `fork+exec` + `waitpid` 托管仍为备选)。 - `cap` 真机实装(平台是否支持 capset/pthread caps 待确认;需先探测)。 - `wifi-up` 事件细节(是否用 `/proc/net/wireless` / 重试文件,接口判定收紧)。 - svcd/服务日志落位到卡 `MeowOS/logs/` 的行为(当前 svcd 日志 stderr + 可选 `--log`)。