diff --git a/patch_v2/meow_svcd/S89meow_svcd b/patch_v2/meow_svcd/S89meow_svcd index 7e7e42b..9738e07 100755 --- a/patch_v2/meow_svcd/S89meow_svcd +++ b/patch_v2/meow_svcd/S89meow_svcd @@ -1,16 +1,19 @@ #!/bin/sh # -# S89meow_svcd - MeowOS 服务管理器开机自启 + 主机名应用 + 板载服务预置 +# S89meow_svcd - MeowOS 服务管理器协同启动 + 主机名应用 + 板载服务预置 # -# 位置:S84ssh 之后、S90meow_bootmenu 之前(S80 < S84 < S89 < S90)。 +# 位置:S70meow_sysui 之后、S90meow_bootmenu 之前(S70 < 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)+ 应用主机名。 +# * 本脚本只装「稳定框架/KB级设置」:主机名 + 板载服务配置 +# (/usr/data/meow/services.d/,Data 区,首启预置 mdnsd.conf)。 +# * **svcd 拉起权**:若 meow_sysui(系统进程总监)已运行 → svcd 由 sysui 监管拉起 +# (sysui.d/svcd.conf),本脚本仅在其已运行时 `meow_svcd reload` 让 svcd 补载 mdnsd.conf, +# 不重复拉起、不重挂(避免与 sysui 双重监护 svcd)。仅当 **sysui 未运行**时才回退到 +# 现网的「重挂循环」自监护(旧固件/无 sysui 场景,向后兼容)。 # -# 红线:不 killall、不碰播放器信号、不刷机。svcd 以 -f 前台被本脚本重挂循环拉起 -#(失败降级不死——若 svcd 死,其所托管服务已是独立子进程、被 init 收养继续运行); +# 红线:不 killall、不碰播放器信号、不刷机。svcd 以 -f 前台被 sysui(或重挂循环)拉起; # 服务本身(mdnsd)由 svcd 按 restart=on-fail 托管。 # case "$1" in @@ -54,9 +57,26 @@ EOF echo -n "skip mdnsd.conf (/usr/data/meow not writable) " fi - # 3) 拉起 meow_svcd(-f 前台,重挂循环防死;/tmp/meow_svcd.stop 标记防停循环) + # 3) svcd 拉起权判归:sysui 运行 → 交给 sysui 监管;否则回退重挂循环。 rm -f /tmp/meow_svcd.stop - if [ -x /usr/bin/meow_svcd ]; then + sysui_alive=0 + if [ -x /usr/bin/meow_sysui ] && ps | grep -q "[m]eow_sysui"; then + sysui_alive=1 + fi + if [ "$sysui_alive" = "1" ]; then + # svcd 由 meow_sysui 监管拉起;此处仅补载刚预置的 mdnsd.conf(若 svcd 已在跑) + 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 + /usr/bin/meow_svcd reload 2>/dev/null + echo -n "supervised by sysui (svcd running, reload) " + else + # svcd 未在跑:交给 sysui 按兜底(有限次重试)拉起,此处不干预,防双重拉起 + echo -n "defer to meow_sysui (svcd down) " + fi + else + echo -n "MISSING /usr/bin/meow_svcd " + fi + elif [ -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 diff --git a/patch_v2/meow_svcd/meow_svcd b/patch_v2/meow_svcd/meow_svcd index b27e072..3a7575f 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 6d23e31..5ce3ecd 100644 --- a/patch_v2/meow_svcd/meow_svcd.c +++ b/patch_v2/meow_svcd/meow_svcd.c @@ -66,6 +66,7 @@ #include #include #include +#include #include /* ---- 默认路径 / 常量 ---- */ @@ -1143,6 +1144,11 @@ static void shutdown_all(void) static int start_daemon(void) { if (setup_signals() != 0) return 1; + /* 设为子进程回收者:被监管服务若派生出孙进程,在服务(直接子进程)退出后 + * 由 svcd 收尸而非 reparent 到 init,强化「服务崩溃隔离(僵死/孤儿)」一环。 + * 失败不致命:仅记录并继续。 */ + if (prctl(PR_SET_CHILD_SUBREAPER, 1) != 0) + svcd_log("PR_SET_CHILD_SUBREAPER failed: %s (continue)", strerror(errno)); if (setup_socket() != 0) return 1; write_pidfile(); diff --git a/patch_v2/meow_svcd/meow_svcd.md b/patch_v2/meow_svcd/meow_svcd.md index e2b0e5a..86dc771 100644 --- a/patch_v2/meow_svcd/meow_svcd.md +++ b/patch_v2/meow_svcd/meow_svcd.md @@ -128,6 +128,8 @@ FAILED 崩溃且不再重启(超 max_restart / restart=never) - **不读服务输出**:子进程继承 svcd stdio(stdout/stderr 落同一 sink),svcd **不 设管道读服务体**——服务写多少都不进 svcd 内存,天然规避「输出撑爆 → svcd 崩」。 - **后退避**:重启间隔受 `backoff_ms` 约束,防崩溃-重启热循环拖垮系统。 +- **孤儿/僵死隔离**:`start_daemon()` 设 `PR_SET_CHILD_SUBREAPER`——服务若派生孙进程, + 在服务(直接子进程)退出后由 svcd 收尸而非 reparent 到 init,避免游离/僵死进程;失败仅记日志继续。 - 服务崩**绝不**连带 supervisor:svcd 是 supervisor 的子进程且自身不死;服务是 svcd 的子进程,服务死只影响 svcd 内部一张表。 diff --git a/patch_v2/meow_sysui/S70meow_sysui b/patch_v2/meow_sysui/S70meow_sysui new file mode 100644 index 0000000..93a6c54 --- /dev/null +++ b/patch_v2/meow_sysui/S70meow_sysui @@ -0,0 +1,92 @@ +#!/bin/sh +# +# S70meow_sysui - MeowOS 系统进程总监(Supervisor Tree 顶层) +# +# 位置:**最早**(早于 S84ssh / S89meow_svcd / S90meow_bootmenu)。 +# * 定案启动链:meow_sysui 先起(S70)→ meow_svcd(由 sysui 拉起)→ 各项服务子进程。 +# * S90meow_bootmenu 里 `exec /usr/bin/meow_bootmenu --fork` 会阻塞 rcS,故任何 S90 之后 +# 的 init.d 都不执行——sysui 必须排在 S90 之前(S70 满足)。 +# +# 职责:监管系统进程(launcher / meow_svcd / meow_mdns / …),一个崩只拉起它、绝不级联。 +# 本脚本只做三件事: +# 1) 预置板载监管配置 /usr/data/meow/sysui.d/*.conf(首启建,已存在不覆盖); +# 2) 拉起 meow_sysui(默认后台 daemon); +# 3) stop 时优雅停 sysui(其会 SIGTERM 被监管的 svcd 并收尸)。 +# +# 红线:不 killall、不碰播放器信号、不刷机、不碰 recovery_all。sysui 自身最小依赖, +# 失败兜底(某被监管进程起不来 → 仅记日志 + 有限次重试,不阻塞其它/不拖垮自身)。 +# +case "$1" in + start) + echo -n "Starting meow_sysui (S70): " + + # 1) 预置板载监管配置(Data 区可写才建;已存在不覆盖) + if [ -d /usr/data/meow ] && [ -w /usr/data/meow ]; then + mkdir -p /usr/data/meow/sysui.d + if [ -f /usr/data/meow/sysui.d/svcd.conf ]; then + echo -n "svcd.conf exists " + else + cat > /usr/data/meow/sysui.d/svcd.conf <<'EOF' +# meow_sysui 监管条目:meow_svcd(服务管理器)。系统进程总监 → svcd 被监管 → 服务为 svcd 子进程。 +# 语义同 services.d:key=value,'#'/';' 注释,未知键忽略。无 trigger——enabled=1 即常驻监护。 +exec=/usr/bin/meow_svcd +args=-f +enabled=1 +restart=always +backoff_ms=3000 +max_restart=5 +EOF + chmod 644 /usr/data/meow/sysui.d/svcd.conf + echo -n "provisioned svcd.conf " + fi + # launcher 当前由 S90meow_bootmenu 的 exec 拥有生命周期;待 bootmenu 让位后交 sysui。 + # 此处仅登记(enabled=0),避免双重拉起。mdns 是 svcd 的服务子进程,由 svcd 负责, + # 本阶段不直接监管(或作兜底条目,默认关)。 + if [ -f /usr/data/meow/sysui.d/launcher.conf ]; then + echo -n "launcher.conf exists " + else + cat > /usr/data/meow/sysui.d/launcher.conf <<'EOF' +# 预留:launcher(系统 UI 渲染宿主)。现网由 S90meow_bootmenu exec 拥有 → 启用前需 bootmenu 让位。 +# 真把 launcher 交给 sysui 监管后,把 enabled=1(并停用 bootmenu 的 exec launcher 路径)。 +exec=/usr/bin/meow_launcherd +args= +enabled=0 +restart=always +backoff_ms=3000 +max_restart=5 +EOF + chmod 644 /usr/data/meow/sysui.d/launcher.conf + echo -n "provisioned launcher.conf " + fi + else + echo -n "skip provision (/usr/data/meow not writable) " + fi + + # 2) 拉起 meow_sysui(默认后台 daemon;S89meow_svcd 会因 sysui 在而把 svcd 拉起权交还 sysui) + rm -f /tmp/meow_sysui.stop + if [ -x /usr/bin/meow_sysui ]; then + if [ -f /tmp/meow_sysui.pid ] && kill -0 "$(cat /tmp/meow_sysui.pid 2>/dev/null)" 2>/dev/null; then + echo "already running" + else + /usr/bin/meow_sysui + echo "started" + fi + else + echo "MISSING /usr/bin/meow_sysui" + fi + ;; + stop) + echo -n "Stopping meow_sysui: " + touch /tmp/meow_sysui.stop + for p in $(ps | grep "[m]eow_sysui" | 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_sysui/build_sysui.sh b/patch_v2/meow_sysui/build_sysui.sh new file mode 100755 index 0000000..be8c234 --- /dev/null +++ b/patch_v2/meow_sysui/build_sysui.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# build_sysui.sh — meow_sysui 交叉编译(mipsel 静态)+ 宿主编译可选。 +# +# 目标:R3 Pro II / MeowOS(mipsel-linux-muslsf,静态)。sysui 极简无外部依赖, +# 仅 POSIX + Linux 基础接口(同 svcd),单文件满足「固件内、不入卡」。 +# +# 用法: +# ./build_sysui.sh # 交叉编译 → patch_v2/meow_sysui/meow_sysui(mipsel 静态) +# ./build_sysui.sh host # 宿主编译 → patch_v2/meow_sysui/meow_sysui_host(集成测试用) +set -e +cd "$(dirname "$0")" +ROOT="$HOME/Project/R3ProII" +CC="$ROOT/.tools/zig-mipsel-cc" + +if [ "$1" = "host" ]; then + HOST_CC="${CC_HOST:-cc}" + "$HOST_CC" -Os -Wall -Wextra meow_sysui.c -o meow_sysui_host + echo "== host build ==" + ls -la meow_sysui_host + file meow_sysui_host + exit 0 +fi + +"$CC" -Os -Wall -Wextra -static meow_sysui.c -o meow_sysui +echo "== meow_sysui (mipsel static) ==" +ls -la meow_sysui +md5sum meow_sysui +file meow_sysui diff --git a/patch_v2/meow_sysui/meow_sysui b/patch_v2/meow_sysui/meow_sysui new file mode 100755 index 0000000..298f9d6 Binary files /dev/null and b/patch_v2/meow_sysui/meow_sysui differ diff --git a/patch_v2/meow_sysui/meow_sysui.c b/patch_v2/meow_sysui/meow_sysui.c new file mode 100644 index 0000000..48d845b --- /dev/null +++ b/patch_v2/meow_sysui/meow_sysui.c @@ -0,0 +1,1105 @@ +/* meow_sysui.c — MeowOS 系统进程总监(Supervisor Tree 顶层)。 + * + * 定位(FRAMEWORK_DESIGN §10.1 / 用户定案[源:2026-08-30]): + * - 系统进程*总监*:监管系统进程(launcher / meow_svcd / meow_mdns / 未来媒体服务 / + * MeowPanel),**一个崩 → 只拉起它,绝不级联、绝不拖垮自身**。 + * - 自身**最小依赖、不渲染、不承载重逻辑** → 最稳。它只做「监护 + 逐个拉起」的轻活, + * **只监护它自己 fork+exec 出来的进程**(waitpid 需为父进程,这是「只拉起它」的可靠前提)。 + * - 由开机启动链 `etc/init.d/S70meow_sysui` 拉起(早于 S89meow_svcd),居顶层监管; + * 监管方向固定:sysui 总监 → svcd 被监管 → 服务为 svcd 子进程。 + * + * 配置(板载权威,目录式,复用 services.d 语义):`/usr/data/meow/sysui.d/.conf` + * key=value,'#'/';' 注释,未知键忽略(前向兼容)。键:exec / args / env / enabled / + * restart(always|on-fail|never) / backoff_ms / max_restart。**无 trigger**——sysui 只做 + * 常驻系统进程的监护,`enabled=1` 即拉起/监管,拔卡无关(系统进程不认卡)。 + * 说明:TF 卡 `MeowOS/sysui.d/` 作为扩展点本阶段不读,代码只读板载(最小化)。 + * + * 控制通道:AF_UNIX 单行文本(默认 /tmp/meow_sysui.sock)。 + * start|stop|restart / status [name|all] / reload / pid + * 守护默认后台常驻(无子命令即启动);`-f/--foreground` 供 init/调试。 + * + * 状态文件:/tmp/meow_sysui.status;pidfile:/tmp/meow_sysui.pid。 + * 日志:stderr(-f)+ 默认追加 /tmp/meow_sysui.log + syslog(LOG_DAEMON) 尝试。 + * + * 护固件设计(防真机开不了机): + * - 任一子进程崩溃只处理该条目(填表/重启),主循环绝不因此退出;不级联、不碰邻居。 + * - 失败兜底(有限次):某进程起不来 → restart_count++,超 max_restart → FAILED 停止 + * 自动重启,仅记日志;绝不形成崩溃-重启热循环拖垮系统。 + * - 空配置可跑:无 sysui.d/ 或全 enabled=0 → 空转存活(只写日志/status),不报错。 + * - 最小依赖:无 dlopen/无渲染/无卡热点扫描;仅 POSIX + Linux 基础接口(同 svcd)。 + * + * 编译(mipsel 静态,见 build_sysui.sh): + * ~/Project/R3ProII/.tools/zig-mipsel-cc -Os -Wall -Wextra -static meow_sysui.c -o meow_sysui + * 宿主编译(集成测试):cc -Os -Wall -Wextra meow_sysui.c -o meow_sysui_host + */ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---- 默认路径 / 常量 ---- */ +#define SYSUI_DIR_DEFAULT "/usr/data/meow/sysui.d" /* 板载监管配置目录(权威) */ +#define SYSUI_SOCK_DEFAULT "/tmp/meow_sysui.sock" +#define SYSUI_STATUS_DEFAULT "/tmp/meow_sysui.status" +#define SYSUI_PIDFILE_DEFAULT "/tmp/meow_sysui.pid" +#define SYSUI_LOG_DEFAULT "/tmp/meow_sysui.log" + +#define SYSUI_MAX_SUP 32 +#define SYSUI_NAME_MAX 64 +#define SYSUI_EXEC_MAX 256 +#define SYSUI_LINE_MAX 1024 +#define SYSUI_ARG_MAX 128 +#define SYSUI_MAX_ARGS 24 +#define SYSUI_ENV_MAX 24 +#define SYSUI_ENV_MAXLEN 128 +#define SYSUI_RESTART_STR_MAX 16 + +#define SYSUI_TICK_MAX_MS 1000 /* poll 上限(监护探测 ~1s) */ +#define SYSUI_STOP_GRACE_MS 2500 /* SIGTERM 后 SIGKILL 兜底 */ +#define SYSUI_STABLE_MS 30000 /* 连续运行此毫秒 → 清零 restart_count */ +#define SYSUI_SOCK_PATH_MAX 108 +#define SYSUI_SIGCHLD_CHAR 'C' +#define SYSUI_SIGTERM_CHAR 'T' + +/* ---- ANSI 终端配色(status 回显用;仅客户端 stdout 为 TTY 时保留,非 TTY 去色) ---- */ +#define AN_RESET "\033[0m" +#define AN_BOLD "\033[1m" +#define AN_DIM "\033[2m" +#define AN_RED "\033[31m" +#define AN_GREEN "\033[32m" +#define AN_YELLOW "\033[33m" +#define AN_CYAN "\033[36m" +#define AN_GREY "\033[90m" + +/* ---- 被监管进程状态机 ---- */ +typedef enum { + SU_DISABLED = 0, /* enabled=0,永不自动拉起 */ + SU_STOPPED, /* 未运行(被停/等待退避) */ + SU_RUNNING, /* 子进程存活 */ + SU_STOPPING, /* 已发 SIGTERM,等退出(超时 SIGKILL 兜底) */ + SU_FAILED /* 起不来/反复崩且超 max_restart,停止自动重启 */ +} su_state_t; + +static const char *g_state_str[] = { "disabled", "stopped", "running", + "stopping", "failed" }; + +typedef struct { + char name[SYSUI_NAME_MAX]; + char exec[SYSUI_EXEC_MAX]; + char argbuf[SYSUI_MAX_ARGS][SYSUI_ARG_MAX]; /* args 拷贝 */ + char *argv[SYSUI_MAX_ARGS + 1]; /* exec + args,末尾 NULL */ + int argc; + char envbuf[SYSUI_ENV_MAX][SYSUI_ENV_MAXLEN]; /* "K=V" */ + int envc; + int enabled; + char restart[SYSUI_RESTART_STR_MAX]; /* always|on-fail|never */ + long backoff_ms; + int max_restart; /* <0=无限 */ + + /* 运行状态 */ + pid_t pid; + su_state_t state; + int restart_count; + int last_exit_status; /* waitpid status */ + int has_exit; /* 1=曾退出 */ + unsigned long long last_exit_wall; /* 上次退出墙钟秒 */ + unsigned long long restart_at_ms; /* 退避到期;0=无 */ + unsigned long long stop_since_ms; /* SIGTERM 发出时刻 */ + unsigned long long run_since_ms; /* 本轮启动时刻 */ + int stop_requested; + int restart_pending; /* 停后需重启(restart 命令) */ + int user_stopped; /* 用户 stop,勿自动拉起 */ + int in_config; /* 本次扫描仍存在 */ +} sup_t; + +static sup_t g_sups[SYSUI_MAX_SUP]; +static int g_nsup = 0; + +static char g_dir[256] = SYSUI_DIR_DEFAULT; +static char g_sock[SYSUI_SOCK_PATH_MAX] = SYSUI_SOCK_DEFAULT; +static char g_status[256] = SYSUI_STATUS_DEFAULT; +static char g_pidfile[256] = SYSUI_PIDFILE_DEFAULT; +static char g_log[256] = SYSUI_LOG_DEFAULT; + +static int g_shutdown = 0; +static int g_fg = 0; +static int g_sigpipe[2] = {-1, -1}; +static int g_listen_fd = -1; +static int g_status_fd = -1; + +static void handle_connection(int fd); + +static unsigned long long now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (unsigned long long)ts.tv_sec * 1000 + (unsigned long long)ts.tv_nsec / 1000000; +} + +/* log:stderr(-f)+ 默认追加文件 + syslog 尝试 */ +static void su_log(const char *fmt, ...) +{ + char buf[512], line[560]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof buf, fmt, ap); + va_end(ap); + int l = snprintf(line, sizeof line, "%llu [meow_sysui] %s\n", now_ms(), buf); + fprintf(stderr, "%s", line); + fflush(stderr); + if (g_log[0]) { + int fd = open(g_log, O_WRONLY | O_APPEND | O_CREAT, 0644); + if (fd >= 0) { ssize_t w = write(fd, line, (size_t)l); (void)w; close(fd); } + } + syslog(LOG_DAEMON | LOG_INFO, "[meow_sysui] %s", buf); +} + +static int safe_strcpy(char *dst, size_t cap, const char *src) +{ + if (!dst || cap == 0) return -1; + int n = snprintf(dst, cap, "%s", src ? src : ""); + return (n >= 0 && (size_t)n < cap) ? 0 : -1; +} + +static char *trim(char *s) +{ + char *end; + while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n') s++; + if (*s == '\0') return s; + end = s + strlen(s) - 1; + while (end > s && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) *end-- = '\0'; + return s; +} + +static void set_cloexec(int fd) +{ + if (fd < 0) return; + int fl = fcntl(fd, F_GETFD, 0); + if (fl >= 0) fcntl(fd, F_SETFD, fl | FD_CLOEXEC); +} + +static int read_file(const char *path, char *buf, size_t sz) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + ssize_t n = read(fd, buf, sz - 1); + close(fd); + if (n < 0) return -1; + buf[n] = '\0'; + return (long)n; +} + +static int write_file(const char *path, const char *data, size_t len) +{ + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) return -1; + ssize_t n = write(fd, data, len); + close(fd); + return (n == (ssize_t)len) ? 0 : -1; +} + +/* ---- 配置解析(复用 svcd 的 tokenizer / apply_field 思路) ---- */ +static int split_args(char *in, char tok[][SYSUI_ARG_MAX], int maxtok) +{ + int n = 0; + char *p = in; + while (*p && n < maxtok) { + while (*p == ' ' || *p == '\t') p++; + if (!*p) break; + char *out = tok[n]; + int oi = 0; + char quote = 0; + while (*p) { + char c = *p; + if (quote) { + if (c == quote) { quote = 0; p++; continue; } + if (c == '\\' && p[1]) { p++; c = *p; } + } else { + if (c == ' ' || c == '\t') break; + if (c == '"' || c == '\'') { quote = c; p++; continue; } + if (c == '\\' && p[1]) { p++; c = *p; } + } + if (oi < SYSUI_ARG_MAX - 1) out[oi++] = c; + p++; + } + out[oi] = '\0'; + if (oi > 0) n++; + } + return n; +} + +static void apply_field(sup_t *s, const char *k, const char *v) +{ + if (strcmp(k, "exec") == 0) { safe_strcpy(s->exec, sizeof s->exec, v); } + else if (strcmp(k, "args") == 0) { s->argc = split_args((char *)v, s->argbuf, SYSUI_MAX_ARGS); } + else if (strcmp(k, "env") == 0) { + if (s->envc < SYSUI_ENV_MAX) + safe_strcpy(s->envbuf[s->envc++], sizeof s->envbuf[0], v); + } + else if (strcmp(k, "enabled") == 0) { s->enabled = (atoi(v) != 0); } + else if (strcmp(k, "restart") == 0) { safe_strcpy(s->restart, sizeof s->restart, v); } + else if (strcmp(k, "backoff_ms") == 0) { s->backoff_ms = atol(v); } + else if (strcmp(k, "max_restart") == 0){ s->max_restart = atoi(v); } + else if (strcmp(k, "name") == 0) { /* 名字以文件名为准,name 仅备忘 */ } +} + +static int load_conf(const char *path, const char *name, sup_t *s) +{ + char buf[SYSUI_LINE_MAX]; + FILE *f = fopen(path, "r"); + if (!f) return -1; + + memset(s, 0, sizeof *s); + safe_strcpy(s->name, sizeof s->name, name); + s->enabled = 0; + s->state = SU_STOPPED; + safe_strcpy(s->restart, sizeof s->restart, "on-fail"); + s->backoff_ms = 2000; + s->max_restart = 5; + + while (fgets(buf, sizeof buf, f)) { + char *line = trim(buf); + if (!*line || *line == '#' || *line == ';') continue; + char *eq = strchr(line, '='); + if (!eq) continue; + *eq = '\0'; + char *k = trim(line); + char *v = trim(eq + 1); + for (char *q = v; *q; q++) { + if ((*q == '#' || *q == ';') && (q == v || *(q - 1) != '\\')) { *q = '\0'; break; } + } + v = trim(v); + if (k[0]) apply_field(s, k, v); + } + fclose(f); + + if (!s->exec[0]) { su_log("supervised %s: no exec, skip", name); return -1; } + if (s->backoff_ms < 0) s->backoff_ms = 0; + if (s->max_restart < 0) s->max_restart = -1; + if (strcmp(s->restart, "always") && strcmp(s->restart, "on-fail") && + strcmp(s->restart, "never")) + safe_strcpy(s->restart, sizeof s->restart, "on-fail"); + + s->argv[0] = s->exec; + for (int i = 0; i < s->argc; i++) s->argv[i + 1] = s->argbuf[i]; + s->argv[s->argc + 1] = NULL; + return 0; +} + +/* ---- 监管表工具 ---- */ +static int find_sup(const char *name) +{ + for (int i = 0; i < g_nsup; i++) + if (strcmp(g_sups[i].name, name) == 0) return i; + return -1; +} + +static void remove_sup_idx(int idx) +{ + if (idx < 0 || idx >= g_nsup) return; + if (g_nsup - idx - 1 > 0) + memmove(&g_sups[idx], &g_sups[idx + 1], + (size_t)(g_nsup - idx - 1) * sizeof(sup_t)); + g_nsup--; +} + +static void build_status_file(void) +{ + char buf[8192]; + int n = snprintf(buf, sizeof buf, + "# meow_sysui status\nsupervised=%d\n", g_nsup); + for (int i = 0; i < g_nsup && n < (int)sizeof buf - 1; i++) { + sup_t *s = &g_sups[i]; + int k = snprintf(buf + n, sizeof buf - (size_t)n, + "sup=%s state=%s pid=%d restart=%d exit=%d backoff_ms=%ld enabled=%d\n", + s->name, g_state_str[s->state], (int)s->pid, s->restart_count, + s->last_exit_status, s->backoff_ms, s->enabled); + if (k <= 0) break; + n += k; + } + if (g_status_fd >= 0) { + if (lseek(g_status_fd, 0, SEEK_SET) == 0 && ftruncate(g_status_fd, 0) == 0) { + ssize_t w = write(g_status_fd, buf, (size_t)n); + (void)w; + } + } else { + (void)write_file(g_status, buf, (size_t)n); + } +} + +/* ---- 子进程管理 ---- */ +static int spawn_sup(sup_t *s) +{ + if (s->pid > 0) return 0; + + pid_t pid = fork(); + if (pid < 0) { + su_log("supervised %s: fork failed: %s", s->name, strerror(errno)); + s->state = SU_FAILED; + return -1; + } + if (pid == 0) { + /* 子进程:独立进程组,防父进程信号误伤。 */ + (void)setpgid(0, 0); + for (int i = 0; i < s->envc; i++) { + char *eq = strchr(s->envbuf[i], '='); + if (!eq) continue; + *eq = '\0'; + (void)setenv(s->envbuf[i], eq + 1, 1); + } + execv(s->exec, s->argv); + fprintf(stderr, "[meow_sysui] supervised %s execv %s: %s\n", + s->name, s->exec, strerror(errno)); + _exit(127); + } + s->pid = pid; + s->state = SU_RUNNING; + s->run_since_ms = now_ms(); + s->stop_requested = 0; + s->restart_pending = 0; + s->restart_at_ms = 0; + su_log("supervised %s start: pid=%d exec=%s", s->name, (int)pid, s->exec); + return 0; +} + +static void stop_sup(sup_t *s, int restart_after, int user) +{ + if (user) s->user_stopped = 1; + if (s->pid <= 0) { + if (s->state == SU_RUNNING || s->state == SU_STOPPING) s->state = SU_STOPPED; + if (restart_after) { s->restart_count = 0; s->user_stopped = 0; spawn_sup(s); } + return; + } + if (s->state == SU_STOPPING) { if (restart_after) s->restart_pending = 1; return; } + s->stop_requested = 1; + s->restart_pending = restart_after; + s->state = SU_STOPPING; + s->stop_since_ms = now_ms(); + (void)kill(s->pid, SIGTERM); + su_log("supervised %s: SIGTERM (pid=%d)", s->name, (int)s->pid); +} + +static void apply_exit(sup_t *s, int status) +{ + int failed = !(WIFEXITED(status) && WEXITSTATUS(status) == 0); + s->pid = 0; + s->last_exit_status = status; + s->has_exit = 1; + s->last_exit_wall = (unsigned long long)time(NULL); + + if (s->restart_pending) { + s->restart_pending = 0; + s->stop_requested = 0; + s->restart_count = 0; + s->state = SU_STOPPED; + su_log("supervised %s: restart -> respawn", s->name); + spawn_sup(s); + return; + } + if (s->stop_requested) { + s->stop_requested = 0; + s->state = SU_STOPPED; + su_log("supervised %s: stopped (exit=%s)", s->name, failed ? "fail" : "ok"); + return; + } + + /* 非受控退出 → 崩溃处理(只处理本条目,无级联) */ + int want = 0; + if (strcmp(s->restart, "always") == 0) want = 1; + else if (strcmp(s->restart, "on-fail") == 0) want = failed; + /* never → want=0 */ + + if (want) { + s->restart_count++; + if (s->max_restart >= 0 && s->restart_count > s->max_restart) { + s->state = SU_FAILED; + su_log("supervised %s: exceeded max_restart(%d) -> FAILED, stop auto-restart (exit=%s)", + s->name, s->max_restart, failed ? "fail" : "ok"); + } else { + long wait = s->backoff_ms; + s->restart_at_ms = now_ms() + (unsigned long long)wait; + s->state = SU_STOPPED; + su_log("supervised %s: restart in %ldms (attempt %d, exit=%s)", + s->name, wait, s->restart_count, failed ? "fail" : "ok"); + } + } else { + s->state = failed ? SU_FAILED : SU_STOPPED; + su_log("supervised %s: exit (policy=%s, %s) -> %s", + s->name, s->restart, failed ? "fail" : "ok", + failed ? "FAILED" : "STOPPED"); + } +} + +static void sig_child(int sig) +{ + (void)sig; + char c = SYSUI_SIGCHLD_CHAR; + ssize_t w = write(g_sigpipe[1], &c, 1); + (void)w; +} + +static void sig_term(int sig) +{ + (void)sig; + g_shutdown = 1; + char c = SYSUI_SIGTERM_CHAR; + ssize_t w = write(g_sigpipe[1], &c, 1); + (void)w; +} + +static void reap_children(void) +{ + char buf[64]; + while (read(g_sigpipe[0], buf, sizeof buf) > 0) { /* drain */ } + + int status; + pid_t pid; + while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { + int idx = -1; + for (int i = 0; i < g_nsup; i++) + if (g_sups[i].pid == pid) { idx = i; break; } + if (idx < 0) { su_log("reap unknown pid=%d", (int)pid); continue; } + apply_exit(&g_sups[idx], status); + } + build_status_file(); +} + +/* --- 配置扫描 / 调和 --- */ +static int scan_conf_names(const char *dir, char out[][SYSUI_NAME_MAX], int max) +{ + DIR *d = opendir(dir); + if (!d) return 0; + int n = 0; + struct dirent *e; + while ((e = readdir(d)) != NULL && n < max) { + if (e->d_name[0] == '.') continue; + size_t l = strlen(e->d_name); + if (l < 6 || strcmp(e->d_name + l - 5, ".conf") != 0) continue; + size_t nl = l - 5; + if (nl >= SYSUI_NAME_MAX) nl = SYSUI_NAME_MAX - 1; + memcpy(out[n], e->d_name, nl); + out[n][nl] = '\0'; + n++; + } + closedir(d); + return n; +} + +static void scan_and_reconcile(void) +{ + char names[SYSUI_MAX_SUP][SYSUI_NAME_MAX]; + int n = scan_conf_names(g_dir, names, SYSUI_MAX_SUP); + + for (int i = 0; i < g_nsup; i++) g_sups[i].in_config = 0; + + for (int i = 0; i < n; i++) { + char path[512]; + if (snprintf(path, sizeof path, "%s/%s.conf", g_dir, names[i]) >= (int)sizeof path) continue; + int idx = find_sup(names[i]); + if (idx < 0) { + if (g_nsup >= SYSUI_MAX_SUP) { su_log("supervise table full, drop %s", names[i]); continue; } + idx = g_nsup++; + } + sup_t tmp; + if (load_conf(path, names[i], &tmp) != 0) { continue; } + sup_t *s = &g_sups[idx]; + + int config_changed = (strcmp(s->exec, tmp.exec) != 0) + || (s->argc != tmp.argc) || (s->envc != tmp.envc) + || (s->enabled != tmp.enabled) + || (strcmp(s->restart, tmp.restart) != 0) + || (s->backoff_ms != tmp.backoff_ms) || (s->max_restart != tmp.max_restart); + if (!config_changed) { + for (int a = 0; a < tmp.argc; a++) + if (strcmp(s->argv[a + 1], tmp.argv[a + 1]) != 0) { config_changed = 1; break; } + for (int e2 = 0; e2 < tmp.envc; e2++) + if (strcmp(s->envbuf[e2], tmp.envbuf[e2]) != 0) { config_changed = 1; break; } + } + + int keep_count = s->restart_count; + int keep_exit = s->last_exit_status; + int keep_has = s->has_exit; + unsigned long long keep_wall = s->last_exit_wall; + pid_t keep_pid = s->pid; + int keep_stop = s->stop_requested; + int keep_rp = s->restart_pending; + int keep_user = s->user_stopped; + int keep_state = s->state; + *s = tmp; + s->restart_count = keep_count; + s->last_exit_status = keep_exit; + s->has_exit = keep_has; + s->last_exit_wall = keep_wall; + s->pid = keep_pid; + s->stop_requested = keep_stop; + s->restart_pending = keep_rp; + s->user_stopped = keep_user; + s->in_config = 1; + s->restart_at_ms = 0; + + if (keep_pid > 0) { + if (config_changed) { + su_log("supervised %s: config changed, restart", s->name); + stop_sup(s, 1, 0); + } else { + s->state = (keep_state == SU_STOPPING) ? SU_STOPPING : SU_RUNNING; + } + } else { + s->state = SU_STOPPED; + } + } + + /* 已移除配置的条目,倒序删(先停运行中的) */ + for (int i = g_nsup - 1; i >= 0; i--) { + if (g_sups[i].in_config) continue; + if (g_sups[i].pid > 0) stop_sup(&g_sups[i], 0, 0); + su_log("supervised %s: removed (no config)", g_sups[i].name); + remove_sup_idx(i); + } + + /* 禁用:运行中 → 停;未运行 → DISABLED */ + for (int i = 0; i < g_nsup; i++) { + sup_t *s = &g_sups[i]; + if (!s->in_config) continue; + if (!s->enabled) { + if (s->pid > 0) { su_log("supervised %s: disabled -> stop", s->name); stop_sup(s, 0, 0); } + else s->state = SU_DISABLED; + } + } + + /* 曾 FAILED 且现启用 → 重置,交 tick 重试 */ + for (int i = 0; i < g_nsup; i++) { + sup_t *s = &g_sups[i]; + if (s->in_config && s->enabled && s->state == SU_FAILED) { + s->restart_count = 0; + s->state = SU_STOPPED; + s->user_stopped = 0; + } + } + build_status_file(); +} + +static void tick_evaluate(void) +{ + for (int i = 0; i < g_nsup; i++) { + sup_t *s = &g_sups[i]; + if (s->state == SU_STOPPING) { + if (now_ms() - s->stop_since_ms > SYSUI_STOP_GRACE_MS) { + if (s->pid > 0) { kill(s->pid, SIGKILL); s->pid = 0; } + s->state = SU_STOPPED; + su_log("supervised %s: SIGKILL grace timeout", s->name); + } + continue; + } + if (s->pid > 0) { + /* kill(pid,0) 存活探测:子进程已死但尚未收尸(或异常)→ 视同死亡 */ + if (!s->stop_requested && !s->restart_pending && kill(s->pid, 0) != 0 && errno == ESRCH) { + su_log("supervised %s: probe pid=%d dead (ESRCH)", s->name, (int)s->pid); + apply_exit(s, 1 << 8); /* 视为非零退出(fail)走重建策略 */ + } else if (s->state == SU_RUNNING && + now_ms() - s->run_since_ms > SYSUI_STABLE_MS) { + s->restart_count = 0; /* 稳定运行 → 清零计数 */ + } + continue; + } + if (s->state == SU_FAILED || s->state == SU_DISABLED) continue; + + if (s->restart_at_ms > 0) { + if (now_ms() >= s->restart_at_ms) { + s->restart_at_ms = 0; + su_log("supervised %s: backoff done, respawn", s->name); + spawn_sup(s); + } + } else if (s->enabled && !s->user_stopped) { + spawn_sup(s); + } + } +} + +/* ---- 控制通道 ---- */ +static void send_resp(int fd, const char *s) +{ + ssize_t w = write(fd, s, strlen(s)); + (void)w; +} + +static int cmd_start(const char *name) +{ + if (strcmp(name, "all") == 0) { + for (int i = 0; i < g_nsup; i++) { + g_sups[i].user_stopped = 0; + g_sups[i].restart_count = 0; + g_sups[i].state = SU_STOPPED; + spawn_sup(&g_sups[i]); + } + return 0; + } + int idx = find_sup(name); + if (idx < 0) return -1; + sup_t *s = &g_sups[idx]; + s->user_stopped = 0; + if (s->pid <= 0) { s->restart_count = 0; s->state = SU_STOPPED; spawn_sup(s); } + return 0; +} + +static int cmd_stop(const char *name) +{ + if (strcmp(name, "all") == 0) { + for (int i = 0; i < g_nsup; i++) stop_sup(&g_sups[i], 0, 1); + return 0; + } + int idx = find_sup(name); + if (idx < 0) return -1; + stop_sup(&g_sups[idx], 0, 1); + return 0; +} + +static int cmd_restart(const char *name) +{ + if (strcmp(name, "all") == 0) { + for (int i = 0; i < g_nsup; i++) stop_sup(&g_sups[i], 1, 0); + return 0; + } + int idx = find_sup(name); + if (idx < 0) return -1; + sup_t *s = &g_sups[idx]; + s->user_stopped = 0; + if (s->pid <= 0) { s->restart_count = 0; s->state = SU_STOPPED; spawn_sup(s); } + else stop_sup(s, 1, 0); + return 0; +} + +/* ---- status 回显(详细 + ANSI 配色;非 TTY 由客户端去色) ---- */ +static void put_field(char *b, size_t cap, size_t *o, const char *color, const char *plain, int width) +{ + size_t pl = strlen(plain); + int w; + if (color && color[0]) + w = snprintf(b + *o, cap - *o, "%s%s" AN_RESET, color, plain); + else + w = snprintf(b + *o, cap - *o, "%s", plain); + if (w > 0) *o += (size_t)w; + if (*o >= cap) *o = cap - 1; + if (width > 0) { + int pad = width - (int)pl; + while (pad-- > 0 && *o < cap - 1) b[(*o)++] = ' '; + } + b[*o] = '\0'; +} + +static void put_str(char *b, size_t cap, size_t *o, const char *s) +{ + int w = snprintf(b + *o, cap - *o, "%s", s); + if (w > 0) *o += (size_t)w; + if (*o >= cap) *o = cap - 1; + b[*o] = '\0'; +} + +static const char *state_color(int st) +{ + switch (st) { + case SU_RUNNING: return AN_GREEN; + case SU_FAILED: return AN_RED; + case SU_STOPPED: return AN_YELLOW; + case SU_STOPPING: return AN_YELLOW; + case SU_DISABLED: return AN_GREY; + default: return ""; + } +} + +static void fmt_exit_str(const sup_t *s, char *out, size_t cap) +{ + if (!s->has_exit) { snprintf(out, cap, "-"); return; } + int st = s->last_exit_status; + if (WIFEXITED(st)) snprintf(out, cap, "%d", WEXITSTATUS(st)); + else if (WIFSIGNALED(st)) snprintf(out, cap, "sig%d", WTERMSIG(st)); + else snprintf(out, cap, "-"); +} + +static void cmd_status(int fd, const char *name) +{ + int idx = -1; + if (name && strcmp(name, "all") != 0) { + idx = find_sup(name); + if (idx < 0) { send_resp(fd, "ERR no-such-supervised\n"); return; } + } + int shown = (idx >= 0) ? 1 : g_nsup; + + int nw = 4; + for (int i = 0; i < g_nsup; i++) { + if (idx >= 0 && i != idx) continue; + int l = (int)strlen(g_sups[i].name); + if (l > nw) nw = l; + } + if (nw > 40) nw = 40; + + const int sw = 8, pw = 7, rw = 7, bw = 9, ew = 6; + size_t cap = (size_t)(shown + 3) * 512 + 1024; + char *b = malloc(cap); + if (!b) { send_resp(fd, "ERR out-of-memory\n"); return; } + size_t o = 0; + + put_field(b, cap, &o, AN_BOLD AN_CYAN, "meow_sysui status", -1); + put_str(b, cap, &o, "\n"); + char hdr[64]; + snprintf(hdr, sizeof hdr, "supervised=%d\n", g_nsup); + put_str(b, cap, &o, hdr); + + put_field(b, cap, &o, AN_DIM, "NAME", nw); put_str(b, cap, &o, " "); + put_field(b, cap, &o, AN_DIM, "STATE", sw); put_str(b, cap, &o, " "); + put_field(b, cap, &o, AN_DIM, "PID", pw); put_str(b, cap, &o, " "); + put_field(b, cap, &o, AN_DIM, "RESTART", rw); put_str(b, cap, &o, " "); + put_field(b, cap, &o, AN_DIM, "BACKOFF", bw); put_str(b, cap, &o, " "); + put_field(b, cap, &o, AN_DIM, "EXIT", ew); put_str(b, cap, &o, " "); + put_field(b, cap, &o, AN_DIM, "ENABLED", 8); put_str(b, cap, &o, " "); + put_field(b, cap, &o, AN_DIM, "EXEC", -1); + put_str(b, cap, &o, "\n"); + + int total = nw + 1 + sw + 1 + pw + 1 + rw + 1 + bw + 1 + ew + 1 + 8 + 1 + 4; + put_str(b, cap, &o, AN_DIM); + for (int x = 0; x < total && o < cap - 1; x++) b[o++] = '-'; + put_str(b, cap, &o, AN_RESET "\n"); + + for (int i = 0; i < g_nsup; i++) { + if (idx >= 0 && i != idx) continue; + sup_t *s = &g_sups[i]; + char pidstr[16], rstr[16], bstr[16], estr[16]; + snprintf(pidstr, sizeof pidstr, "%d", (int)s->pid); + if (s->max_restart < 0) snprintf(rstr, sizeof rstr, "%d/inf", s->restart_count); + else snprintf(rstr, sizeof rstr, "%d/%d", s->restart_count, s->max_restart); + snprintf(bstr, sizeof bstr, "%ldms", s->backoff_ms); + fmt_exit_str(s, estr, sizeof estr); + + put_field(b, cap, &o, AN_BOLD AN_CYAN, s->name, nw); put_str(b, cap, &o, " "); + put_field(b, cap, &o, state_color(s->state), g_state_str[s->state], sw); put_str(b, cap, &o, " "); + put_field(b, cap, &o, AN_YELLOW, pidstr, pw); put_str(b, cap, &o, " "); + put_field(b, cap, &o, AN_YELLOW, rstr, rw); put_str(b, cap, &o, " "); + put_field(b, cap, &o, AN_YELLOW, bstr, bw); put_str(b, cap, &o, " "); + put_field(b, cap, &o, "", estr, ew); put_str(b, cap, &o, " "); + put_field(b, cap, &o, (s->enabled ? AN_GREEN : AN_GREY), (s->enabled ? "yes" : "no"), 8); + put_str(b, cap, &o, " "); + put_field(b, cap, &o, "", s->exec, -1); + put_str(b, cap, &o, "\n"); + } + + size_t sent = 0; + while (sent < o) { + ssize_t w = write(fd, b + sent, o - sent); + if (w <= 0) break; + sent += (size_t)w; + } + free(b); +} + +static void handle_connection(int fd) +{ + char buf[SYSUI_LINE_MAX]; + ssize_t n = read(fd, buf, sizeof buf - 1); + if (n <= 0) { send_resp(fd, "ERR empty\n"); return; } + buf[n] = '\0'; + char *line = trim(buf); + if (!line[0]) { send_resp(fd, "ERR empty\n"); return; } + char *sp = strchr(line, ' '); + if (sp) *sp++ = '\0'; + + if (strcmp(line, "pid") == 0) { + char r[64]; snprintf(r, sizeof r, "%d\n", (int)getpid()); send_resp(fd, r); + } else if (strcmp(line, "status") == 0) { + cmd_status(fd, sp); + } else if (strcmp(line, "reload") == 0) { + 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-supervised\n"); + } else if (sp && strcmp(line, "stop") == 0) { + send_resp(fd, cmd_stop(sp) == 0 ? "OK\n" : "ERR no-such-supervised\n"); + } else if (sp && strcmp(line, "restart") == 0) { + send_resp(fd, cmd_restart(sp) == 0 ? "OK\n" : "ERR no-such-supervised\n"); + } else { + send_resp(fd, "ERR unknown-command\n"); + } +} + +/* ---- Daemon 生命周期 ---- */ +static int setup_socket(void) +{ + unlink(g_sock); + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { su_log("socket: %s", strerror(errno)); return -1; } + set_cloexec(fd); + struct sockaddr_un a; + memset(&a, 0, sizeof a); + a.sun_family = AF_UNIX; + safe_strcpy(a.sun_path, sizeof a.sun_path, g_sock); + if (bind(fd, (struct sockaddr *)&a, sizeof a) != 0) { + su_log("bind %s: %s", g_sock, strerror(errno)); + close(fd); return -1; + } + if (listen(fd, 4) != 0) { su_log("listen: %s", strerror(errno)); close(fd); return -1; } + g_listen_fd = fd; + return 0; +} + +static int setup_signals(void) +{ + if (pipe(g_sigpipe) != 0) { su_log("pipe: %s", strerror(errno)); return -1; } + int rf = fcntl(g_sigpipe[0], F_GETFL, 0); + if (rf >= 0) fcntl(g_sigpipe[0], F_SETFL, rf | O_NONBLOCK); + int wf = fcntl(g_sigpipe[1], F_GETFL, 0); + if (wf >= 0) fcntl(g_sigpipe[1], F_SETFL, wf | O_NONBLOCK); + set_cloexec(g_sigpipe[0]); + set_cloexec(g_sigpipe[1]); + + struct sigaction sa; + memset(&sa, 0, sizeof sa); + sa.sa_handler = sig_child; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGCHLD, &sa, NULL); + + struct sigaction st; + memset(&st, 0, sizeof st); + st.sa_handler = sig_term; + sigemptyset(&st.sa_mask); + st.sa_flags = 0; + sigaction(SIGTERM, &st, NULL); + sigaction(SIGINT, &st, NULL); + + signal(SIGPIPE, SIG_IGN); + return 0; +} + +static void daemonize(void) +{ + pid_t pid = fork(); + if (pid < 0) { su_log("fork: %s", strerror(errno)); } + if (pid > 0) _exit(0); + if (setsid() < 0) { /* ignore */ } + pid = fork(); + if (pid < 0) { su_log("fork2: %s", strerror(errno)); } + if (pid > 0) _exit(0); + if (chdir("/") != 0) { /* ignore */ } + int nul = open("/dev/null", O_RDWR); + if (nul >= 0) { dup2(nul, 0); dup2(nul, 1); if (nul > 2) close(nul); } + /* 保留 stderr=2(g_log 仍会写文件;stderr 落到 /dev/null 由调用方决定) */ +} + +static void write_pidfile(void) +{ + char buf[32]; + int l = snprintf(buf, sizeof buf, "%d\n", (int)getpid()); + (void)write_file(g_pidfile, buf, (size_t)l); +} + +static void shutdown_all(void) +{ + for (int i = 0; i < g_nsup; i++) { + if (g_sups[i].pid > 0) { + kill(g_sups[i].pid, SIGTERM); + unsigned long long wait_until = now_ms() + 1500; + while (g_sups[i].pid > 0 && now_ms() < wait_until) { + int st; pid_t p = waitpid(g_sups[i].pid, &st, WNOHANG); + if (p == g_sups[i].pid) { g_sups[i].pid = 0; } + else usleep(50000); + } + if (g_sups[i].pid > 0) { kill(g_sups[i].pid, SIGKILL); g_sups[i].pid = 0; } + } + } + build_status_file(); + if (g_listen_fd >= 0) close(g_listen_fd); + if (g_status_fd >= 0) close(g_status_fd); + unlink(g_sock); + unlink(g_pidfile); +} + +static int start_daemon(void) +{ + if (setup_signals() != 0) return 1; + /* 子进程回收者:被监管进程若派生孙进程,在直接子进程退出后由 sysui 收尸,强化「孤儿/僵死隔离」。 */ + if (prctl(PR_SET_CHILD_SUBREAPER, 1) != 0) + su_log("PR_SET_CHILD_SUBREAPER failed: %s (continue)", strerror(errno)); + if (setup_socket() != 0) return 1; + write_pidfile(); + + g_status_fd = open(g_status, O_RDWR | O_CREAT, 0644); + if (g_status_fd < 0) su_log("status file open fail: %s", strerror(errno)); + else set_cloexec(g_status_fd); + + scan_and_reconcile(); + + su_log("meow_sysui up: dir=%s sock=%s pid=%d supervised=%d", + g_dir, g_sock, (int)getpid(), g_nsup); + + struct pollfd pfds[2]; + nfds_t nfds = 2; + + unsigned long long last_tick = 0; + while (!g_shutdown) { + unsigned long long soon = 0; + for (int i = 0; i < g_nsup; i++) { + if (g_sups[i].restart_at_ms > 0) { + unsigned long long d = g_sups[i].restart_at_ms > now_ms() + ? g_sups[i].restart_at_ms - now_ms() : 0; + if (soon == 0 || d < soon) soon = d; + } + } + int timeout = (soon > 0 && soon < (unsigned long long)SYSUI_TICK_MAX_MS) + ? (int)soon : SYSUI_TICK_MAX_MS; + if (timeout < 20) timeout = 20; + + pfds[0].fd = g_listen_fd; pfds[0].events = POLLIN; pfds[0].revents = 0; + pfds[1].fd = g_sigpipe[0]; pfds[1].events = POLLIN; pfds[1].revents = 0; + + int r = poll(pfds, nfds, timeout); + if (r < 0) { + if (errno == EINTR) continue; + su_log("poll: %s", strerror(errno)); + break; + } + if (g_shutdown) break; + + if (pfds[0].revents & POLLIN) { + int cfd = accept(g_listen_fd, NULL, NULL); + if (cfd >= 0) { + set_cloexec(cfd); + handle_connection(cfd); + close(cfd); + } + } + if (pfds[1].revents & POLLIN) reap_children(); + + if (now_ms() - last_tick >= (unsigned long long)SYSUI_TICK_MAX_MS) { + last_tick = now_ms(); + /* 仅做监护评估(tick_evaluate),不在此处定期 scan:scan_and_reconcile 会把 + * FAILED(超 max_restart) 重置回 STOPPED,若每 tick 调会导致「无限次重试」, + * 破坏「有限次兜底」。配置扫描只在 启动 + reload 命令 执行(同 svcd)。 */ + } + tick_evaluate(); + build_status_file(); + } + + su_log("meow_sysui shutdown"); + shutdown_all(); + return 0; +} + +/* ---- CLI ---- */ +static char *strip_ansi(char *s) +{ + char *r = s, *w = s; + while (*r) { + if (*r == '\033' && r[1] == '[') { + r += 2; + while (*r && !(*r >= '@' && *r <= '~')) r++; + if (*r) r++; + } else { + *w++ = *r++; + } + } + *w = '\0'; + return s; +} + +static int run_client(const char *cmdline) +{ + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { fprintf(stderr, "[meow_sysui] socket: %s\n", strerror(errno)); return 2; } + struct sockaddr_un a; + memset(&a, 0, sizeof a); + a.sun_family = AF_UNIX; + safe_strcpy(a.sun_path, sizeof a.sun_path, g_sock); + if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { + fprintf(stderr, "[meow_sysui] daemon not running (%s): %s\n", g_sock, strerror(errno)); + close(fd); + char *buf = malloc(8192); + if (buf && read_file(g_status, buf, 8192) > 0) { fputs(buf, stdout); free(buf); } + else if (buf) { free(buf); printf("supervised=0\n"); } + return 1; + } + ssize_t w = write(fd, cmdline, strlen(cmdline)); + (void)w; + shutdown(fd, SHUT_WR); + + size_t cap = 65536, len = 0; + char *rbuf = malloc(cap); + if (!rbuf) { close(fd); return 2; } + ssize_t n; + while ((n = read(fd, rbuf + len, cap - len - 1)) > 0) { + len += (size_t)n; + if (len >= cap - 1) break; + } + close(fd); + rbuf[len] = '\0'; + if (!isatty(STDOUT_FILENO)) strip_ansi(rbuf); + fputs(rbuf, stdout); + free(rbuf); + return 0; +} + +static void usage(const char *argv0) +{ + fprintf(stderr, + "usage: %s [options] [command]\n" + " commands: start | stop | restart \n" + " | status [name|all] | reload | pid\n" + " no command -> start daemon (default; -f/--foreground keeps fg for init)\n" + " options:\n" + " -f, --foreground stay in foreground\n" + " --dir supervised config dir (default /usr/data/meow/sysui.d)\n" + " --sock control socket (default /tmp/meow_sysui.sock)\n" + " --status status file (default /tmp/meow_sysui.status)\n" + " --pidfile pid file (default /tmp/meow_sysui.pid)\n" + " --log also append log to (default /tmp/meow_sysui.log)\n", + argv0); +} + +int main(int argc, char **argv) +{ + const char *cmd_tok[16]; + int cmd_n = 0; + for (int i = 1; i < argc; i++) { + const char *a = argv[i]; + if (strcmp(a, "-f") == 0 || strcmp(a, "--foreground") == 0) g_fg = 1; + else if (strcmp(a, "--dir") == 0 && i + 1 < argc) safe_strcpy(g_dir, sizeof g_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]); + else if (strcmp(a, "--log") == 0 && i + 1 < argc) safe_strcpy(g_log, sizeof g_log, argv[++i]); + else if (a[0] == '-') { usage(argv[0]); return 2; } + else if (cmd_n < 16) cmd_tok[cmd_n++] = a; + } + + setvbuf(stderr, NULL, _IONBF, 0); + + if (cmd_n > 0) { + char buf[SYSUI_LINE_MAX]; + buf[0] = '\0'; + size_t off = 0; + for (int i = 0; i < cmd_n; i++) { + int k = snprintf(buf + off, sizeof buf - off, "%s%s", i ? " " : "", cmd_tok[i]); + if (k < 0 || (size_t)k >= sizeof buf - off) break; + off += (size_t)k; + } + return run_client(buf); + } + + if (!g_fg) daemonize(); + return start_daemon(); +} diff --git a/patch_v2/meow_sysui/meow_sysui.md b/patch_v2/meow_sysui/meow_sysui.md new file mode 100644 index 0000000..4281dff --- /dev/null +++ b/patch_v2/meow_sysui/meow_sysui.md @@ -0,0 +1,130 @@ +# meow_sysui — MeowOS 系统进程总监 + +系统进程**总监**:监管系统进程(launcher / meow_svcd / meow_mdns / 未来媒体服务 / MeowPanel), +**一个崩 → 只拉起它,绝不级联、绝不拖垮自身**。自身**最小依赖、不渲染、不承载重逻辑** → 最稳。 +由开机启动链 `etc/init.d/S70meow_sysui` 拉起(早于 S89meow_svcd),居顶层监管。 + +权威锚点:`FRAMEWORK_DESIGN §10.1`(进程角色/Supervisor 树)、`docs/MEOWOS_SPEC_REF §4`、 +`devdocs/plan/SYS_BASELINE.md`(本线方案)。 + +二进制入口:`patch_v2/meow_sysui/meow_sysui`(mipsel 静态,~113KB,<200KB 固件阈值)。 +宿主编译:`patch_v2/meow_sysui/meow_sysui_host`。 + +--- + +## 1. 定位与监管模型 + +- **只监护自己 fork+exec 出来的进程**:`waitpid` 需为父进程,这是「一个崩只拉起它」的可靠前提; + sysui **不做**「adopt 外部 pid」式监护(那只 probe `kill(pid,0)`、无法真正 waitpid/重启)。 +- 要监管什么由配置决定;**默认配置只开启 `meow_svcd`**(符合定案「sysui 总监 → svcd 被监管 → + 服务为 svcd 子进程」)。`launcher` 当前由 S90meow_bootmenu 的 `exec` 拥有,配置中登记但 + `enabled=0`(避免双重拉起;真正移交需 bootmenu 让位,见 §6)。 +- `meow_mdns` 是 svcd 的服务子进程(svcd 负责 spawn/restart),sysui 本阶段**不直接监管**它 + (避免与 svcd 双重管理);`§10.1` 的「mdns 崩→svcd 拉起(或 sysui 兜底)」——兜底可后续 + 在 `sysui.d/` 加一条 meow_mdns 条目实现(默认关)。 + +## 2. 配置:`/usr/data/meow/sysui.d/.conf`(板载权威,目录式) + +复用 services.d 语义:一个被监管进程一个 `.conf`,`key=value`,`#`/`;` 注释,未知键忽略(前向兼容)。 + +| 字段 | 必填 | 说明 | 默认 | +|---|---|---|---| +| `exec` | ✅ | 被监管二进制绝对路径 | 无(缺省 → 跳过该条目) | +| `args` | 否 | 空格切分,支持单/双引号与 `\` 转义 | 无 | +| `env` | 否 | `K=V`,可多条;exec 前 `setenv` | 无 | +| `enabled` | 否 | `1`=监护该进程;`0`=只登记不拉起 | 0 | +| `restart` | 否 | `always`/`on-fail`/`never` | `on-fail` | +| `backoff_ms` | 否 | 崩溃后重启退避毫秒(固定) | 2000 | +| `max_restart` | 否 | 连续重启上限,超限 → `failed` 停止自动重启;`-1`=无限 | 5 | + +> **无 `trigger`**:sysui 只管**常驻系统进程**(launcher/svcd/mdns),`enabled=1` 即拉起/常驻监管, +> 拔卡无关(系统进程不认卡)。卡相关服务归 svcd 的 `services.d`。 +> TF 卡 `MeowOS/sysui.d/` 作为扩展点本阶段不读(代码只读板载,最小化)。 + +示例(`svcd.conf`,由 S70 首启预置): + +```ini +# 系统进程总监 → svcd 被监管 → 服务为 svcd 子进程 +exec = /usr/bin/meow_svcd +args = -f +enabled = 1 +restart = always +backoff_ms = 3000 +max_restart = 5 +``` + +## 3. 状态机 + +``` +DISABLED enabled=0,永不自动拉起 +STOPPED 未运行(被停 / 等待退避) +RUNNING 子进程存活 +STOPPING 已发 SIGTERM,等退出(SYSUI_STOP_GRACE_MS=2500 后 SIGKILL 兜底) +FAILED 反复崩/起不来且超 max_restart,停止自动重启(有限次兜底) +``` + +转换(主循环事件驱动 + 周期 tick,同 svcd): + +- **子进程死亡**(`waitpid` 匹配):无 `stop_requested`/`restart_pending` → 按该条目 `restart` + 策略决定重建(先进 `backoff_ms` 退避)或落 `FAILED`/`STOPPED`;稳定运行 `SYSUI_STABLE_MS=30s` + 清零 `restart_count`。**只处理该条目,无级联。** +- **`kill(pid,0)` 存活探测**:`tick` 里对 `RUNNING` 条目做 `kill(pid,0)`;若 `ESRCH`(子进程已死 + 未收尸/异常)→ 视同死亡走重建策略(对应「kill(pid,0)/waitpid 探测」定案)。 +- **`stop` 命令**:`STOPPING → STOPPED`(置 `user_stopped`,抑制自动拉起)。 +- **`restart` 命令**:收尸后原地 respawn(清零计数)。 +- **配置变更**:`reload` 命令重读 `sysui.d/`;新增→监护,移除→停,配置变更→重启该条。 + +## 4. 崩溃隔离 / 护固件(防开不了机) + +- **独立进程**:每个被监管进程 `fork()+execv()` + `setpgid(0,0)`,sysui 只经 `waitpid`/`SIGCHLD` + 感知;无中间层内存共享、无 dlopen。 +- **一个崩不全部崩**:`reap_children()` 只命中死掉的那条;重启只作用于该条;主循环绝不在子进程 + 死亡时 `exit`。 +- **有限次兜底**:某进程起不来/反复崩 → `restart_count++`,超 `max_restart` → `FAILED` **停止自动重启**, + 仅记日志(`sysui: exceeded max_restart -> FAILED`);绝不崩溃-重启热循环拖垮系统。 + 重新尝试需 `reload`/配置变更(`scan_and_reconcile` 仅在启动与 reload 执行——**不在每 tick 调用**, + 否则会每秒重置 FAILED、破坏「有限次」语义)。 +- **自身不死**:主循环只在 `SIGTERM/SIGINT` 退出;`SIGPIPE` 置 `SIG_IGN`;poll `EINTR` 续跑。 +- **空配置可跑**:无 `sysui.d/` 或全 `enabled=0` → 空转存活(只写日志/status),不报错、不阻塞后续 S84/S89/S90。 +- **子进程回收者**:`PR_SET_CHILD_SUBREAPER`——被监管进程若派生孙进程,在直接子进程退出后由 sysui + 收尸而非 reparent 到 init,强化「孤儿/僵死隔离」(失败仅日志,不硬失败)。 + +## 5. CLI / 控制协议 / 日志 + +守护默认后台常驻(无子命令即启动);`-f/--foreground` 供 init/调试。控制走 AF_UNIX 单行文本 +(默认 `/tmp/meow_sysui.sock`): + +``` +meow_sysui status [name|all] # 列出各被监管进程 + 全局(daemon 未运行则打印状态文件) +meow_sysui reload # 重读 sysui.d/ +meow_sysui start +meow_sysui stop +meow_sysui restart +meow_sysui pid +``` + +- 状态文件 `/tmp/meow_sysui.status`;pidfile `/tmp/meow_sysui.pid`。 +- status 回显:每条目 服务名/状态/pid/重启次数+max_restart/backoff_ms/最近退出码/是否启用/exec; + TTY 带 ANSI 配色(表头 dim、服务名加粗青、RUNNING=绿、FAILED=红、STOPPED·STOPPING=黄、DISABLED=灰), + 非 TTY 自动去色。 +- 日志:stderr(`-f`)+ 默认追加 `/tmp/meow_sysui.log` + `syslog(LOG_DAEMON)` 尝试。 + +## 6. 迁移说明 / 边界(诚实标注) + +- **launcher 监管**:现网 launcher 由 `S90meow_bootmenu` 的 `exec` 拉起(bootmenu 拥有其生命周期)。 + sysui **本阶段不拉起 launcher**(`sysui.d/launcher.conf` 默认 `enabled=0` + 注释); + launcher 真正归 sysui 需 bootmenu 让位(后续迁移)。sysui 代码完全支持加一条 launcher 条目即可。 +- **mdns 监管**:由 svcd 负责;sysui 兜底条目默认关。 + +## 7. 编译 + +```bash +./build_sysui.sh # mipsel 静态 → meow_sysui(0 warning) +./build_sysui.sh host # 宿主编译 → meow_sysui_host(集成测试) +``` + +## 后续(不在本阶段) + +- launcher 正式移交 sysui 监管(依赖 bootmenu 让位)。 +- `MeowOS/sysui.d/` TF 双源扩展(复用 svcd 板载优先语义)。 +- 与 meow_watchdog / 崩溃捕获的职责对齐(见 WORK_STATE SDK 缺失清单 #5)。