1164 lines
43 KiB
C
1164 lines
43 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 / reload / pid
|
||
* 后台常驻模式默认(无子命令即启动守护进程);`-f/--foreground` 供 supervisor
|
||
* fork/exec 托管(保证 supervisor 可 waitpid)。
|
||
*
|
||
* 状态文件:`/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 <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'
|
||
|
||
/* ---- 服务状态机 ---- */
|
||
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 */
|
||
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 仅备忘 */ }
|
||
/* 未知键忽略(前向兼容) */
|
||
}
|
||
|
||
/* 解析单个 .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");
|
||
|
||
/* argv[0]=exec;随后 args */
|
||
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_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;
|
||
|
||
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;
|
||
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->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;
|
||
}
|
||
|
||
static void cmd_status(int fd)
|
||
{
|
||
send_resp(fd, "# meow_svcd status\n");
|
||
char line[384];
|
||
int k = snprintf(line, sizeof line, "card_present=%d wifi_up=%d services=%d\n",
|
||
g_card_present, g_wifi_up, g_nsvc);
|
||
send_resp(fd, line);
|
||
(void)k;
|
||
for (int i = 0; i < g_nsvc; i++) {
|
||
svc_t *s = &g_svcs[i];
|
||
snprintf(line, sizeof line,
|
||
"svc=%.63s state=%.16s pid=%d restart=%d exit=%d trigger=%.15s\n",
|
||
s->name, g_state_str[s->state], (int)s->pid, s->restart_count,
|
||
s->last_exit_status, s->trigger);
|
||
send_resp(fd, line);
|
||
}
|
||
}
|
||
|
||
/* 读一条命令并分发。返回响应内容(静态/栈缓冲由调用方使用)。 */
|
||
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);
|
||
} 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;
|
||
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 ---- */
|
||
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);
|
||
char rbuf[8192];
|
||
ssize_t n;
|
||
while ((n = read(fd, rbuf, sizeof rbuf - 1)) > 0) {
|
||
rbuf[n] = '\0';
|
||
fputs(rbuf, stdout);
|
||
}
|
||
close(fd);
|
||
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 | 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();
|
||
}
|