From 9b8a1e5c8b4fb212d4f5343b404becaa8a64d708 Mon Sep 17 00:00:00 2001 From: MeowOS Dev Date: Sun, 30 Aug 2026 09:25:16 +0800 Subject: [PATCH] =?UTF-8?q?M2.54:=20=E5=AD=97=E4=BD=93=E5=BD=92MeowOS/font?= =?UTF-8?q?s+SDK=E8=B7=9F=E9=9A=8F+=E5=A4=9A=E5=AD=97=E4=BD=93+=E5=85=A8?= =?UTF-8?q?=E5=B1=8F=EF=BC=9F(=E7=AD=89)+boot=E6=9D=BF=E8=BD=BD+=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=A0=8F28+=E4=BA=AE=E5=BA=A6/=E9=9F=B3=E9=87=8F?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D+mDNS=E5=9B=BA=E5=8C=96(meow=5Fsvcd=E5=8F=8C?= =?UTF-8?q?=E6=BA=90+meow=5Fmdns)+hostname=3Dmeowos+=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SYSTEM_SETTINGS.md | 1 + TF_CARD_LAYOUT.md | 3 +- docs/MEOWOS_SPEC_REF.md | 16 +- mips-repo/README.md | 3 +- mips-repo/meow_mdns/bin/meow_mdns | Bin 0 -> 79468 bytes mips-repo/meow_mdns/build.sh | 31 ++ mips-repo/meow_mdns/manifest.md | 69 ++++ mips-repo/meow_mdns/meow_mdns.c | 483 ++++++++++++++++++++++++ patch_v2/meow_launcherd/meow_settings.c | 3 + patch_v2/meow_svcd/S89meow_svcd | 84 +++++ patch_v2/meow_svcd/meow_svcd | Bin 99388 -> 101132 bytes patch_v2/meow_svcd/meow_svcd.c | 102 ++++- patch_v2/meow_svcd/meow_svcd.md | 47 ++- 13 files changed, 807 insertions(+), 35 deletions(-) create mode 100755 mips-repo/meow_mdns/bin/meow_mdns create mode 100644 mips-repo/meow_mdns/build.sh create mode 100644 mips-repo/meow_mdns/manifest.md create mode 100644 mips-repo/meow_mdns/meow_mdns.c create mode 100755 patch_v2/meow_svcd/S89meow_svcd 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 0000000000000000000000000000000000000000..02bbf368d344f44c53d7e4846e14eca95472c988 GIT binary patch literal 79468 zcmd?SdthAEb?Cj$nUOqtV2_P#1UTf3=8QC0!iLVZKo!Xt^Jv3EHqSIPA!#1md-+9@E%67(RBO%3`|jwD#kjy|STm(c;l9?E;1H(ZCcyjB{%nUZoX~(+RmQEH+O98xu~aO@y%O0y8S|G-Q3f& zZt=~%YlEy!AD7LYYj4}obE~Z1c#EuAzp>*?FmxEWSVOE3D6W12ZVof|uPH#wU-dq0@?iw>uA^X844KA7k!H`u>< z19*MEvu;xlr9Rf3_KuFt{Bn9ex%Tvm>$|l8LfyRn7LePy^@E9yV7Sel-CgTDJDkK{ zY;o4y-rEUan*cA-^ZrDKlWh607F<{^*EqLq-n4-NvKt(VKCz{FvE;R&{RPu)e2<4&k6Gg9ud&HY4h#VCEc4hAwqiO3a!DUdU|{0`u46ZoiOMor>lMQt(^|=J00!449;RQ zExX&-b~;_1xAZFe!SRS8`N$T2-QsNM+@Sq4jZIrNQ)|tZp4;WBjlCdaOE)k%J-5TU z8yrMfSD_X;-J3SIZ(iSZyR&6u`)%#(yUe7o+IU-g*LoUtc79xzZ{F0?bCEZk)3tu% z$7$c*anYuYG%_>vx|Y^pmIfR249=Es+PIOaE6x{+{ceK$M@tkYy&*4}Y?vGUd0b?du2oPZ^@Qh*-A zp3@vz7+blZN1H18ibt5{zYbW$;P#W7w$S;ewQIL*mg{=kdprF#wszBPEFN89t2V>Q zo3`{g(700<%Z!z^ww<-wMb;HtHhPStYjpjF?ygS7rAMs|(6XH+!$M%@N<(kaT*X4| zMJ>?nW){e8)NVD)#sJao^z_0>8+0OOp%pOY*HYe8&h!_VSG`AB*{rf2SqUPXb?rUQ znog+E)5)9|W7XzOy_?o<>hkHY81*6a7JPv^%C~DM?orGDhsqk?5@}Txw9ykUK0OK? zdbK~{0&S+p{N!fX#7y5v5WlzSV#Quzo-?w0wsa#66s6Z0W60}78x%?u#KN!X=C`zO zP#H3_Zm>4bRA>Z&k0uE!&V=p5oxPvjwE5%H%Rl&Cy!_83Up9K}ip5F$8gE_@dd-$w zZh;+oj4Ul6!k;msX00c(3RqC63Yp8>H=0>u!Z_`>AYXlYIX&yP^cpc$&bN!YL zP(*e#=hLCCY1@BBf%y>& zUmCtLe7)allb_yU(hA+)$lmZb?XQyNX7oH?{(IbL(42~zle;@uy_}AfTZ{uWo z4*vP?MVpx*Pvqi~D@xi*HkS04oU|S;`FhF!DEWCwMQPT$ptPxUWl@W-ndf%?J!Cy@ zebaiy+EF@G`e;!Nk>^7{E&X-r1+(5a>#A9QHtV6#mqXvV%$A$dwuJdM@=blnmK@)$ zd`}g9U*oy-bJ29^=R_j5(7=Om7J0Ky%d1-5mabYIPTyEm&uPh|otCb2mu*GDk;-Ue zQ*+vF$xON}U6Wm*LY{L&YSOvEopeK%9rib7@s?ea@s^#F5<1;tV}EysJSSw8Fhupj z0upD`E4zNxWZCttldX(Vr5iy$bI6->THajp=AM@KG4eikTHc>rzbpMG*Jsih=DTXO z*WYv3JCn~{Up=`XRKV3`z0ve#y^(Y={_g6Irtj*WnRj=0G;?=%Bona;ZReCmGjpJ2 zah^Al{+rKx|FTmp`s`ybo_a1MPOmL1Z=3l|ZkhS}^m6?j?e^;Adf*Hrk_O4(P#5dnU~GK#pOj+d0bHR6MvA1`KQdw=3oBr{~P$_E4KV>4Bp}U zGQWFC^&9cOQOY&{HUDepBbg3yq$HEiml}VT#Y(aGPd}O#|EGE7`3n9;UF5_0kFfzP z(aii>mf%`B5mJF}GDw47%x|+26>k4CLl*=!#c~j!8Ttyw*0xr@G(RgJ$(Naq9=9LI z8$~YA>iYJ2_g1>!w@Ka^m0m6FA!zH3=H+|yE$%7E4^nFIXuhTc%~yPBTIew;<>rS! z%5;g=`v3al;j?&t3W^6$W&SK6wayQH;3QM{AfQy0S*p8a^?CR}N!v(Qkgi;42_jNn z%vf?S>CZ?HlP3Etd6Bf6^aSY`X&4LPWm1QOpt|nbUI!U^YRCZXhlk^bjC~0)3 zC6lDvNWVk+8tLn#tM9NR3V%FK>XN=p+N}KY8GMwaNzz`@qolh?*L@DSNLPNIen>O@ z^h5dzX%xwp`2xOE(oxb@(w9kZB28y4*-E-=(2~8RgS&x;^eE}eq)mH45V~a-X*Fr( zUCfKLl~gT^RrfM4(k{|%q@$#Rq%V_>lFFAYd5m-&>5HU;q*J7oe+_)3FOx>FkX{?I zBt_af41A>P4p`Dfy6XYpB7L0n5b4XLlcan9hIx{{PI`j0Ys`{UT230pHn{04;Ei-E zX)|fpge7gHfhe^8;##q`z1yld>s@%kJ%4ce0`!Oqz}DG=tJd9T{k45h=3Yu`$h%*FP!K-Ts*^6e+@XSyU>XHVMi(B7qZC*Q40&_!O|RNH;l-S%Cf zz2QA2yG!q!H3*mn&N0$A@3O@q)sLDZlbYUbOEamgo?X4-K5G#6Gi9%67G*26taWDD zn`GS^v}>ImEpAr@94}MW;I)ecWs0j5W#N0!pIXL8^IFO4ATO|?G*$n3hP*DV#MrCu zvv%8eh6Z6l1KU>0e@1!QE1w=;+bWAS>_b0$WzOksk5c{}%GddASKepcX%B`BEH6>k z{6&9U%C*1O$U8)yU;k~egh5#JOSW`-{Z$wD=TMfR%(SIkC8)}S6XcosGPlVUe zS=&*j@alXO51PN4{FlgY)chL@_|Y9xcm!N}a7^P_$Js?$*C@QIWo@%_w$F4)eyUtN zx*vV*;Ucx}%uCUX{StK_KR6R6h3^FUQ{?aT#$MHNpLLIYcj&J0-jY3~yJy`Q87wpH zRmOJQZ_6s$$jaG~fVN5UkCMMr^IK;-!J5$dwo=yg0QRR>Hr?hX^1I15ykn=Q7ogI7 zD`ndrEUX#L>nCp?d8QwJYhI4Lm&h}?^z)9A7e387@ zy=~On`&H!G@2IC^j8gCDQw1C-&L1Q11bM}4Q0u-#ewan^IM4dgyw}K^B(F^~_-Njn zryT z;pC$el6++1RNcc9Z`K{1c)jkSiIa6lCVp1;;KYf#!}Q;moG(t5B^S7vNLXUgA>bT^ z)FJmGDRFO;QkTV1`KKDF(|ljQ zRNi8{Zl$E7KdqWaKP{|UN69G(C(XYl?wbdsvYc@egK4bGmefzHY`1){MaMAhpQrsS z+OwN7?ZbZi{Rb5fY3g0q5T@?ba}Jh+*XHkN6ESUcq?W;w)7p6TYo_a8?~h^0{4vgV9D}1*bY6+kx)ZgBi{>V_j$zxBxy5t#^4}9q zxF_PHb^FIK$t>kLt9c8!Q~r4^o|~9E4R_1nY@e2|#SheBfw`-8gS({ifd%|61~;$8 zhqA7ZryG2nYCoZ@RPHMqtvk=hDbLyYk}>pQeKBsxO(Zh_aiBjUlRn*!VOH0eej_r} zUj_|ICGMB)?|(g?Gi96oal&$}pYbPNAMngeh2_759Dj@|_qF)`x@Y2t4eW`O*!%93 zN{2P3b6yrgG}P{|I}txv_c7%O$~C_s>h{mOTPnB7C{}P3I)>V5 zN$g5X;_#GxqdYuulRPvr89zJ`mUECNk51(CXE#p9AFk6ilSsDNVcPAXeWEF6Yk9(P zY|R(DkRM^JpiI|lq^M0kf8El<@uLSH2d|Iu9UG)Cr_DYRA3k_L&q!hSFmpW`A2xk| z8+=4*KhKACj5de(>ioWAC)`8vBL#Y<(zfDl3_M2VuxYR3$EU|n+q396VOiF{m-(zHiS-WxTRvaXI7s`b-=FrQI9)~`D!VfmFEzAkz02^ZJqMfw&u971 zN=FX|m%9Bkcsge*uPV=Gp!-&NaN^zYZQ}4`XkXdkEH;>N^4KyLIPHP63TtFDg_<26?%v8CIlTCb1BDyw|hKi$rmsi6l3hVdb z(?l|7CsJ*;;Zu01R3?oZN6SA>`7_Yo>pgF% zZWq6AB;84>a8%ndx5Iz_kri`S_|NmnU+6!-D-?69{pU-=F*oTy|BO1W|J+X<$AA7? zo*Vt=Jatk$x5^_E-Ew52OP-wAb$)JQC*KU;ZG5+$_vi$0W`S?wrt=;KKI)x!bYk6k zPb!RV2l>FAZ6obIFK2jtr|i$(1Rv?z{+9S_dtaK2mWB&#r|ML`)a^QNBwH!%4d)@} z`r`+)>&_d_p5yaCta>?kZb625x*}qp!`Kn7;fpGMmq%WeM3VzWTKg-duOX-#kF1dp zy4-a>!+3YN^^qa@S>CCNMO$Uy{GlChS&or4gXa(Jk#bA!Jb!pkqPj+Yo^N+g=3@=N zvb-@@LeEO%2i`%qSB_*?$-#+rGBQygIotlRn%vJ#y&CkO>W`;uV~1~%`p9keSA0Cgc3mN2#TMTmv~`Y$bo~A3x>zz!-G8*d z27VRB+~+<0ulb85;?vn5s9NvjvWN|*)ktziA>t|bb4!%PPftN$QpEdrFCtsC2!P1 zyX;{8hYQHFTjgjrCYc45&}E?HsX^=rP&jW zW>v>{GDBtV*=|N2&8FmVR;-`gZCkJ2t#UcxK=+_tY6>0LQ~O5#k_DC48@0ucOzQYIs%U$^v=S$m0`<{dIqb{inJ)?9*KPyr1!uE~;mdw-fuU z(Yn1BwnySX-CnsLn^0|%i2K9OOJ!Kfd2VTl9X@7jTOBLpkF`*K;f;g5Khrp0^T*jM z*o5wZx<5ll3pOwM3Kgcl=08eJkB$D*u`3x{$HkLj#?>*kpJ4o1@|M3Q3v-SE(|dwB zyJv=_)Td)HuC(2Jznv_w+tU}jDVbIgZX zaTegJ7*Dh1ErTceJ~%yJziqS#*OrfKowLFl%(=L)av$d_SaSi+gZ<7bcuw0k_;c0Y z!Ly+OI1Xqq`wTSDxvY=_=$4r@DDGEb3Vzo|M&K(ay4QL%TWR&w-jttOV9CR^Hv&VK zOc^@;xlEe$KI@SY)@Jt0*3ihkR&Hb`I(nZqG+~!7kiRCcA6~3w@0Vi@C=IZoTzK;! z_+0EFyK>~+%hzIkEXAhT=RXh1@QCtpJra90@&6h)JDUeK_+{)mfBd@Hd&RA z&;QobRYavv+t7zjv!Pw^7xMR5_dcMq$1(euF7`AxmS8(ZGdBK*EPjYA{)jAoi7ft! zEPjeC{)#Mqi!A<&EPf2*ab}#z-HU~YP$feP>7RL=!p}}XJvB|fdsB5;KnrO0~oM^Njok&@a zOf*;zPb96Q6E5=GLH8`Q9-OGQ*rR09R#fhXKMqYK4o$Ha%SmEiM(I4g-Y)QMIgt+y z)JKko7GR$x4lAF`QyY~1ePgOx9<7Txo8i+w{K1XJE=VM&>|3xG5~)dhxi7;lSymg1 zT#x;5EEM+by!>S?i%()ps4pcJ`5uuMu?F_brVsY(E^uanHw)ZZ;LkFTEc3}SuPpP+ zGS4jYH8@XOPXNcK>}uIxr#ei>#$FnTIqv~pcf4G7xUon#dN0=S)4UVa{$qn3(D|do zBI#kpt1YgeO!}uxwuH3=8f26^E4T+MmoHbB|#w zW3w3ASTc;=cpO@O+dez6AtILoV{IqC>^Ty5QJ z;a$hr2H427b#f{T5>Wx4hqKH98|g@vIc1p}vf<$@bImemL+=!R#FxQ=3l7dScb(%D zzMl_5Lr1P*oTwWgyaqm7%2=_%^ZmIf-CujIra#z=h+N)ZB{>6ESXR3q#O6yN4{~ag zu+J~`<;1Cc-#{$7l-af5dA-E1-&c!Ybl{ixSQ0r${@$k*ekf$fJI%|RPm=lQI#zrI zX^Q#1$oyV_rq4jrNobnLK)=BZXFv}ceXshV4_fU*UU+j^ItEMwZ-MJD`vT2-=J^=D z5Y1mcut3@jtP$2@E&H*+*O-Zfz)cK0aCtv`JqFLhH_Q8frM|MLW8};_bn~g_YsLSPmN)qUV!$w~z&sKv&)mzFDZ-)W4(T7C#Qb_6;0AaYFk{q5u2%$Y)1E-~vek!0`nLfY@Y&3;U6 z87D0#SUXj$9mb=N46yASI~#e>-M!Nd^jg?o%ZhKs^)t`cKzXU9)7FA zZ$Xwnj4VHjJe!GwLC#>}5f^&tI=URE$Z&r?zl=kJ7KzG8e=M~Fy*=DthfhA!G)rQ; zo@YTE>7NhHW0^kAozJe#U~R;)Gh?|vk<5G)b<2S2m009baML%gzaw(RKv*sx(6tbA zROehf@bB#Z!m@W-j#Ntx@}XwDOXiNNOoK+q=?7WYN08HM!yQFVKRiQD2ehb??foC{ zu*GFbsfy_I8)@;d0q`5qO z%mRin#p#dnBgEE?T62AWO$7gVthx@|Tg^XvE(UGmV^KNYkG(!tCC^|lANS_){C?J4 zDZF!l@|v-$fvr-8#){g(3odi6>5oNru#ddLb*rDI-xB)W4^2nL;z%}^brf@y)-4So zWL2e%(ueZQ@K`L;!8)sv|B9bcdF+|z*Se>$Q>tXmn43gH%22~x^jcIp$5`FiwD@%+ zvyiDU54hEJ9L3(N!tUGb^R=$;Y5C^FWh-!(J6D?dhM#xcZ?V=uyc{s+cxhx`OIUyN zK{yw9_yZ?G+f;9jX4kj@y*GsWnl=FY2HDY6?zBibV&@>fJGG(G9E7W^daKfPwQVf2 zT~1`zpnES@8?1R}$ueM$?fp9IIY!&!u>$>AkV~GdI9>LsY>W8v{#5?lX}NbQADtoh zzU=RF3cOs9eZgE*j%YoVCHMLDH-6E>&C{BX-niSx>5wn)p9PnSD210j#I#KM+Vpq@ z`~~CaI#55t>GZnl9q9EV`ciro--ww?!-!x>fR^fDigGmY(JJ-pH$8RXcE#U9~7_6Qz6lk5c!!RxD$ z|EuK5iItoKtiVog#ZFEmUz?GyP3V|LbWBS119~hhLwgZt_)lx9bRUF_8#twB-^f@; zLhd|?SB(Pe8h`96i4R;R?I{h6Ryi7xRR1DN_XM`vFV;Y{f9AH3^9oMb%|3hMpa*v> zT4OsBubsvGm1j@oYmsrc?zN?TFEYZ|4EU=UyH;Y=?Z|EQkK?Om-=cU9V9-=y%E^!1 z4baD;ziwZ?>YhsX1GK|OX|o^p{0mj=EBd8nyqXj{XOOqjG(%BOfw0Qhu%D`CTaMk6&)2YDYH4_%zvfiu z#3GGy)9e`X(oxX)9J`14y64KVhKv1r3e!BPxw~Gj7*5Kv11@mQ6$>5PazOnK`_V^X zc@}*>lzoahbFKi~UO#i`=Usk3T1V^ZzD4VW<+|y5I$p3>3C80p;&3Ej2Ry|Fl_d=uSG>ufZz}1G%9*t6jA{A&0OTnRq6RpVM;;VAB3`#hCTV#}Se-+^+!KVnm`BQs`i zss0*~E#nC%1MW-A{zvo9-0!3hZ)O_agj2|SI}LA`LBk_H4dFRML&t|_uDoY^UXCrz zNKHeAy=6tF23u^&?&JHS(jd8sBwv@WW4=f0@@BuC;YN@Lubw-!$`#0f$;VdP zd0vKy@32|(HBsGfd$#kCfdxCc_HO8{=k_8$AjULYt3KiR(h}8k^|yEq;vXz|H8Yuy zC7FkTyIRV|>m#LVr}^_LIbirGImI+n*s6ieSv^*7E}uVhk+88% z?5ddPvt1c8I@u`VRg&3V30ERjh=?<1^-wAtSr zJK)6&T=Kb+#sq@&duEMjSz7e>0pBJN8A>|fyn^`WL^N&Bq=Pq~H25y)QuYG)?W)nK zRp``4>mYu;1AK?-knN38{L$Y#eSf(xE_L`Y%J*Fp8|NaN|Zxw!n*>_6hC;78cMi2ROY{M5M_BouD=~;{J6B77El#kCezK&fg zZKcYW_{Y3H{Ps5O)o#%>&z!Q%4L=|I2Yh_RI=l>@qRYAOHlHsZgI2B3DveHWMyEHS z(;Lz0DRg=Rx-N-MchTvN>h!@W3HFYc%U$l%`A2r2>!105Ge26ZYbuxzu}t<4$qP%O zmY9)RoulTxL0&6)D~j?~N|fJA^lZOYWO%n5U1p!qv&Tb@d>)+Xe4on??SAj{`oxb( ze1Q7jtK=v;Bm~Ycz!&+X{e1qtgYnc4*@GRII5zPt*_n#%>r?uG_ZCC5m=iZLkJVwv zl4m0aJbkFKEivc4_&bNN<2Zx#WuvaQ+k7};oa^bj;oO$C@3mE4>Tj(RbIxY(rst4D z6Hm{vuoX0R#q(oo-+AfAQz(p|( z&?O6fve1eB;X_&Ih75Wb8T1G;=uu?QlgOZ_kU<4sSQ9jV2AWSo^T(n2W6=C4G(QZ@ z$DsKkXg&(fbHmB^L5^=x~VwC>h3 z>@vB0H+$;cvaFN~TvzNam#cQmAC+;oTx%tgC04?5B~g7V^lm}d()RtcBqS?<&##wG zT9rI6M4l&_Ugh06ExTs}yUeq><@HBiUn>?_iAW3nz#(}5et7=?{*(RqPez&B2y;8c z`IFVW->P++@t5h`I?zG0{P_gu%-g^dvS4hv$&Ye2UC>uZ?x^}ENo5d0O7 z(JaGz>U~|r88mBFW2UMYXFhwSR)Zx%kz!q3Ks_X2!gkZF&CpGS~s4cs&n+t!xey*)M*A{!`Bl-Eu*cT<{81rSn);6_53WcN$k)On-=4@hVev@Cbz?SEbvpG}Oh9A5*T8HtyIEP^V-^#a#bsjqYH}ZW3 zrvWc{sfR1Yb?{rCS!3v>2eYg>^wLAbIY{B{Jxs zU6bB9Th6m1iFRs?0&`$PRtGjDXYR;0c%}sZ0zS^BjGah3cDMRNR)}4Of7-Jj+iaUU z3rwB#j5tWf@x?-V~?Q{Vupjvwjjjoo#3vB9`bB`U4xO zLC>U>zhfuY*oj;l`^@ckUmzbfJb#{CZ_A`xC3e;dd2*sv9-T;|i<+Tb6S}Ao zU6eu>HK2=<=%Olo_p3NZOk_0nyuIOSp9ZRzR2FOOb{rbH==*oU7pL+UAeUSE&zDQE zlhX+H;elB`Z5GH4h89(RjL12AC2|>kr1@6L{#vC$V6$p`h}vpuud2<9z52HDVtW?* ze1LuXv@f&@KT1?WdhUVjx)fVWeKvo_9F`L|+AiP2#*IaX?Q{KeYxYM2arEaxcp^A6 z;B06Bt=aey;M8-Xh0=oVvBufg8D0u}X|c!!ovIiHU;67QTn zep!humlv91?2{w%*=(%(hf>d;B33=ZS@jR4B&zcph98C|%2VsWQ3<-}ApU-}5C1T{ zz6dS?*gsQ*o8mKq#1#C8Jb0(`iyO{FGtQgz?BahyD{OWnt9S97^zB;p!DLu#Zlo_1 z_v!OS-fI3db9a+{A?4rylkuXG2`g>2f9*S1+p1&r3~!qcYexDm3CcL9b2CL{N=NO( z)34rI(X)^jI0I?q3?zlmrU9Q#(wu{U8~kJHFAC%93v}cz`je>Dyw}LG!^8@DF`Qrb z?_a>rE-@I%dy(xG(YVkT@lVJLd?k$G=?~62_944Fklh-ieiJhKMqNLdN%lKA*~3~9G#$aOeUP|noga{ zXAF%rKMd|Pt^|90u*&CA&5OPe!)A@8=J@elMg~%e6ODa25F51{bdakRnq-%YA zYkP$$`a(@^r>+NZqvtQ|SV^22{@#C=w@$}QI?vU1>^sq4N6=qK;PY1e zI(L|J0hPIqJYJVbWkTgCJlqa)54%-iSmVPmeHOYa&bTtKeAkui;~isns{UagGtS+L z>`rs8AI4u)=-XS{jt%~aJ9#h*J+jcn&}p6W26SqKPN(nfrt*E*G_i&tCMm=3KH{y; z@#n3y?Ib^x9x7)`)|1mG{i46-J_(jxLT1?a6tX6H5&>3hRlm|AA z1Kk$F!>R)!{u$s(>Fb{@PFUBF5C^GrTXeVXcdl&+b5=b0TucAOzI>Q`uGQQgfDAmgD|RY@muWy)yjITILc5qy6Yw^x|Rk{2(>}&P_RIG6S6TqwCk0-%08-N9Hy( z5l^j=r!ON~L1S9z13fYx<;yx4kMIriT_S0CChC5~hj%Nm=(zYp7ATHS&)2#-mn3ru zWKObZE{V)4>7p;~L*yFb)g0p|St+pr>W59XKh`=QH^uYD{xb8>drb_U3H+}bFTq_f{Baq0H;hfyijG?XLOfi% zaxS!hS6Pc*oRDKb>*H$`_LghrceaZzG;@v}x(2$borzvX7R2-g0?47+{+X-paetF< z1kcOdCrv+RyEk%HZ^nqEdH;{f6XXo`UB5q0d|x9xemWd&_yDh)mOt^qqwHD21Ms1N zRqeP$$|29frd-0@tnpa%6Z{U&C*CXjZ>z6$-^}kecs*b4v|qOzdtOHL zMBopa$D7EsFzZ!Vulc7uyJRK%3OCsz8rPA_uI+D;5qQtWFR>H9(r>3vI8W9nHn9zR9#Xi~GX*JCX8! z^D>;tb7hT}S3OC|4#sVp9``+ddyRR`AQ#lO7&u)o_%VR5FC{7d~odYPI%DAruB*?^3pL2Uf*n&_PxXh^D=zVRdqP<^EEZc^U-SA>wwx?EcSDW8vj`;0a>w0&)-TYSnC^ppCYlgQvazF89 zv-cbRc5<@RoLR7IE}~&!KUIhcH}YgKWB6*N9Cq`U>6)!+g#V65g7s1jT_p5_;0XxEww2O4OMrjU5g*v@RaH?<;ev4W;XYmRae~z9@vZRDb_{mQ#L*$bkPS4 zp0F?tz164iiClr-3a|$Jm55}-<(_JA=EL0`d^!gv)Q(6!*XoX|&f~QC_wj_TqttUt zSeKqHk;r7G?X3R_OloJTEulC);Gd1feZKbEy6!4$v~pzJ9I5encso4Y*I(iDf>%%X z3(Mh&_?Vt$U~}e(_cC_UK7a1YALYooF!@h^mR$m{>Do&Xi>o^6o#^)_ajI%p1^t|n z_cJ*$G5|Z$^M9z^z)y<2_hg6IwcJZ!|I(nbOz$*SfPck3-@01lBYSH0*ai8i^0qkU zuUU5XKgZm3onW_1;IEzDZ?G5E{ln?8e8D`leTscYC;mLOL5qC{ljvYChT)yhj#L&e zgdVyV(>Ppw_e5j`XN5x(v0N?6bRM*MsIC$pYcqFE^t=t+*h^2>fkm*MBioIPe%CZV zv=7wd>o3RGuVWDR;cDDx%;~W8oiVZG5E!_F*C}kbc-5_lj<hhVtdW)lPZK z*stIRjl*kzC$ZhJtz+XUKojThVDdOjAC7GTpjs`i0+ z)H=f+Ch+llbTe@m;1U~`HAM^~Hcet`()iZsuX*R%nCnX4m`LAXTD(23;#F}KNn6e9 zdK1J|AE8C6nj#}!^S3#(dQ%e(XN1I%8Po&%QItd zN2j!;f<18q<KWgdR|@Hb}0*gx@|3Mmd8X#q!l=C{h5fQRAniNd~ORe!0u z*Cg_J-(GGaeo(t+Wwyk3rvi1gryz=n_(UoOQ^Z{JE^Vwgu(8T5Qi|!rmfA7o29n zZ5AA7!F3j#XTg0I8W?+6@MAr8iuj%o@jW5pdqTwbgoy765#JLcz9&R{PY9oPi1?n+ z;fWk^4Wr0h?vqL4WKLcr-XUoXb7(q*{EEASoP+Gb4|Iw>;3`sG1J97ownplvWVr5J zX(iTmbmAP+Hqr`-ai3!dpF_K={hJhmGS>JQ&o5S5 z%`d(fYdq@br{>1o!#ux*uTuTA_xky}t(cqPS=JySF?Gwgzk-O7>-o-fm{_~H>zlrCB$B*#jJohE}%EXJDN4&s!L~Z0A;xCrU zyodt}pvQJiB3q8{iAK`VSZ=kQIO!1IxGWXeY{cLC_UrLIJK=SWDM~~qLy781Y@>Ew zZ+VI7$CgjA29~+RKM(#oe@Rr!|BCuLhNh;?$u`~;agEgS(#J)Sd3Ko(L%Dxfn;6vH z%73gG4>luP)3&!Sp0t@q_M6xU+9&V5z)pK+$4mJ;@E;5{=-xtQA~DQ+T-LV6w>>6@ zvWxJKAC>%h6|{jn)g=O)-Y!-OB(M)owU> z5I^f7;&;5YZOJX*x!O%kJ_cmZWPi?=}*j@wH*E4--d{)HsqMiKShst#IVf%8{{71D z`+b1N?BC7u=vE2L+DQ420;hjAy+ZW?Ynopd6K0lj!Pw$v7>wW$_2Cu}yLHl%k zbI9;Y*sT*xCO(bRxDJ8q5pb<`%;Nt4%ZF9pTa_`-bLjh@xbwBv^k>N&H)ilX8#u$@ zzr(^WW?)D&k4aRA(k<2<_i&3XQPJH3Zl8mP3vhAvZkfB(Cy|H#SP7mLx3~JZEdxfI zv1j6TasQ`$7!^Kk>;0Y$9`Ppmb5TE=V7H6R#-3swQ*Y7M4_n-h+;Io*(s>Slb%Jpg zXbj2W9I+lHl6g$;1-@!$VkOvu%puZeDPEq+CkFHRA>wpmDL6;Z+u`khsyiRrpv$5w zknQtk<&5m8^7ZJ8%wq?5N*tP!$%qY(dG8K35ofCXZz9*hQvX+h{}g->n$;3knyCEJ zcHex0DIPKgoW+-4~2I#wv3KeoqNX>Ak7zJwSWDX+Fet>}?|M7Frm8cXAok^N1qAFG`E zq!lY}lj|gQVc_@wO1>sh%X#$*bmZff2cO30TkPl5u1iG=G5V*_ofCGh^iArjF4|?) zxQxZEhxU}p>c@HSlFrqVKPS4a#VwEgksSDIAtGmJ;;Y0i>3M^WgOA13YcaTwnYgUw zJl|`@=4*TKiJl(Ls{Oez?7{Rh_WVc1wI^>{9vihM|CCdtz)gkOzcn!JT@{e zK81$&-YXGg>V)DZH_Zo$vD=N#;N43ZtGRZU#s^xi&R6Awsn1+e{MPYwZ0*Ox(ad_= z{CWxOBs~k!bE@A=Nae{-&uXq8w52%zU;TWA%SAVOvJ{D9=~vdb@RXI?4% z4b5^tu>$Q}^dFh{Vg7pRJTj4Rrqs`6U_ab4tJ(SrV`dyH7*o%aDl(VKe(2T=9W@R& zBu8qIBlyfp@WCA*HsMfq?});h27V70iImtnXLDaGlLmdq2iRLo>)jM`H^IBs0=*lQ zvrf>V9#5pfudbnP>UKdty&I@^%RAuT@@TtMIC?fw;!gMBoOpi$E@A?JA%LqsvdXMY zr9CnAwuhT#GjQ_;el{20$;>Y2E;o^hjGglNCJAmUiS2tFo(TK!y~Mm*?(=x#_52b@ z7H#(4myk32HKmi{GnJmztUh>+Lwu8SlSHOX<8Y0PxCndm&+VEjrH|pqWQ$E$Nwc8~ zcV}KvdNhGk_`&c3_gf5Kupd+!DGW+iosYs&JGhyc*kjxei8UxLzw}#Oi+VS~LN>f= zzfdF_-UJ43Ei{g)EI2PG5!UdYQ_T5IaGcc47iGzG6x3zoY+;M^z@N%!!%x)yjClW#rBYBX`Q(e*Jd#T~v)8m;$cg~ItUdy@B zM{U(}dUySECF#kT`!CsjIO*F1UbxwUUM4Y#)XZOQ) zSx7`_gJdsH4NueNm-)#mdoFJ$@#(R|hd-c2(0}l|n1-rPRF~-ZswWht#NmvIH^t6J z&lmjoiEhWu`E4PI>E7 z%QYXlE!;Z_*1g(3fgCNAdpbg8_fLpJSN?pTOrA$W#(g;Z66?5`eZ;%u*kX-|t9a}f z@7h?(J>-wtH9nnmk9r7R#{V@Cu71^ix4icLW$cGCw0V;sokDP=a{GUh0+z+n8^gZTJjY)QYUt@wmS@))<@=9x_{C> zJlnUF_~2Z$B1KiXeEdDYrteA#$}~TA=zyJw5QZEXwp?p}HguA%H|d%A!21aIVH?jqL4g|>$)aa-J8 z_@4JQ2p8_;IPzjSayu4XE|b-y)%1f*$KS#3&54#HAKx^4lDWXe+YboQ*Y~V&CQ}vL z)hF)1o#lp4>Y5tKR;-J&ru5yG7fEIXeWfpCe)bp6muq-8Id>I#^W1fuC2@vV0y%_@SI{kiJks~f3H#fE+!)So$3|A0@Kt{w zspm~98?>&%0c@s;iNxVaOY>Q?=y7O^?hDpU8hlS;>girfX&dN4)p^Sz z^M##EgZj$QJ?uf+;Ca>2Utw=BY3m+=F|egQU30EnZR_6zc~fdq8dsHu4;1h5>SZ>* z+}d(!U7V7&=-K$NuFGE%S7l`Bt~5U0BiTT%6+h3TKYB@3`uvk#9&Oh3z`X=~8t1TY zJY?f%^w+m_r`E-<@W79b7 z=K?v&KFd~HT5X?pD{bJT4ZMW5!sgMohu~Y){eeun+49zIHTAf!W%hme^s+I|$G`M} zaC0$lAy2U#9quZp$4=#Q<0{*br~Tjq{%IM@e{Dyj&hy`q2kIN#1}}zXh3ml!UtpVg z_PI>Ue$AV<*L<73*gjL+yb(Fa`&P_+3THa)GAR3zb-RqLdsE^bKd5}EzJ*`<9*_~Y zTt3^7l4szD(H!~`ewd1?&iPZ{20hDqaO6hT9sZq5b^qeXswK=NS&wYENabX-kF(U4 z#9R6HTt2@hNADN5f*;=T#7N87m$V2rH~z?Wvp&$pHhXmDH)Zz)deBMBn4v=rw2uw0 z(ELNG-=h=1I~?dlV+;GZvE;k8kJDFt*U&T$wVu*lbrkE{#Rm6%aV-sLU{x5yK5s9k z9Nz{(J>O+NGBv%A3EIDeeg#NytG2>$HlAT`tgv&&!(J&%%r>^b{_LViLQ=j>HygjO z!t)lfkD71G2sC%>3a4EXhe!|I!K{bvGue`Aw+8m)>9!&7pknRs&jz-k%C6e2``IZ70})r?z=z}umyQH&(?82Ngdrsu$Qx5WS^pYUEKpKtSgwi?#~MJw{|iVO!OPj zmLQ0677*(2rGU~de4;n&9I zJd(H-IdG92Gk&Q5z~)nWvJXJs^;?Ua<2GkyQpY;eyG|9!K6LD`yhVJv`Hg-<=AkS4 zH04}pf$n9I80U7Q=%7CS9;50W&ajbR znx-SXIUa9EEOkuImjl>4lSZ~H%z6Cj-}Z49jHi8bcQ*SoKR?ly2A-LD`g@1&w|$&> z=YZ|j74S;A@5|SBKfRG}fdBh0tY}labEQ={XB{U*0i0j%@)CTf|6O%46rs12gqtlj^}+CEhfG9^47to@Ts5=)xbN z3!lwt?8-0lTntUM$p!uxra%0rybsO7S~Iq*!WVbqmaZ+{ABg_j>FVE#IfahruOzDc zTvQM3^l!vez!$0q-^za+UD=Vt#+is8j#KXEcg6nQ`53s<%jC-+H?!Jd%F%% znkdcx(vKZWGG=|`-_c*_oJPuh%K@+9THErf;&AYy8^rogSRy~eQzc{8Ss{&-)D%; z_h8HC?|d6x0(dKY8wmU8qB7;zWQ_HbwhY{lIX(`iY@Kfb59ioB>H7`N57-Y}_$}R| ztVqkP{=VoXcu;MuHTd1tZ%YhnZN;utqBh`*#>c4fyT=DKM!PW2E6zMmN9WniJTGUS z+l%IzW1hqn$af8YsQt(L*N{)%oF8X=k0(OI#A!jhRmgjfCoc8jB%Z-tzeZLZ2N#mkr_Pd=&#q_ta7L0A4=< zZw36I{dl;Y!Vl}UHGKD2%A6C?jvbnLm#5+1MUkJNlc%I2sWzT~zXP7_m(liO#tyTG zuaA6-`@2pw-t-3j+@C#|e>JSQz1E+r<`qBBlOL-+YN-sHhdrb=fsq**2m7pG8_b12 z68OK>_I2P*g9r)P9A2E$#?t-@!c>k&p6@ zNB&MnG-Ep<&ZI)TizLLmNJ6}eB*eQ&LcEJ4#JfmByo)5nyGTM_Y|4tz6Ls33&>yxA z`*-h-u=XF1D!a!90>08a2l)Tke=ey#X4;4ZbJSGNM}t)RRCt2lLA~O9g+ItYJq=(A z^54i0!#gc{&(Ywo`0VgR(fI&6s2`ev%j09f-XFw;DbBTwH;Cd3GG~@ddO7u;rk?&C ziV)A+$HLL}ZQ4#_%fiHzovA-PXZc;_URmZ3YWtsrXE2ywn0UA|!TP&kDK7g@!m;=F z!Erjy{*&YNpFZAyvcH|$-~Y=nW=`*OQRK(6*ymrhg~W+*|I@NAMD8hfPI13-A@?h* zxnEhu{mQ7hUzw5kuICYxOK}~Px)rIVHe5-Z>}VbDhcJ3X?_LISw6MRz9^-F&cs2p% z48A_GVvF$&ne}seeeJKXUndrs`VL;&wWgX7&he>g)`#EItFo;-!gl7y<=90 zhxF>|osA4S%62ov35ET+6C;}o`U?8cy8|b(?a(#WAFOeS3^mO0+gv6?sf@%L*(2n( zmz7|z#2UeGEuoBs+4pY|l-@QCT7VapF54lZP~ZbK)>( zKVw2%r|RilllX5l>_&#_$_EpYqg>-r>Z3!V=SAq93GY1=ZP?ExuJY0#`#<7_np^LT zxqn0aU`|Hr^xdg9;e%+F;R*FSbACP1#&6;82_(3Ssk~xh@X@P`O&r;SSzzH^Ko4P~ zdweiSOlslo;~Cy3GMEG241U!+h~4h%kMB#!YT1h{-p=1-;BMN!7AU!sI~Z;A^uE}B zb0H^efz}LcIC8DK1guR^^WZ#+OBV0B$I0>ip5Su z;eX~-xJwbdr{9~G#xAqpH+O`I1kbejdywM$VsmuxOz@t)?UTXZjzphR@EPuAO&Z?Q zdc<&tNl1e}o!koFUe)^}UTkKf(d)0au^f9R7P*`jE0Pt_Bxe*?;3L(~=O%eyq45DE zR*1!K#^JDo&ri&H5K|BTmHYGjt@`eJ;&$}2ebQ2UDmtmpQ@kf{8n%P^h5q;{_6Y$a&qqAZkjiy!H1z+tnnB5 z76V5g^W>!R9p|Zc(|+Q=mr?Ea;<~}OUp@VvLBxCCzLrA=Nkpdb-(`d|3Ha_%mOB^O zgv0tsPU2T|MB~b%;=SKG##vV-P;1Qhv_Jl|{(^Pagnh{xW{*^Of6DosSw~sZ#j*G| zqI*WsJvnsGz33kPz83p_#?NGA9{Od#f4`>Q`O&i&l}XgeQa4MREN!u8r$UF&6IK4* zwibykZh?O*s$+Hwf0F?Yhi}82?Y08_{=pJs>n3vQ)A)71!{Y&`O@5l+!Lz>KD`c%32R}=pM{ulZ( zOWQuh9d*oa6T^2Nx}zKomX|*E?n*0WH2sbR{atwXwca;S`G2POQ11eMeTPDz@3b!a z{N|YQQywk#9_MWTUGeeiQfQ{{ zLtPWi%a2oxLyU8+Ss&iJHP=*GvfmA3hkoDCDl=C6jJ-n{`WhXic9Dn6jOjB5Erh-3 zZ`m`ee@4r+&jPNF5XbbOH&=8<05kWi=IPm@&Rfq6SSR*DosaQF>EFf}+VeWFlt*JW zG`3kc1`qVb`Z~fo^X92^nTb#MKAAE7s(dqjqUZ1jn?4n{@Wc$927FnKzC~}KV|D$2 zSJiR;IySK)=&|~UZ9`kmXgxWrGCt-k$G;h8|G(B1**Qp*U9O&3CVa1kH#EK^Zr~5v z=vwL!T{}nu>TXwGw5jXyO5q-0BHCu;pYnELo)5x*YBK;M@EM%4-t`U}y3^3rtkp;6-d!V{e3~c`+>>FFNqa(4N3Q22W=Hin-`{o|oE;t$#!1Fg)pvtMk(F=ExmB z9dvK1@^A#WlY{y<7I=3cf4}=gRsD)vh_|0I_3$GZT|Y|yiaQTKYJ+ROjj!?qIMw%f zA(N3SUOD;_S;BlLa^ND*-!m;<7eSfwbA{qc`DNNZDy*O4dYAr=dM~k$*Y{tlpJBFd zYd_`3ORKC6&h>x79KJ@~`-;k_Z{~Tw>El=`FUwPU-W~Sy)vo@P;gifLn;&f2zC+&c zKK)DJ<~OqEF`c*a&m{A1X5PQ^UAuBj-&GRD{|xT*eJ{rh47`WIb<{S?Z{Pg}v8x(G z5x`z{Q1QQ)_x~+7b?a%z8Mb-8h-cpIW_a2)_2#=UfZ-kCo&Ul9VFC3tuJ3N@=srSY zG_hB9^8!kKW5eY4qEC6(D7Zv4!9O*oPnFk}Y)3E?1M9;Y;kiMMnR4zS4*rSX2j?Gd zd8$@A_^rM&EpyO6PjQBz@#{wSITp6b4%te+=FRc%1lV%NZksv2LUL%i8kF9gS9x=Gream;1H(BphGIUSCf9J|ldjT<|>ie!Io<;Mk zWn{#TEZrR2RfD{a*}R9pYM~5`tOHj{+XBt1#^c1AD4i;H>3i2aTCz`tKGrlnYnY4P zA5&VYE^=7!F6%vse0lr(g1vQ>Mh5Wrpbc+ieVP^CO~Bv1lZPgX-zlT|264w2ad@(Q z22YL~xR=0_Yo>W}v9wR~WP`LD+*z^|xTD}$@9jNUSB@W0c-QZl@%+#XJg=C>vt#h= zohuIAv&^SUwN%+@0=6crqsITvy;yD=>EEAng&0s2?8u(?z!zcerpGhOIg<*>BJ@iw zXHzNqK#qKP(DOgz!h$j^Ly>t192A+=#s?BAlzDm$y8J?{a^dheX!Z~t*$u))`z#qXq> zVM}Bt*#{tdn{w=#b7JDu2Q#b*&vulL)_f{I%W^OD<;;aa{=V3!xZhyc`*W7_=i%po zrOuu!%T|$Q!p(aB!Kq%`?L=$39rQ*;b&J@iym!}LChJmh;=9~x*Vg!{aB?X;_P^V^ z`rxRp>we$UTC62(78&AoY)5a|53P|5izMvDRjP-Oshr@c5};A0ZX*dv7V!rgz#dPl z2`d6bO*KYIoJ47pj)WlK7)!#&AJe+2j6X^fH$$hHj%S)bL~K%~^-P(JGsDzw)Ze-9 zz1?^A?(WlC2GfK$Bkk@x_k7%Q&pr3tbMLz^mhiQ$0q;{!Ncxd)_yOXdSs_M40~LNh?sth!=ps54k)Jds>DMh4zbV6*v?!GD{rWQl1lQkR z0h=Q`))Bj!I@acL=HpiXpm>JLQ(1RROJkHD@ybI_ip8|o9x;Y&?^D;qJE8xX*9Oo| zp+@S9*hd6DG>^1sQq23`z=Jase`eFpjhW`=PQdr;$Thu^F#MsN^ei~(q84LnC7uue z4c7W8cBAhGwx6> z+>MhR*R7ZbKCOvzb#xBH``DWx4>S8Q9I>}oo=?O|YK5V*X6lZp%k+(QPc{A9|)~z@T2YSG}dXH}18g6W8(3%<=B8^Ly zXiXmn&m~Ku=o8xGqv9yuchiD6I}oE?c9Fy@u)P>ga-nndBKZ9(>^CO#HDt#z+>7EB zo~U{&eq-;5xSl@C@jM*L(0Bh?F;^B!(7Oai#rRSDeTU?xduhC5q%yq%{Sx_b*o~zF z=s~PN&TNBhUy>d3A>@V9QZJN`#vDpiU~HiGsFJ@WT~UrYJ3jsh?bb=jW5nbIyS;)`z@Po{{t=Beh(V%i|AjbZL?2h`WL;^6iD){D>`>N z0x+5^Q_;$6dZCiE@BIv>(EGLC)3EqZvm3UTIoy%O{zb~kLWs$6EvPst~?y5$FoU$F{ig|J!CAC zV@)`|1?9D-&cz|SZ*I2wCiX?Z2GBP9Lb7dRzC829!< z-mtM3GSRr5&HrZeFw;{viTyIptPN=5W6%}$S5N5a{iz4!*eLYHwx6xc%)dyNctTX> zU?``O%H&iI*!m+Iv*NdG4C2HKR+(%p@^L#K>9gnOx_!&OtaRQ-HYY;$wBCiacFCXYA7|Of^~kwqU;Rdm zO{cXZHCC=Y(Rdoq*wkX@5$&U)80ih{E3Zg*!yZ?FE}jFy=UdQmMH+2upQ&cefw8Zh z&h3OhLvg85ZV)G+jSa^C^vqOn$(=o6F=(UDidK2Eet1^O_? zIh~v9gN>F?;@r7|v3NJuethuJ`x+~PccXG;leO_{Q054HC(d=i{#Tm6Ef&{I zTDfx_Yl?U-4bS`EB7JQY=I|PFE!yN5^oG33q}7%y8YynF&bBUlocuccr>p~&hfXI+ z9+a1577HC^bXJCF5S0 z=h4A-euh3q_CxI*MK1rnB5hvm+_-l^hwR}T*%bV%o4=zI@{vxz$*i9tcA|5@E2WP` z9?0K|XZ2_d3`9bBb~v1v0M&q8FQ8&^ZrNN2&-#p0S-jJ8EK`=^9BMPCY;y>cb%Mlj(!|uFzD01HsR7=| zjt0jCoe>*~4+5tP_1FfvC@(P%96ICbNaWkLz0H^QP2yFYWdw^wjc*cfiz+;~Nb`l8 z#A~9e9p4l5{hX+Z<9iZpMd;~JaDxc7e-Y2$;u&232F)i{K26UghXSubMx1MG(6hN4 z5vOz58Bn|OX%&%@s7$TnV zCBukgZ(G*_A8qz8MqG#I;e%n!uOHH)$TyV-e++$3XyyHHU@mqZc_rkNf|FWh|Mj^4 z7W&-1!iV=pqy3M!#PP&wV5RV3&wn|dk(Fzin3GlpDF@Yta(jm&AoDclH$hstPzN<&koSOo0$Kc5b5$JF*-3IMt?mZw!VwK zgomfJi`}tEItKkv4&}Ki{o;B$YX^DG3BU$p`o&OPjXl21R4o@R$cX`lzPcQ9;=>1L z>X!=iw;JpXs3HF#(t1-g{)oV)AFOpEmiO;QKKl^L)7d=rA`;&U-Ht8O-p6aWF(%^d z%!5?tQ>!&$VjgJvHF4@rMGhatx6bjK8f+G>HAbZJ&qU>&pa#|#OP{` zgBK8UQEXtrHT@QmhDGAe|Lz?Czk`nhkKTtXE}R?CpSuS1e*hY>Rdb6@ufjXb#WnCw zRGxUy7-7*lD@Z1iCF&oPd3!1o^+)whvM>4~#Mj|TF;qAa5hm7juCwJL7|BKakhi2f zsqELZ=JClqG$;=IDfAwc=b^4o1Z}-j9ioWis2xOFYpP8|QHOElwS9u#YeDjb;(e%7 z2+u6y*>AkVB4k9-cj%oMfT3NZX)8YVS{oqWL4JqU2T0E4w%?(8J8K<2nP;eqyo$3p zU_&(&U-i$;$oQP@`Ed`nh&cMh!CRpNI)8$6adA&T>dX0b+KfJ}K0P>0=YbjVUOQiW z1pD~ci$}yEod2~!7|5$ppY~CG%rQ}Ek}nuK-YnV@)xy87JV<9f;G8Gu8~Xu{?-A?e zzKU?*teDT3N&QS*I5ZGIKcjI^r2VkT{dMBPTLU5gM!APi^JCA3|B(0;+A=nB9=avH z*Q|$_8T+wa)HcjOz69BipxDrSt23A9>MEV&u< zzk=V$zu$;`jOdHeU_bia3gm~ERZ-s#w=L66JR{+}YdY+|R#CdxJ`mIg)`ol8c zTy8`(nS1XvwQ~O|QQ27uI~&(3{h!v%jxzXwmC$!sGsJCceC?lx-Uqd)(W=#gXQi_`{ZWBP=?7PBl7MgM%g zj_iBAn8}p2%B|NO+xz>uG|rDPMb_?~mv#qt>yM6^f^rzh^WYq*L)gP{S}YZ_z8QFq zP<%heoj&xB!}#pNa6|9R=n}2Ay|*APpIJ?E>`Q2rtAyR54~VCTZk*0*J1k%~RKviPf-!uyI%z{=t;AYY~c6Vy*om)-QeV7u6!3Y%!pJ z#E1rR#$h}UUT*ks4hZ6^Am#`0MdBIotPK7mba)N?(cQAYFShL~7TBzdbQ$hP^cebk z6m#8+d-SFX+NWqhiy7T^LbttuhxCR_pWch-eXY0x{YKwn=j7Zt(Kup>McPX4YnMjs zc7RV3ABPeUD~==f^F_?&GI0v}Jq3N$CI+D=ysI;Q6u!9Z%b^7M-B4W={ej-mUTa|N zABxE_o^oD9Gc>XueOHT^cnUv^=U~d=$EvY11zUrDLJ40SzI5oYs2rm85Y%s^8+{3J zebCHV$0;2DQC)v!*EKQ}k$fW~oAnRkBgj9{I#fRYVEK)!{-Kb6=$V?ofB1JQI}iV0 zmGkxwL85mF{KKK2hrN%h4Fz}#3}TtdZL}ve!Ci8Ow`mpjWlq02&57}>CCZ=6lm`)3 zK3kURk(T^uTx$d_-VxPA@j@7S5r~7DMQ_3v=n*uwRf{9ZavB41??N;7o*H*!K7=^j zH~_yHk^DpO?<9jIOBtSPv*^FF#3vdthKuwyO?W>N&R{`I9>BRe2A&;797y{LsznR< zb_;R44!-^1+l4b)H1I`Ch-Y97+@rZ#B{t#2FjmVmS26y`Hp2c8H($JK3)P11w@wVq zm37hn9vOR_eXk4qgRQ+rq(j7N14tV;H9$7fzrGx2bwUQbdRmmD-p!&XQI2>5dma#* z?o0Y0Z#1wMGFW;CQ#@8@@v!oLdgK@2aTYwv?Y5*`%qxiF=qzNCHHP_9UcMMlO++c> zm`68@R`7*haE6J+7j2E0kn-Y~^T)%1GOTTO*=;W5-f!$ro_Z4Y84&z^nWvG9X#^~R zd1W2euHsm`!n-M_aMn83ou;R1{YTLcKp!ziEO1n`A?C;Uj;GUml9a3VLwC{i80I=- z*bG7!^d|TGoR|!rpywZ_C!?dY*sCy$bF*ep>+YTR*m+x8yYk_gi^VuAyxjN_Vu(Tf z-VELDL`;wpu@jh|Ce|a*(4xzHIA&USM~#jLsAX3p&F}3v{49n0@Qm9Ygs;s+sQZO^F`NA3DE=`N5zD9Z1@tj_sPh zxKg}}=f)^6rThC*0vYY7@ih@{AH=g&_h=cK2GPVDp))l1+cbxY(8PHL{sSpdhtnY9 z$KWrBT8c8mAG3RpX=KLnHXqU-CuEgskS8k0(`ogX9Uiy9TaOfNOax4C- z@n?R5;Bzx>IIU$l@bqehCdy~w!xGN%O;pgqe{2om?_vDm5;E`s>MEj|!NEQQ-_%f? z{}vDUeGj95ndl(6{dv#9wCN7liNjpg$ntWenc{Q-}Yogcq*=;@>MSf5Zb`XTceN zw|>0p-{yh;goGEC|BMHGaTC>#>jxo8Mn9tx&ihY?OMmCrtN4Zsez>K${-!4N(@Wc_f5Uk*OM zoBid$;~w~J63+GS*5CV|C;o-~Z}N|e!^M3BFB9(Z(~mojd`*8+ynfRbyr6#H^nhQK z@WS#h`-0?OkpA~1oa@JM$$z?2){o)4;k&zv)1Q{`!t_VG3BNG>T?sF2f58GVp`(90 zW@Pfa?U&+4Tnz56AH|lr_F>|VL!V+i#lO4YoxPGi?(oaOx9>9kZuwr6_=Wp#T_4fs za07Q7`Y%d2w+{-g9r&hx!sq_04WXn1AO6eY^0z-u@SOH@(GMP!_yz0N>H!}eA$;yX zW^l)mul4H$FFZcn@(qHQi9-Ez=9^MJl;M{{f727i;SYMiU$Wq={%-&Cj)Zgl9dqgL ziEl~%4BvpLj(l9|?62Q3IMa{2|7117zx|HshgTlIocdq1?5iML0oXSj{HyT%8YoRa z+bEf5qRi*#?)Ir0Rolk^lEeRJ z63+XVo4-YW53|373*Uklw!fzD7Vke6zE}Gch98skz1q(U&g<`P|G;07{Dt9`{G7h3 zzry-SOZv7JT^~;SoVVai|8Ds#6s{lD{<;0Q@qK?y^(zdo^MJcVW?voot()9Fleo#@ zTt9{f+%11T`YBY&p}$4SZX+N6+CMnlfQ(!|FaN66lH)HF&c_#3eoy;l?N_>e zaR1k)=*O#mrTLdU@_ES5!ORQZ>pe~U7guMIaH`X?oQKE4_*xP{N*Dt|9Hw=Z}5B!Bo} z)X$RA{okpdeUG(|DnD=E4*++wmftTk|4UszFZy2fUnqQ`{4X{BLg`=W`thn?NpN@n zPs#qz{f8TFE2bp+AE5tw(f4SdYK9+2Y2N>N{Z#q9;DyJ}H>LhK{Jd*?3qDQ$mHS_} zeBYZ?{jXd9##C{*r5HYbb}IGf@)-)8*Wb;*-Qo`nemVN>kZ>>h+`qf&bE?^24*laE z^*6+h9loqczzB1cIl_E{aCeMC|uRym7>2okN)w52i&R|A0OQF zGYiiBM=AdEgW!Al$I{_^d{*W6s{f^K|0`WS-hWj6d%=I#^>?MW?^XG7EwnWMepP>0 z^_P9VRUJQbwUdpnRfu(`H2d=!pS|Fhcl=ADipA=u!ukBN&CJE;@D2rT>D2w7^XK>} z1)syy3fyyk$>wir{K4suK+sR%kDiaVFAsd)zUusN!SiDe{?#+?=UdVcn#<4SO95U? z|Fa4{*UvEz{1-80*rZzyoyzCi`v%=AzEbM(*phgv^Q zKac$|e3d_!zjS=A|1nj5R=*LY{v4m+IrV4wDGz)vxEFm@zs*d4rU(B#aE6~O9iP=- zwLjkeYWr3*{51G;{c``M>W|xpT0ag?DE0G_Z=rB*KO{e|pBH`JzH0k${gmE6y!bB^ z&g-w*2ZyWmTPXj9;&b`_zsujQ*#92Z|JDA%`;VdEd+C?!uM_y({<(dp75W^m%I9VO zUhvZ7C;b(Z-&ExH;_szjFZ?TA{?g)0L(yN3{a}5M>z~6@z%K?*D{$O!T?W4M_>a>c zRp2gU`)e`$JpN4~!12A>pTkx9Tz)V8^egq_{z0YB+gBZbu5|en_2;zzM-FHHIjFRc zSN~rq+^hY);0xuyP<(Fx>iAvF^sA09UjErjzG@cVcB1{bf8zb4Lup^WzThRF3ipza z>xa&d*<{wVHn{v58h z4~MJ%liQc-ALp3;srJd~w?n?1_F?!*MSf5I%sz1hZZ3TW?^Nh>`OYiwJxo5+1%GK? z>$5<}xp@0{*$3}mZv7{pozMOXUcU>Fh3lUSXW*wJpYJ5mFI@k?3-jU6>C-QVz6!DK z~sNtDv z%P%JW=hUAS@PZp2_#ZVK;uWpmrG~rfKXY0754SH7FE>cB_m)xOn?|3c%#Qsh%5x9&Ld zTfeS!`MCYGLuf8Pucc8koYPMM|MJ@B9J3Fv{_9mgh@a7q@6>$!59e>X@QGILW#C3J z{ZskU;Cop8)%*cb%w8tG7Nc)?zyU6b?**s$y%>M<62raZ`)!9C9{TmF|3dLUYWgaF z?!P)wpqPES=a)n)-&G8s%3p=E{eg-KbM(XfTOR#0xL?sPZ{L0e&gT!Rf8zM2g3t5w zDu15;&m%u5@{8BMOt|M4Dt(Sizc@aZzfFPj^*=*_<3`TK>GS%>QI^BGehmfA^`qkF zFn8e(S>rQnf0aJxUkZJdvvr5pkH>#*`iG@_%}hS`_&oaReBujL3eNv3JXZr(^;436 z%~fAc|5d=hHSlc@c0GK*@o4YPUE6m&Y}Bt=wPsb)i1+W_o!WKR>ecse-`)R6_o{6> z9$LL^$HP5cxA*PXd4KO}iPY2k=tuZ|vK)t#{pRV$=4AAL!kAm$7M6>wNh=_~4@tadyc&lYMv8 zCmOo@wyo`Lxbu!RU5P|bVr}=jhGepFO;1DPx`uU)>v|Kn39;(ld)haz>gwL!_h8rk zyTq!Uz4vc_xNir(?C9y*-6dA__J5&oXV*i$U+C}Inf)bJ?e2YSH~x3;0+Lno{vEg> ze?y$^NA4%84fSGGiV&fshk74IfnEK(cJJKX)vX#$fjd~P6f1?GV+lnE-VEA-8#G2b zmmgo~H;v19L%J-NLb+5k9CFm4oLAHAyGzMOLF@SDmJX!=$ie?t8Hs9gvjBtKov_#+w0 zfH?e`jz9K#yUeTZ?nW7+M{lMZ!F%&YP>$X$1=S&z&Fmbol-sYA?zN literal 0 HcmV?d00001 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 263db9dba45f02efce0d648ff4d3398a6674cbba..846cadffc0d7346559b0e83409ac6e768b3a38a5 100755 GIT binary patch delta 27951 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.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`)。