1367 lines
53 KiB
C
1367 lines
53 KiB
C
/* 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;
|
||
int moved = g_nsvc - idx - 1;
|
||
if (moved > 0) {
|
||
memmove(&g_svcs[idx], &g_svcs[idx + 1],
|
||
(size_t)moved * sizeof(svc_t));
|
||
/* memmove 整块字节拷贝 svc_t 后,被左移槽位的 argv[] 指针仍指向移动前
|
||
* (g_svcs[i+1]) 的 exec/argbuf 成员地址 —— 同 *s=tmp 的悬垂,需重定位到
|
||
* 新槽位自身的成员(svc_rebuild_argv)。否则删除一个服务后,其后被左移的
|
||
* 槽若在同 tick 内被拉起,execv 会用错位的 argv → cmdline 错乱。 */
|
||
for (int i = idx; i < idx + moved; i++) svc_rebuild_argv(&g_svcs[i]);
|
||
}
|
||
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();
|
||
}
|