Files
MeowOS-For-R3P2/patch_v2/meow_svcd/meow_svcd.c
T

1360 lines
52 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* meow_svcd.c — MeowOS 服务管理器守护进程(类 systemd/systemctl 角色)。
*
* 定位(用户定案,见 docs/MEOWOS_SPEC_REF §1.2 / DEV_NOTES):
* - 独立于 supervisor(meow_bootmenu/launcher)的进程,但由 supervisor 托管
* fork/exec 启动;
* - 每个「服务」也是一个独立子进程(fork+exec),由本进程 waitpid 收 SIGCHLD,
* 实现严重崩溃隔离:服务崩绝不拖垮 svcd,也绝不连带 supervisor;
* - svcd 本身极简:无 dlopen、无外部依赖,仅依赖 POSIX + Linux 基础接口,
* 交叉编译为 mipsel 静态(zig-mipsel-cc),打进固件(不入卡)。
*
* 配置来源(双源,目录式;板载优先):
* - 板载 `/usr/data/meow/services.d/<name>.conf`(Data 区,恒定存在,权威源);
* - TF 卡 `$CARD/MeowOS/services.d/<name>.conf`(默认 `CARD=/usr/data/mnt/sd_0`)。
* 扫描**两处都读**;同名服务**板载优先**(扫 TF 时若 name 已在板载 → 跳过 + WARN
* 「<name> 板载与 TF 都存在,采用板载」)。无双源冲突时行为不变。
* 卡拔出 → svcd 优雅停止所托管**卡上**服务并保留运行状态(板载服务视 trigger 保留);
* 插卡 → 自动重建。
*
* .conf 字段(极简 key = value,'#'/';' 注释,见 meow_svcd.md 文档):
* name / exec / args / env / enabled / restart / backoff_ms / max_restart /
* cap / trigger。字段含义与状态机见文件尾部注释块 + meow_svcd.md。
*
* 事件触发(低成本轮询,避免复杂订阅):
* - 卡挂载:轮询 `$CARD/MeowOS/services.d` 存在性(每 SVCD_SCAN_MS 扫一次);
* - WiFi 状态:可选,读 /sys/class/net/<iface>/operstate 判定(wifi-up 触发用,
* 本平台无 WiFi 接口时恒为 0,属扩展点);
* - boot:配置在卡 → 随卡存在即启动(拔卡即停)。
*
* 控制通道:AF_UNIX 单行文本协议(默认 /tmp/meow_svcd.sock):
* start|stop|restart <name|all> / status [name|all] / reload / pid
* 后台常驻模式默认(无子命令即启动守护进程);`-f/--foreground` 供 supervisor
* fork/exec 托管(保证 supervisor 可 waitpid)。
*
* status 回显(详细 + ANSI 配色):
* 每服务列出 状态/pid/重启次数+max_restart/backoff_ms/trigger/来源(板载|TF)/
* 最近退出码/退出时间/exec 路径;标题+列名+对齐;关键字段着色(RUNNING=绿、
* FAILED=红、STOPPED·STOPPING=黄、DISABLED=灰、服务名=青、数字=黄、表头=dim)。
* ANSI 色由守护进程统一产出,客户端在 stdout 非 TTY(重定向/SSH 管道)时去色。
*
* 状态文件:`/tmp/meow_svcd.status`(可 --status 覆盖),状态变更+周期落盘,
* 板载 tmpfs 存活期内可读。
*
* 日志:svcd 自身日志走 stderr(便于 supervisor 捕获);可选 MEOW_SVCD_LOG 追加
* 文件。服务子进程继承 svcd 的 stdio(其 stdout/stderr 落到同一 sink),无需
* dlopen/管道,天然规避服务输出撑爆 svcd 内存导致的崩溃。
*
* 编译(mipsel 静态,见 build_svcd.sh):
* ~/Project/R3ProII/.tools/zig-mipsel-cc -Os -Wall -Wextra -static meow_svcd.c -o meow_svcd
* 宿主编译(集成测试):gcc -Os -Wall -Wextra meow_svcd.c -o meow_svcd_host
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <time.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/prctl.h>
#include <poll.h>
/* ---- 默认路径 / 常量 ---- */
#define SVCD_CARD_DEFAULT "/usr/data/mnt/sd_0" /* TF 卡挂载点(SPEC §1.2) */
#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"
#define SVCD_MAX_SERVICES 64
#define SVCD_NAME_MAX 64
#define SVCD_EXEC_MAX 256
#define SVCD_LINE_MAX 1024 /* .conf / 命令单行上限 */
#define SVCD_ARG_MAX 128 /* 单参数串上限 */
#define SVCD_MAX_ARGS 24 /* args 个数上限 */
#define SVCD_ENV_MAX 24 /* env 条数上限 */
#define SVCD_ENV_MAXLEN 128 /* 单条 env "K=V" 上限 */
#define SVCD_CAP_MAX 128
#define SVCD_RESTART_STR_MAX 16
#define SVCD_TRIG_STR_MAX 16
#define SVCD_SCAN_MS 1000 /* 卡/wifi 轮询间隔 */
#define SVCD_TICK_MAX_MS 500 /* poll 上限(卡变化 ~1s 内感知) */
#define SVCD_STOP_GRACE_MS 2500 /* SIGTERM 后 SIGKILL 兜底 */
#define SVCD_STABLE_MS 30000 /* 连续运行此毫秒 → 重置 restart_count */
#define SVCD_SOCK_PATH_MAX 108
#define SVCD_SIGCHLD_CHAR 'C' /* 自管道字节(拉起 poll) */
#define SVCD_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 {
SV_DISABLED = 0, /* enabled=0,永不自动启动 */
SV_STOPPED, /* 未运行(因触发不满足/被停/等待退避) */
SV_RUNNING, /* 子进程存活 */
SV_STOPPING, /* 已发 SIGTERM,等退出(超时 SIGKILL 兜底) */
SV_FAILED /* 崩溃且不再重启(超 max_restart/restart=never) */
} sv_state_t;
static const char *g_state_str[] = { "disabled", "stopped", "running",
"stopping", "failed" };
typedef struct {
char name[SVCD_NAME_MAX];
char exec[SVCD_EXEC_MAX];
char argbuf[SVCD_MAX_ARGS][SVCD_ARG_MAX]; /* args 拷贝 */
char *argv[SVCD_MAX_ARGS + 1]; /* exec + args,末尾 NULL */
int argc;
char envbuf[SVCD_ENV_MAX][SVCD_ENV_MAXLEN]; /* "K=V" */
int envc;
int enabled;
char restart[SVCD_RESTART_STR_MAX]; /* always|on-fail|never */
long backoff_ms;
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;
sv_state_t state;
int restart_count;
int last_exit_status; /* waitpid status */
int has_exit; /* 1=曾退出(last_exit_* 有效) */
unsigned long long last_exit_wall; /* 上次退出墙钟秒(0=未知;仅供 status 展示) */
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; /* 本次扫描仍存在 */
} svc_t;
static svc_t g_svcs[SVCD_MAX_SERVICES];
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 托管) */
static int g_sigpipe[2] = {-1, -1}; /* 信号自管道 */
static int g_listen_fd = -1; /* 控制 socket */
static int g_status_fd = -1; /* 状态文件 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:svcd 自身日志走 stderr(supervisor 捕获),可选追加文件 ---- */
static void svcd_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_svcd] %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); }
}
}
/* ---- 简单工具 ---- */
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;
}
/* 让 fd 在 exec 后自动关闭:服务子进程 fork+exec 后绝不继承守护进程的
* 监听 socket / 已接受连接 / 自管道 / 状态文件 fd —— 否则子进程一直持有
* 客户端连接副本,客户端读不到 EOF 而阻塞;监听 socket 也被子进程占着。 */
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 long 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;
}
/* ---- 配置解析 ---- */
/* 极简 tokenizer:按空白切分,支持单/双引号与反斜杠转义。int。 */
static int split_args(char *in, char tok[][SVCD_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 < SVCD_ARG_MAX - 1) out[oi++] = c;
p++;
}
out[oi] = '\0';
if (oi > 0) n++;
}
return n;
}
static void apply_field(svc_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, SVCD_MAX_ARGS); }
else if (strcmp(k, "env") == 0) {
if (s->envc < SVCD_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, "cap") == 0) { safe_strcpy(s->cap, sizeof s->cap, v); }
else if (strcmp(k, "trigger") == 0) { safe_strcpy(s->trigger, sizeof s->trigger, v); }
else if (strcmp(k, "name") == 0) { /* 名字以文件名为准,name 仅备忘 */ }
/* 未知键忽略(前向兼容) */
}
/* 重建 s->argv[]:argv[0]=exec、argv[i+1]=argbuf[i]、末尾 NULL。
* s->argv 是指针数组;load_conf 里它指向 s 自身的 exec/argbuf(栈局部 tmp 亦成立),
* 但 scan_and_reconcile 用 *s=tmp 整体拷贝时只复制指针值,仍指向已销毁的 tmp 栈帧。
* 故赋值后必须重定位到 s 自己的 exec/argbuf,否则 execv 传出的 argv[0]/cmdline 读到脏值。 */
static void svc_rebuild_argv(svc_t *s)
{
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;
}
/* 解析单个 .conf 到 svc(name=basename)。成功返回 0。 */
static int load_conf(const char *path, const char *name, svc_t *s)
{
char buf[SVCD_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; /* 默认禁用(显式 enabled=1 才启动) */
s->state = SV_STOPPED;
safe_strcpy(s->restart, sizeof s->restart, "on-fail"); /* 默认策略 */
s->backoff_ms = 1000;
s->max_restart = 5;
safe_strcpy(s->trigger, sizeof s->trigger, "card-mount"); /* 默认触发 */
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]) { svcd_log("service %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");
if (strcmp(s->trigger, "boot") && strcmp(s->trigger, "card-mount") &&
strcmp(s->trigger, "wifi-up"))
safe_strcpy(s->trigger, sizeof s->trigger, "card-mount");
svc_rebuild_argv(s);
return 0;
}
/* ---- 服务表工具 ---- */
static int find_svc(const char *name)
{
for (int i = 0; i < g_nsvc; i++)
if (strcmp(g_svcs[i].name, name) == 0) return i;
return -1;
}
static void remove_svc_idx(int idx)
{
if (idx < 0 || idx >= g_nsvc) return;
if (g_nsvc - idx - 1 > 0)
memmove(&g_svcs[idx], &g_svcs[idx + 1],
(size_t)(g_nsvc - idx - 1) * sizeof(svc_t));
g_nsvc--;
}
static void build_status_file(void)
{
char buf[8192];
int n = snprintf(buf, sizeof buf,
"# meow_svcd status\ncard_present=%d\nwifi_up=%d\nservices=%d\n",
g_card_present, g_wifi_up, g_nsvc);
for (int i = 0; i < g_nsvc && n < (int)sizeof buf - 1; i++) {
svc_t *s = &g_svcs[i];
int k = snprintf(buf + n, sizeof buf - (size_t)n,
"svc=%s state=%s pid=%d restart=%d exit=%d trigger=%s backoff_ms=%ld\n",
s->name, g_state_str[s->state], (int)s->pid, s->restart_count,
s->last_exit_status, s->trigger, s->backoff_ms);
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);
}
}
/* ---- 子进程管理 ---- */
/* 实际拉起子进程(fork+exec)。调用方负责 restart_count。返回 0=成功。 */
static int spawn_service(svc_t *s)
{
if (s->pid > 0) return 0;
pid_t pid = fork();
if (pid < 0) {
svcd_log("service %s: fork failed: %s", s->name, strerror(errno));
s->state = SV_FAILED;
return -1;
}
if (pid == 0) {
/* 子进程:独立进程组,防父进程信号误伤;cap 实装属后续任务(见 meow_svcd.md),
* 此处仅保留 s->cap 字符串,不应用(平台支持与否未定)。 */
(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_svcd] service %s execv %s: %s\n",
s->name, s->exec, strerror(errno));
_exit(127);
}
s->pid = pid;
s->state = SV_RUNNING;
s->run_since_ms = now_ms();
s->stop_requested = 0;
s->restart_pending = 0;
s->restart_at_ms = 0;
svcd_log("service %s start: pid=%d exec=%s", s->name, (int)pid, s->exec);
return 0;
}
/* 停止一个运行/停止中服务。restart_after=1 → 停后立即重启(restart 命令)。
* user=1 → 用户 stop(置 user_stopped,触发自动拉起被抑制)。 */
static void stop_service(svc_t *s, int restart_after, int user)
{
if (user) s->user_stopped = 1;
if (s->pid <= 0) {
if (s->state == SV_RUNNING || s->state == SV_STOPPING) s->state = SV_STOPPED;
if (restart_after) { s->restart_count = 0; s->user_stopped = 0; spawn_service(s); }
return;
}
if (s->state == SV_STOPPING) { if (restart_after) s->restart_pending = 1; return; }
s->stop_requested = 1;
s->restart_pending = restart_after;
s->state = SV_STOPPING;
s->stop_since_ms = now_ms();
(void)kill(s->pid, SIGTERM);
svcd_log("service %s: SIGTERM (pid=%d)", s->name, (int)s->pid);
}
/* 对单个已死亡子进程应用退出(由 reap 调用)。 */
static void apply_exit(svc_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) {
/* restart 命令:停后原地重启,无视崩溃策略 */
s->restart_pending = 0;
s->stop_requested = 0;
s->restart_count = 0;
s->state = SV_STOPPED;
svcd_log("service %s: restart -> respawn", s->name);
spawn_service(s);
return;
}
if (s->stop_requested) {
s->stop_requested = 0;
s->state = SV_STOPPED;
svcd_log("service %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 = SV_FAILED;
svcd_log("service %s: exceeded max_restart(%d) -> FAILED (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 = SV_STOPPED;
svcd_log("service %s: restart in %ldms (attempt %d, exit=%s)",
s->name, wait, s->restart_count, failed ? "fail" : "ok");
}
} else {
s->state = failed ? SV_FAILED : SV_STOPPED;
svcd_log("service %s: exit (policy=%s, %s) -> %s",
s->name, s->restart, failed ? "fail" : "ok",
failed ? "FAILED" : "STOPPED");
}
}
/* SIGCHLD 处理器:写自管道唤醒 poll(async-signal-safe)。 */
static void sig_child(int sig)
{
(void)sig;
char c = SVCD_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 = SVCD_SIGTERM_CHAR;
ssize_t w = write(g_sigpipe[1], &c, 1);
(void)w;
}
/* 排空 SIGCHLD 自管道,waitpid 匹配所有已退出子进程。 */
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_nsvc; i++)
if (g_svcs[i].pid == pid) { idx = i; break; }
if (idx < 0) { svcd_log("reap unknown pid=%d", (int)pid); continue; }
apply_exit(&g_svcs[idx], status);
}
build_status_file();
}
/* ---- 触发判定 ---- */
static int trigger_satisfied(const svc_t *s)
{
switch (s->trigger[0]) {
case 'c': /* card-mount */
return g_card_present;
case 'w': /* wifi-up */
return g_card_present && g_wifi_up;
case 'b': /* boot:板载服务恒定存在,触发不依赖卡;TF 服务仍需卡 */
default:
return (s->src == 1) ? 1 : g_card_present;
}
}
static int wifi_up(void)
{
DIR *d = opendir("/sys/class/net");
if (!d) return 0;
int up = 0;
struct dirent *e;
while ((e = readdir(d)) != NULL) {
if (e->d_name[0] == '.') continue;
char wp[256], oper[256];
if (snprintf(wp, sizeof wp, "/sys/class/net/%s/wireless", e->d_name)
>= (int)sizeof wp) continue;
if (access(wp, F_OK) != 0) continue; /* 非无线接口 */
if (snprintf(oper, sizeof oper, "/sys/class/net/%s/operstate", e->d_name)
>= (int)sizeof oper) continue;
char buf[16];
if (read_file(oper, buf, sizeof buf) > 0 && strstr(buf, "up")) { up = 1; break; }
}
closedir(d);
return up;
}
/* ---- 卡挂载检测 / 配置扫描 ---- */
static int card_present(void)
{
const char *ov = getenv("MEOW_SVCD_CARD_PRESENT");
if (ov && ov[0]) return ov[0] == '1';
struct stat st, pst;
if (stat(g_card, &st) != 0 || !S_ISDIR(st.st_mode)) return 0;
/* 配置子目录存在 → 有卡内容(设备+宿主 test 通用) */
char sp[512];
if (snprintf(sp, sizeof sp, "%s/%s", g_card, SVCD_SERVICES_REL)
>= (int)sizeof sp) return 0;
if (access(sp, F_OK) == 0) return 1;
/* 真实挂载点:st_dev 与父目录不同(卡文件系统 vs rootfs) */
size_t plen = strlen(g_card);
while (plen > 1 && g_card[plen - 1] == '/') g_card[--plen] = '\0';
size_t slash = 0;
for (size_t i = plen; i > 0; i--)
if (g_card[i - 1] == '/') { slash = i; break; }
if (slash == 0) return 0;
char parent[384];
if ((size_t)snprintf(parent, sizeof parent, "%.*s", (int)(slash - 1), g_card)
>= sizeof parent) return 0;
if (stat(parent, &pst) == 0 && pst.st_dev != st.st_dev) return 1;
return 0; /* 同文件系统空目录 = 未挂载/未用 */
}
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)
{
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; /* 去掉 .conf 得服务名 */
if (nl >= SVCD_NAME_MAX) nl = SVCD_NAME_MAX - 1;
memcpy(out[n], e->d_name, nl);
out[n][nl] = '\0';
n++;
}
closedir(d);
return n;
}
/* 双源目录调和:板载(恒定存在、权威优先)先扫,卡上有则叠加扫 TF,
* 同名服务 TF 被板载屏蔽(WARN 记录)。无双源冲突时行为不变。 */
static void scan_and_reconcile(void)
{
char names[SVCD_MAX_SERVICES][SVCD_NAME_MAX];
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.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; }
idx = g_nsvc++;
}
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];
/* 判定是否需重启(结构字段变化) */
int config_changed = (strcmp(s->exec, tmp.exec) != 0)
|| (s->argc != tmp.argc) || (s->envc != tmp.envc)
|| (s->enabled != tmp.enabled);
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; }
}
/* 保留运行期字段,其余用 tmp 覆盖 */
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;
svc_rebuild_argv(s); /* 重定位 argv[] 到 s 自身成员,防悬垂指针 */
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;
/* 子进程仍存活:配置变 → 停后按新配置重启;不变 → 维持 RUNNING/STOPPING,
* 绝不能被 tmp 的 STOPPED 覆盖成「停了却还有 pid」的泄漏态。 */
if (keep_pid > 0) {
if (config_changed) {
svcd_log("service %s: config changed, restart", s->name);
stop_service(s, 1, 0);
} else {
s->state = (keep_state == SV_STOPPING) ? SV_STOPPING : SV_RUNNING;
}
} else {
s->state = SV_STOPPED;
}
}
/* 已移除配置的服务,倒序删(先停运行中的) */
for (int i = g_nsvc - 1; i >= 0; i--) {
if (g_svcs[i].in_config) continue;
if (g_svcs[i].pid > 0) stop_service(&g_svcs[i], 0, 0);
svcd_log("service %s: removed (no config)", g_svcs[i].name);
remove_svc_idx(i);
}
/* 禁用:运行中 → 停;未运行 → DISABLED */
for (int i = 0; i < g_nsvc; i++) {
svc_t *s = &g_svcs[i];
if (!s->in_config) continue;
if (!s->enabled) {
if (s->pid > 0) { svcd_log("service %s: disabled -> stop", s->name); stop_service(s, 0, 0); }
else s->state = SV_DISABLED;
}
}
/* 曾 FAILED 且现启用 → 重置,交 tick 重试 */
for (int i = 0; i < g_nsvc; i++) {
svc_t *s = &g_svcs[i];
if (s->in_config && s->enabled && s->state == SV_FAILED) {
s->restart_count = 0;
s->state = SV_STOPPED;
s->user_stopped = 0;
}
}
build_status_file();
}
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;
}
svcd_log("card removed: stopping %d services", g_nsvc);
build_status_file();
}
static void tick_evaluate(void)
{
for (int i = 0; i < g_nsvc; i++) {
svc_t *s = &g_svcs[i];
/* STOPPING 先判(含 SIGKILL 兜底),否则 pid>0 分支会吞掉超时逻辑 */
if (s->state == SV_STOPPING) {
if (now_ms() - s->stop_since_ms > SVCD_STOP_GRACE_MS) {
if (s->pid > 0) { kill(s->pid, SIGKILL); s->pid = 0; }
s->state = SV_STOPPED;
svcd_log("service %s: SIGKILL grace timeout", s->name);
}
continue;
}
if (s->pid > 0) {
if (!s->enabled || !trigger_satisfied(s))
stop_service(s, 0, 0);
if (s->pid > 0 && s->state == SV_RUNNING &&
now_ms() - s->run_since_ms > SVCD_STABLE_MS)
s->restart_count = 0; /* 稳定运行 → 清零计数 */
continue;
}
if (s->state == SV_FAILED || s->state == SV_DISABLED) continue;
if (s->restart_at_ms > 0) {
if (now_ms() >= s->restart_at_ms && trigger_satisfied(s)) {
s->restart_at_ms = 0;
svcd_log("service %s: backoff done, respawn", s->name);
spawn_service(s);
}
} else if (s->enabled && trigger_satisfied(s) && !s->user_stopped) {
spawn_service(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_nsvc; i++) {
g_svcs[i].user_stopped = 0;
g_svcs[i].restart_count = 0;
g_svcs[i].state = SV_STOPPED;
spawn_service(&g_svcs[i]);
}
return 0;
}
int idx = find_svc(name);
if (idx < 0) return -1;
svc_t *s = &g_svcs[idx];
s->user_stopped = 0;
if (s->pid <= 0) { s->restart_count = 0; s->state = SV_STOPPED; spawn_service(s); }
return 0;
}
static int cmd_stop(const char *name)
{
if (strcmp(name, "all") == 0) {
for (int i = 0; i < g_nsvc; i++) stop_service(&g_svcs[i], 0, 1);
return 0;
}
int idx = find_svc(name);
if (idx < 0) return -1;
stop_service(&g_svcs[idx], 0, 1);
return 0;
}
static int cmd_restart(const char *name)
{
if (strcmp(name, "all") == 0) {
for (int i = 0; i < g_nsvc; i++) stop_service(&g_svcs[i], 1, 0);
return 0;
}
int idx = find_svc(name);
if (idx < 0) return -1;
svc_t *s = &g_svcs[idx];
s->user_stopped = 0;
if (s->pid <= 0) { s->restart_count = 0; s->state = SV_STOPPED; spawn_service(s); }
else stop_service(s, 1, 0);
return 0;
}
/* ---- status 回显(详细 + ANSI 配色;非 TTY 由客户端去色) ---- */
/* 追加一个字段:可选上色(color 包裹 plain + 复位),再按纯文本宽度补空格对齐。
* width<0 表示最后一列(不补齐)。b 为累积缓冲,*o 为偏移。 */
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; /* 截断保护: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';
}
/* 状态 → 关键色:RUNNING=绿 / FAILED=红 / STOPPED·STOPPING=黄 / DISABLED=灰。 */
static const char *state_color(int st)
{
switch (st) {
case SV_RUNNING: return AN_GREEN;
case SV_FAILED: return AN_RED;
case SV_STOPPED: return AN_YELLOW;
case SV_STOPPING: return AN_YELLOW;
case SV_DISABLED: return AN_GREY;
default: return "";
}
}
/* 解码 waitpid status 为短串:exit=N / sig=N / "-"(未曾退出)。 */
static void fmt_exit_str(const svc_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, "-");
}
/* 上次退出时间 HH:MM:SS;无则 "-"。 */
static void fmt_exit_time(const svc_t *s, char *out, size_t cap)
{
if (!s->has_exit || s->last_exit_wall == 0) { snprintf(out, cap, "-"); return; }
time_t t = (time_t)s->last_exit_wall;
struct tm tm;
if (!localtime_r(&t, &tm)) { snprintf(out, cap, "-"); return; }
strftime(out, cap, "%H:%M:%S", &tm);
}
/* 回显状态:`status`(全部+全局)或 `status <name|all>`(单服务)。 */
static void cmd_status(int fd, const char *name)
{
int idx = -1; /* -1=全部 */
if (name && strcmp(name, "all") != 0) {
idx = find_svc(name);
if (idx < 0) { send_resp(fd, "ERR no-such-service\n"); return; }
}
int shown = (idx >= 0) ? 1 : g_nsvc;
/* NAME 列宽(按纯文本计) */
int nw = 4; /* 至少等于 "NAME" */
for (int i = 0; i < g_nsvc; i++) {
if (idx >= 0 && i != idx) continue;
int l = (int)strlen(g_svcs[i].name);
if (l > nw) nw = l;
}
if (nw > 40) nw = 40; /* 防超宽 */
const int sw = 8, pw = 7, rw = 7, bw = 8, tw = 11, scw = 5, ew = 6, etw = 9;
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;
/* 标题 + 全局汇总(services= 恒为已知总数) */
put_field(b, cap, &o, AN_BOLD AN_CYAN, "meow_svcd status", -1);
put_str(b, cap, &o, "\n");
char hdr[64];
snprintf(hdr, sizeof hdr, "card_present=%d wifi_up=%d services=%d\n",
g_card_present, g_wifi_up, g_nsvc);
put_str(b, cap, &o, hdr);
/* 表头(dim) */
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, "TRIGGER", tw); put_str(b, cap, &o, " ");
put_field(b, cap, &o, AN_DIM, "SRC", scw); 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, "EXIT_TIME", etw); put_str(b, cap, &o, " ");
put_field(b, cap, &o, AN_DIM, "EXEC", -1);
put_str(b, cap, &o, "\n");
/* 分隔线(dim) */
int total = nw + 1 + sw + 1 + pw + 1 + rw + 1 + bw + 1 + tw + 1 + scw + 1 + ew + 1 + etw + 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_nsvc; i++) {
if (idx >= 0 && i != idx) continue;
svc_t *s = &g_svcs[i];
char pidstr[16], rstr[16], bstr[16], estr[16], etstr[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);
fmt_exit_time(s, etstr, sizeof etstr);
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, "", s->trigger, tw); put_str(b, cap, &o, " ");
put_field(b, cap, &o, "", (s->src == 1) ? "board" : "tf", scw); put_str(b, cap, &o, " ");
put_field(b, cap, &o, "", estr, ew); put_str(b, cap, &o, " ");
put_field(b, cap, &o, "", etstr, etw); put_str(b, cap, &o, " ");
put_field(b, cap, &o, "", s->exec, -1);
put_str(b, cap, &o, "\n");
}
/* 分块写回(避免单次大 write 阻塞/截断) */
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[SVCD_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); /* sp=NULL=全部;<name|all>=单服务 */
} 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-service\n");
} else if (sp && strcmp(line, "stop") == 0) {
send_resp(fd, cmd_stop(sp) == 0 ? "OK\n" : "ERR no-such-service\n");
} else if (sp && strcmp(line, "restart") == 0) {
send_resp(fd, cmd_restart(sp) == 0 ? "OK\n" : "ERR no-such-service\n");
} else {
send_resp(fd, "ERR unknown-command\n");
}
}
/* ---- Daemon 生命周期 ---- */
/* 绑定控制 socket。返回 0=成功。 */
static int setup_socket(void)
{
unlink(g_sock);
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) { svcd_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) {
svcd_log("bind %s: %s", g_sock, strerror(errno));
close(fd); return -1;
}
if (listen(fd, 4) != 0) { svcd_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) { svcd_log("pipe: %s", strerror(errno)); return -1; }
/* 双端 O_NONBLOCK:drain 循环读到空即停,绝不阻塞在 read 上(否则 SIGCHLD
* 唤醒 poll → 进入 reap_children → 第二次 read 阻塞,守护进程卡死)。 */
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; /* 无 SA_RESTART:让 poll 中断 */
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); /* 服务关管道不杀 svcd(崩溃隔离一环) */
return 0;
}
static void daemonize(void)
{
pid_t pid = fork();
if (pid < 0) { svcd_log("fork: %s", strerror(errno)); }
if (pid > 0) _exit(0);
if (setsid() < 0) { /* ignore */ }
pid = fork();
if (pid < 0) { svcd_log("fork2: %s", strerror(errno)); }
if (pid > 0) _exit(0);
/* 清 cwd + stdio 到 /dev/null(日志走 g_log,若无则静默丢弃) */
if (chdir("/") != 0) { /* ignore */ }
int nul = open("/dev/null", O_RDWR);
if (nul >= 0) { dup2(nul, 0); dup2(nul, 1); dup2(nul, 2); if (nul > 2) close(nul); }
}
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_nsvc; i++) {
if (g_svcs[i].pid > 0) {
kill(g_svcs[i].pid, SIGTERM);
unsigned long long wait_until = now_ms() + 1500;
while (g_svcs[i].pid > 0 && now_ms() < wait_until) {
int st; pid_t p = waitpid(g_svcs[i].pid, &st, WNOHANG);
if (p == g_svcs[i].pid) { g_svcs[i].pid = 0; }
else usleep(50000);
}
if (g_svcs[i].pid > 0) { kill(g_svcs[i].pid, SIGKILL); g_svcs[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;
/* 设为子进程回收者:被监管服务若派生出孙进程,在服务(直接子进程)退出后
* 由 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();
/* 打开状态文件(留存期读写) */
g_status_fd = open(g_status, O_RDWR | O_CREAT, 0644);
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();
g_board_online = board_services_exist();
scan_and_reconcile(); /* 板载服务恒在,无卡也扫;卡上有则叠加扫 TF */
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;
unsigned long long last_scan = 0;
while (!g_shutdown) {
/* 计算 poll 超时:取最早退避到期与扫描周期的最小值 */
unsigned long long soon = 0;
for (int i = 0; i < g_nsvc; i++) {
if (g_svcs[i].restart_at_ms > 0) {
unsigned long long d = g_svcs[i].restart_at_ms > now_ms()
? g_svcs[i].restart_at_ms - now_ms() : 0;
if (soon == 0 || d < soon) soon = d;
}
}
int timeout = (soon > 0 && soon < (unsigned long long)SVCD_TICK_MAX_MS)
? (int)soon : SVCD_TICK_MAX_MS;
if (timeout < 5) timeout = 5;
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;
svcd_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();
/* 周期扫描卡/wifi(节流到 ~1s);tick_evaluate 每醒来都跑(保 backoff 精度) */
if (now_ms() - last_scan >= (unsigned long long)SVCD_SCAN_MS) {
last_scan = now_ms();
g_wifi_up = wifi_up();
int present = card_present();
if (present != g_card_present) {
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();
}
svcd_log("meow_svcd shutdown");
shutdown_all();
return 0;
}
/* ---- CLI ---- */
/* 就地去除 ANSI 转义序列(\033[...<final>);用于 stdout 非 TTY 时去色。 */
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_svcd] 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) {
/* daemon 未运行 → 显示状态文件(若有) */
fprintf(stderr, "[meow_svcd] 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("card_present=0 services=0\n"); }
return 1;
}
ssize_t w = write(fd, cmdline, strlen(cmdline));
(void)w;
shutdown(fd, SHUT_WR);
/* 累积完整响应(可能含 ANSI 色码),再按 stdout 是否 TTY 决定去色:非 TTY/重定向去色。 */
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 <name|all> | stop <name|all> | restart <name|all>\n"
" | status [name|all] | reload | pid\n"
" no command -> start daemon (default; -f/--foreground keeps fg for supervisor)\n"
" options:\n"
" -f, --foreground stay in foreground (supervisor fork/exec 托管)\n"
" --card <path> TF card mount point (default /usr/data/mnt/sd_0)\n"
" --board <path> board services dir (default /usr/data/meow/services.d)\n"
" --sock <path> control socket (default /tmp/meow_svcd.sock)\n"
" --status <path> status file (default /tmp/meow_svcd.status)\n"
" --pidfile <path> pid file (default /tmp/meow_svcd.pid)\n"
" --log <path> also append svcd log to <path>\n",
argv0);
}
int main(int argc, char **argv)
{
/* 收集命令及其参数:首个非选项 token 为命令,其后非选项 token 拼接为其参数
* (如 `stop all`、`restart foo`)。选项可出现在任意位置。 */
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, "--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]);
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;
}
/* 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);
setvbuf(stderr, NULL, _IONBF, 0);
if (cmd_n > 0) {
char buf[SVCD_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();
}