Files
MeowOS-For-R3P2/patch_v2/meow_amsd/meow_amsd.c
T
MeowOS 980d161972 fix(launcher/amsd): amsd MSG_AMS_START 拉起缺口——pending-start 抽 meow_ams_start 模块复用启动链 + 负错误码符修复
根因(真机日志实证,M2.92 遗留):meow_ams_pending_start_set 误返回正错误码
(-MEOW_ERR_BUSY=+8/-MEOW_ERR_INVAL=+1),ipc_dispatch 的 rc<0?-rc:MEOW_OK 把「有前台 App
拒绝/空包名非法」误判为 OK(0) → amsd 报 OK 但 pending 未置位 → App 永不拉起(am start 报 OK
但界面没出现/进程未起)。

launcher:
- 新增自包含模块 meow_ams_start.c/.h(ops 注入 launch=launch_app/launch_activity、
  resolve=resolve_chain、log=logmsg):pending 状态 + 拉起决策 + picker 上下文。
- set 统一返回负 MEOW_ERR_*(根因修复);consume 复用既有启动链路真正拉起目标 App,
  列表未就绪(state!=0)保留 pending 待 2s 重扫重试,不再静默丢弃。
- main_menu run_menu 改经 meow_ams_start_consume 消费(保留 launch_fail_toast/菜单收尾);
  app_launcher 移除旧 g_ams_start/g_ams_pick_* 迁入模块;util meow_ipc_process 顶部同步 busy、
  meow_ipc_init 末尾 meow_ams_launcher_init()。
- build_launcher.sh 编入 meow_ams_start.c;meow_launcher_internal.h 更新挂点声明。

amsd(协议层归属说明):cmd_start 的 busy 判定对照 launcher ACK 正幅度(busy=8),
原 (uint32_t)MEOW_ERR_BUSY=0xFFFFFFF8 永不匹配 → busy 落入 launcher offline,改 (uint32_t)(-MEOW_ERR_BUSY)。

验证:宿主 test_ams_start.c 33 断言全 PASS(合法->拉起链、activity chain 透传、空包名/非法/busy
不拉起不崩、列表未就绪保留 pending、无路由 NO ROUTE、启动失败 idx 回报、picker 取回);
launcher 目录全量回归 test_media/bgplay/depends/notif_panel/notif_tf/settings_brightness/
vol_ramp_jack/host_icon + test_font_tier/weight 全 PASS;mipsel -Wall -Wextra 0 warning。
新 md5:meow_launcherd a8200ae1b0aa68d7262749db87377d49、meow_amsd 7ddc677ce7938bc9b95c83c17bc78295。(M2.98 待打包,未刷机)
2026-09-01 19:40:29 +08:00

1551 lines
62 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_amsd.c — MeowOS 活动管理器守护(AMSD_DESIGN.md,一期)。
*
* 定位(用户定案,见 devdocs/plan/AMSD_DESIGN.md / FRAMEWORK_DESIGN §10 /
* devdocs/plan/SYS_BASELINE.md / APPS.md 附录):
* - svcd 托管的小守护(板载 /usr/bin/meow_amsd + services.d 板载 conf,trigger=boot);
* svcd fork+exec 直接拉起,故本进程【前台常驻】(不 daemonize),与 meow_crash 同款。
* - 一期:生命周期监控中心 + 应用注册表。launcher 经 AF_UNIX `/usr/data/meow_ams.sock`(0666)
* 上报 launch/exit/crash 事件;`am` CLI 同 socket 查询/下发 start/kill。
* - 注册表 = 启动首扫 + 周期合并重扫(卡未挂载扫空 3s 快重试、常态 30s 自愈;
* `am rescan` 手动触发)。重扫合并式,绝不清运行时状态(M2.90 真机扫空缺陷修复)。
* - 核心目的:从侧面详细抓 App 日志(尤其异常崩溃日志)。launcher 给 App 建 stdout/stderr
* 管道,前台循环按行转发(EV OUT);amsd 落盘 `logs/<pkg>/stdout_<ts>_<pid>.log`。
* - 崩溃双保险:amsd 检测到异常退出时另以 meow_crash 认识的 `SVC ...` 格式转发
* `/tmp/meow_crash.sock`(meow_crash.c handle_conn 只认 SVC 前缀;launcher 现行 `app ...`
* 格式落不了盘,amsd 的 SVC 转发正好补上)。
* - 落盘:TF 卡 `MeowOS/logs/`(amsd 只清 `ams_` 与 `stdout_` 前缀文件,不与 meow_crash 的
* `meow_` 与 `crash_` 前缀清理冲突)。无卡回退 /tmp(256KB 滚动);卡出现自动迁移。
* - 铁律:上报/转发全程非阻塞、可失败(amsd 不在,launcher 照常跑);amsd 自身绝不因
* App 事件而崩(只改内存表 + poll,不 exit)。
*
* 编译(mipsel 静态,见 build_amsd.sh):
* zig-mipsel-cc -I../meow_launcherd -Os -Wall -Wextra -static meow_amsd.c -o meow_amsd
* 宿主编译(集成测试):gcc -I../meow_launcherd -Os -Wall -Wextra meow_amsd.c -o meow_amsd_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 <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <poll.h>
#include "meow_ipc.h" /* MEOW_IPC 帧/MAGIC/m_ams_* 结构(与 launcher 同源,杜绝漂移) */
/* ---- 默认路径 / 常量 ---- */
#define AMS_CARD_DEFAULT "/usr/data/mnt/sd_0" /* TF 卡挂载点 */
#define AMS_LOGS_REL "MeowOS/logs" /* 卡上日志目录(同 launcher/crash) */
#define AMS_SOCK_DEFAULT "/usr/data/meow_ams.sock"
#define AMS_IPC_SOCK_DEFAULT "/usr/data/meow_ipc.sock" /* 连 launcher IPC 用 */
#define AMS_APPS_DEFAULT "/usr/data/mnt/sd_0/MeowAPPs" /* 注册表扫描源 */
#define AMS_CRASH_SOCK_DEFAULT "/tmp/meow_crash.sock"
#define AMS_TMP_LOG "/tmp/meow_ams.log"
#define AMS_TMP_MAX (256u * 1024u)
#define AMS_LOG_SIZE_LIMIT (1u << 30) /* logs/ 总量 1GB 纪律 */
#define AMS_FILE_MAX (1u << 20) /* stdout 单文件 1MB 轮转 */
#define AMS_LINE_MAX 2048 /* 单行 read 缓冲 */
#define AMS_HIST 8 /* 每 App 最近事件数 */
#define AMS_MAX_APPS 64
#define AMS_MAX_CONNS 32
#define AMS_MAX_ROUTES 8 /* 每 App 路由数(MEOW_MAX_ROUTES 对齐) */
#define AMS_MAX_PICKERS 8 /* picker action → handler 注册表 */
#define AMS_MAX_PENDING 8 /* 进行中 picker 请求(结果关联) */
#define AMS_TICK_MS 1000
#define AMS_CLEANUP_MS 60000
#define AMS_RESCAN_MS 30000 /* 常态合并重扫周期(卡热插拔/App 热安装后注册表自愈) */
#define AMS_RESCAN_RETRY_MS 3000 /* 扫描源尚未成功打开(开机早于卡挂载扫空)→ 快重试 */
#define AMS_PATH_MAX 512
#define AMS_SOCK_PATH_MAX 108
#define AMS_BOOTTS_LEN 64
#define AMS_REQ_MAX 512 /* 命令响应缓冲 */
#define AMS_IPC_TIMEOUT_MS 1200 /* launcher IPC 单次收发超时:amsd 绝不为一次转发阻塞无限久 */
#define AMS_IPC_RETRY 2 /* launcher 一次性不可达/超时 → 短退避重连一次 */
#define AMS_IPC_BACKOFF_MS 250 /* 重连退避基数 */
/* 每 App 状态(一期:STOPPED/FG;BG 预留二期) */
enum { AMS_STOPPED = 0, AMS_FG = 1, AMS_BG = 2 };
struct ams_ev {
char detail[192]; /* 事件描述(exit=0 / sig=11 / ams_start=... 等) */
long epoch; /* 墙钟秒 */
long up; /* uptime 秒 */
char kind[16]; /* launch / exit / crash / ams_start / ams_kill / picker_result */
};
/* 路由行(来自 app.json route.<id>=<chain>;运行时经 DECLARE 增量并集)。 */
struct ams_route {
char act[16]; /* route 键后缀 */
char chain[MEOW_CHAIN_MAX];
};
struct ams_app {
char pkg[MEOW_APP_ID_MAX]; /* app_id=package */
char name[17];
char exec[129];
int known; /* 注册表(app.json 扫描)命中 */
int state; /* AMS_* */
pid_t pid;
char activity[MEOW_ACT_MAX];
char chain[MEOW_CHAIN_MAX];
long long launch_ms; /* CLOCK_MONOTONIC ms */
long launch_epoch; /* 墙钟秒 */
long launch_up;
int launches;
int crashes; /* 异常退出次数(sig 或 exit!=0) */
int last_kind; /* 0=干净 exit 1=exit 2=sig */
int last_code;
long long last_dur_ms;
/* §6.2 二期:路由权威(resolve 用;静态 app.json + 运行时 DECLARE 并集覆盖) */
int nroute;
struct ams_route routes[AMS_MAX_ROUTES];
struct ams_ev hist[AMS_HIST];
int hist_n, hist_idx;
};
/* picker action → handler app(app.json picker.<action>=<chain>;amsd 权威路由)。 */
struct ams_picker {
char action[24];
char app_id[MEOW_APP_ID_MAX];
char chain[MEOW_CHAIN_MAX]; /* handler 入口 activity 链(v2 常空=裸启动,handler 读 env) */
int used; /* 注册表命中 */
};
/* 进行中 picker 请求(`am picker`/App 发起;结果经 PICKER_RESULT 关联)。 */
struct ams_pending {
int active;
uint32_t req_id;
char caller[MEOW_APP_ID_MAX];
char action[24];
char app_id[MEOW_APP_ID_MAX]; /* handler app(记录用) */
int done; /* 结果已回 */
int code;
char value[256];
};
/* stdout 捕获会话(一期同一时刻仅一个前台 App → 单槽) */
struct ams_out {
int active;
char pkg[MEOW_APP_ID_MAX];
pid_t pid;
int fd; /* 打开的日志文件 fd */
char base[AMS_PATH_MAX]; /* 当前文件路径 */
long long bytes; /* 当前文件字节数 */
};
struct ams_out g_out = { .active = 0, .fd = -1 };
/* ---- 全局状态 ---- */
static char g_card[256] = AMS_CARD_DEFAULT;
static char g_sock[AMS_SOCK_PATH_MAX] = AMS_SOCK_DEFAULT;
static char g_ipc_sock[AMS_SOCK_PATH_MAX] = AMS_IPC_SOCK_DEFAULT;
static char g_apps_dir[AMS_PATH_MAX] = AMS_APPS_DEFAULT;
static char g_crash_sock[AMS_SOCK_PATH_MAX] = AMS_CRASH_SOCK_DEFAULT;
static char g_bootts[AMS_BOOTTS_LEN] = "";
static int g_log_fd = -1; /* 卡日志 fd;-1=未开(落 /tmp) */
static int g_on_card = 0;
static int g_shutdown = 0;
static int g_sigpipe[2] = {-1, -1};
static int g_listen_fd = -1;
static struct ams_app g_apps[AMS_MAX_APPS];
static int g_napps = 0;
static struct ams_picker g_pickers[AMS_MAX_PICKERS]; /* §6.2 二期:picker 权威路由表 */
static int g_npickers = 0;
static struct ams_pending g_pending[AMS_MAX_PENDING]; /* §6.2 二期:进行中 picker */
static uint32_t g_pick_req_seq = 1; /* req_id 单调分配 */
struct ams_conn {
int fd;
char buf[AMS_LINE_MAX];
int n; /* buf 有效字节(可能含未完结行,跨 poll 保留) */
char resp[AMS_REQ_MAX];
int resp_n; /* 已产生的命令响应(写回后即关连接) */
};
/* ---- 工具 ---- */
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;
}
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;
}
/* 有界字符串拷贝(memcpy 实现,不触发 gcc -Wformat-truncation)。 */
static size_t scopy(char *dst, size_t cap, const char *src)
{
if (!dst || cap == 0) { if (dst) dst[0] = '\0'; return 0; }
size_t l = strnlen(src ? src : "", cap - 1);
if (src) memcpy(dst, src, l);
dst[l] = '\0';
return l;
}
/* 把 src 追加到 dst 末尾(有界,memcpy 实现)。返回 0=成功;-1=溢出(截断到 cap-1)。 */
static int path_append(char *dst, size_t cap, const char *src)
{
size_t dl = strnlen(dst, cap);
size_t sl = strnlen(src ? src : "", cap > dl + 1 ? cap - dl - 1 : 0);
if (dl + sl + 1 > cap) {
/* 溢出:截断并保存 NUL */
if (cap > 0) { dst[cap - 1] = '\0'; }
return -1;
}
if (sl) memcpy(dst + dl, src, sl);
dst[dl + sl] = '\0';
return 0;
}
static long read_uptime_sec(void)
{
int fd = open("/proc/uptime", O_RDONLY);
if (fd < 0) return 0;
char b[64];
ssize_t n = read(fd, b, sizeof b - 1);
close(fd);
if (n <= 0) return 0;
b[n] = '\0';
return (long)atof(b);
}
/* 本 boot 时间戳 "%Y%m%d_%H%M%S":复用 /tmp/meow_bootts(launcher/crash 共用,跨进程同一 boot)。 */
static void boot_ts(char *out, size_t n)
{
out[0] = '\0';
int fd = open("/tmp/meow_bootts", O_RDONLY);
if (fd >= 0) {
char b[AMS_BOOTTS_LEN];
ssize_t r = read(fd, b, sizeof b - 1);
close(fd);
if (r > 0) {
b[r] = '\0';
char *p = b;
if (strncmp(p, "meow_", 5) == 0) p += 5;
snprintf(out, n, "%s", p);
if (out[0]) return;
}
}
time_t now = time(NULL);
time_t up = (time_t)read_uptime_sec();
time_t boot = now - up;
struct tm tm;
localtime_r(&boot, &tm);
strftime(out, n, "%Y%m%d_%H%M%S", &tm);
}
static void ams_append_line(const char *line)
{
if (g_log_fd >= 0) {
ssize_t w = write(g_log_fd, line, strlen(line));
if (w < 0) { close(g_log_fd); g_log_fd = -1; g_on_card = 0; }
else return;
}
struct stat st;
if (stat(AMS_TMP_LOG, &st) == 0 && st.st_size > (long long)AMS_TMP_MAX)
unlink(AMS_TMP_LOG);
int fd = open(AMS_TMP_LOG, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd >= 0) { ssize_t w = write(fd, line, strlen(line)); (void)w; close(fd); }
}
static void emit_log(const char *fmt, ...)
{
char line[AMS_LINE_MAX];
long epoch = (long)time(NULL);
long up = read_uptime_sec();
va_list ap;
va_start(ap, fmt);
int l = snprintf(line, sizeof line, "%ld %ld amsd ", epoch, up);
if (l < 0) l = 0;
int m = vsnprintf(line + l, sizeof line - l, fmt, ap);
va_end(ap);
if (m > 0) {
l += m;
if ((size_t)l < sizeof line) { line[l++] = '\n'; line[l] = 0; ams_append_line(line); }
}
}
/* ---- 卡可用性 / 持久日志 ---- */
static int card_available(void)
{
const char *nc = getenv("MEOW_AMSD_NOCARD");
if (nc && nc[0] == '1') return 0;
char logs[AMS_PATH_MAX];
if (snprintf(logs, sizeof logs, "%s/%s", g_card, AMS_LOGS_REL) >= (int)sizeof logs) return 0;
if (strcmp(g_card, AMS_CARD_DEFAULT) == 0) {
FILE *f = fopen("/proc/mounts", "r");
if (!f) return 0;
char needle[AMS_PATH_MAX], line[AMS_PATH_MAX];
snprintf(needle, sizeof needle, " %s ", g_card);
int found = 0;
while (fgets(line, sizeof line, f))
if (strstr(line, needle)) { found = 1; break; }
fclose(f);
if (!found) return 0;
}
(void)mkdir(logs, 0755);
return access(logs, W_OK) == 0;
}
/* 只对 ams_ 前缀与 stdout_ 前缀文件做 1GB 清理(不与 meow_crash 的 meow_/crash_ 前缀冲突)。 */
static void log_cleanup_old(void)
{
char logs[AMS_PATH_MAX];
if (snprintf(logs, sizeof logs, "%s/%s", g_card, AMS_LOGS_REL) >= (int)sizeof logs) return;
DIR *d = opendir(logs);
if (!d) return;
struct lf { char name[96]; long long size, mtime; } files[128];
int n = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL && n < 128) {
const char *nm = de->d_name;
if (strncmp(nm, "ams_", 4) != 0 && strncmp(nm, "stdout_", 7) != 0) continue;
char path[AMS_PATH_MAX];
if (snprintf(path, sizeof path, "%s/%s", logs, nm) >= (int)sizeof path) continue;
struct stat st;
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) continue;
size_t nl = strlen(nm);
if (nl >= sizeof files[n].name) nl = sizeof files[n].name - 1;
memcpy(files[n].name, nm, nl);
files[n].name[nl] = '\0';
files[n].size = (long long)st.st_size;
files[n].mtime = (long long)st.st_mtime;
n++;
}
closedir(d);
long long total = 0;
for (int i = 0; i < n; i++) total += files[i].size;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (files[j].mtime < files[i].mtime) { struct lf t = files[i]; files[i] = files[j]; files[j] = t; }
int deleted = 0;
for (int i = 0; i < n && total > (long long)AMS_LOG_SIZE_LIMIT; i++) {
char path[AMS_PATH_MAX];
if (snprintf(path, sizeof path, "%s/%s", logs, files[i].name) >= (int)sizeof path) continue;
if (unlink(path) == 0) { total -= files[i].size; deleted++; }
}
if (deleted) fprintf(stderr, "[meow_amsd] log_cleanup deleted %d old log(s)\n", deleted);
}
static int log_open_card(void)
{
if (g_log_fd >= 0) return 0;
if (!card_available()) return -1;
char ts[AMS_BOOTTS_LEN];
boot_ts(ts, sizeof ts);
char path[AMS_PATH_MAX];
if (snprintf(path, sizeof path, "%s/%s/ams_%s.log", g_card, AMS_LOGS_REL, ts)
>= (int)sizeof path) return -1;
int fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd < 0) return -1;
g_log_fd = fd; g_on_card = 1;
return 0;
}
static void poll_card(void)
{
if (g_log_fd >= 0) return;
if (log_open_card() != 0) return;
int fd = open(AMS_TMP_LOG, O_RDONLY);
if (fd >= 0) {
char chunk[1024]; ssize_t cn;
while ((cn = read(fd, chunk, sizeof chunk)) > 0) {
if (write(g_log_fd, chunk, cn) < 0) { close(fd); g_log_fd = -1; g_on_card = 0; return; }
}
close(fd);
unlink(AMS_TMP_LOG);
fprintf(stderr, "[meow_amsd] /tmp buffered log moved to card\n");
}
}
/* ---- 注册表扫描(app.json 迷你解析:package/name/exec) ---- */
static void appjson_get(const char *content, const char *key, char *out, size_t n)
{
out[0] = '\0';
char pat[64];
snprintf(pat, sizeof pat, "\"%s\"", key);
const char *p = strstr(content, pat);
if (!p) return;
p += strlen(pat);
const char *c = strchr(p, ':');
if (!c) return;
c++;
while (*c == ' ' || *c == '\t') c++;
if (*c != '"') return;
c++;
const char *e = strchr(c, '"');
if (!e) return;
size_t l = (size_t)(e - c);
if (l >= n) l = n - 1;
memcpy(out, c, l);
out[l] = '\0';
}
/* §6.2 二期:在 content 二分/扁平解析 route.<id>=<chain>(或 JSON "route.<id>":"<chain>")到 app。
* 跳过 route.<id>.fullscreen / .keep_awake 子键;chain 取直链(';' 分隔保持原样)。 */
static void ams_parse_routes(const char *content, struct ams_app *a)
{
if (!content || !a) return;
const char *p = content;
while ((p = strstr(p, "route.")) != NULL) {
const char *q = p + 6;
char id[16] = "";
int idl = 0;
while (*q && *q != '"' && *q != ':' && *q != '=' && *q != '.' && idl < 15)
id[idl++] = *q++;
id[idl] = 0;
if (idl > 0 && *q == '.') { p = q; continue; } /* route.<id>.xxx 子键,跳过 */
if (idl > 0) {
const char *vp = q;
while (*vp && *vp != ':' && *vp != '=') vp++;
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++;
char chain[MEOW_CHAIN_MAX] = "";
int vl = 0;
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
*vp != '\r' && vl < MEOW_CHAIN_MAX - 1)
chain[vl++] = *vp++;
chain[vl] = 0;
while (vl > 0 && (chain[vl - 1] == ' ' || chain[vl - 1] == '\t')) chain[--vl] = 0;
int ri = -1;
for (int i = 0; i < a->nroute; i++)
if (!strcmp(a->routes[i].act, id)) { ri = i; break; }
if (ri < 0) {
if (a->nroute >= AMS_MAX_ROUTES) { p = vp; continue; }
ri = a->nroute++;
}
scopy(a->routes[ri].act, sizeof a->routes[ri].act, id);
scopy(a->routes[ri].chain, sizeof a->routes[ri].chain, chain);
p = vp;
} else {
p += 6;
}
}
}
/* §6.2 二期:解析 picker.<action>=<chain> 到全局 picker 表(action → handler app_id)。
* app_id = 当前扫描目录名(=该 app.json 所属 App)。链 v2 常空=裸启动(handler 读 env)。 */
static void ams_parse_pickers(const char *content, const char *app_id)
{
if (!content || !app_id) return;
const char *p = content;
while ((p = strstr(p, "picker.")) != NULL) {
const char *q = p + 7;
char action[24] = "";
int al = 0;
while (*q && *q != '"' && *q != ':' && *q != '=' && *q != '.' && al < 23)
action[al++] = *q++;
action[al] = 0;
if (al > 0) {
const char *vp = q;
while (*vp && *vp != ':' && *vp != '=') vp++;
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++;
char chain[MEOW_CHAIN_MAX] = "";
int vl = 0;
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
*vp != '\r' && vl < MEOW_CHAIN_MAX - 1)
chain[vl++] = *vp++;
chain[vl] = 0;
struct ams_picker *pk = NULL;
for (int i = 0; i < g_npickers; i++)
if (!strcmp(g_pickers[i].action, action)) { pk = &g_pickers[i]; break; }
if (!pk && g_npickers < AMS_MAX_PICKERS) {
pk = &g_pickers[g_npickers++];
memset(pk, 0, sizeof *pk);
scopy(pk->action, sizeof pk->action, action);
}
if (pk) {
scopy(pk->app_id, sizeof pk->app_id, app_id);
scopy(pk->chain, sizeof pk->chain, chain);
pk->used = 1;
}
}
p = q;
}
}
/* 扫描源至少成功打开过一次(空表快重试的终止条件;卡未挂载时恒 0)。 */
static int g_scan_ok = 0;
static struct ams_app *app_find(const char *pkg); /* 定义在后(App 表辅助) */
/* 扫描 --apps 下每个目录(app_id=目录名;name/exec 从 app.json 抽,缺省回退)。
* 合并式(merge-preserving):已存在表项只刷新静态字段(known/name/exec/route 表/picker 表),
* 运行时状态(state/pid/launches/crashes/history、运行时 DECLARE 并集进来的路由)一律保留;
* 新目录追加建项。扫描源不可达(开机早于卡挂载/卡热拔)→ 保留现有表直接返回,绝不清空。
* verbose=1(启动/`am rescan`)记全量明细;周期调用仅在结果变化时记一行摘要(防刷屏)。 */
static void registry_scan(int verbose)
{
static int last_found = -1, last_napps = -1, last_npick = -1;
DIR *d = opendir(g_apps_dir);
if (!d) {
if (verbose || !g_scan_ok)
emit_log("registry scan: %s not reachable (%s)", g_apps_dir, strerror(errno));
return;
}
g_scan_ok = 1;
for (int i = 0; i < g_napps; i++) g_apps[i].known = 0;
for (int i = 0; i < g_npickers; i++) g_pickers[i].used = 0;
struct dirent *de;
int found = 0;
while ((de = readdir(d)) != NULL) {
if (de->d_name[0] == '.') continue;
char nm[96];
scopy(nm, sizeof nm, de->d_name);
char dir[AMS_PATH_MAX + 64];
scopy(dir, sizeof dir, g_apps_dir);
path_append(dir, sizeof dir, "/");
if (path_append(dir, sizeof dir, nm) != 0) continue; /* 路径过长 → 跳过 */
struct stat st;
if (stat(dir, &st) != 0 || !S_ISDIR(st.st_mode)) continue;
char jpath[AMS_PATH_MAX + 64];
scopy(jpath, sizeof jpath, dir);
if (path_append(jpath, sizeof jpath, "/app.json") != 0) continue;
char content[2048];
content[0] = '\0';
FILE *jf = fopen(jpath, "r");
if (jf) {
size_t rd = fread(content, 1, sizeof content - 1, jf);
fclose(jf);
content[rd] = '\0';
}
/* app_id 权威 = 目录名(APPS.md §2);已有表项(含运行时 EV/DECLARE 建的)原位刷新 */
struct ams_app *a = app_find(nm);
if (!a) {
if (g_napps >= AMS_MAX_APPS) continue;
a = &g_apps[g_napps++];
memset(a, 0, sizeof *a);
scopy(a->pkg, sizeof a->pkg, nm);
}
a->known = 1;
char tmp[96];
appjson_get(content, "name", tmp, sizeof tmp);
if (!tmp[0]) scopy(tmp, sizeof tmp, nm);
scopy(a->name, sizeof a->name, tmp);
appjson_get(content, "exec", tmp, sizeof tmp);
if (tmp[0]) scopy(a->exec, sizeof a->exec, tmp);
else if (!a->exec[0] || !strcmp(a->exec, "?")) {
scopy(a->exec, sizeof a->exec, dir);
path_append(a->exec, sizeof a->exec, "/meow");
}
/* §6.2 二期:路由权威表(静态 app.json route.*)+ picker 表(picker.<action>=<chain>) */
ams_parse_routes(content, a);
ams_parse_pickers(content, a->pkg);
found++;
}
closedir(d);
if (verbose || found != last_found || g_napps != last_napps || g_npickers != last_npick) {
emit_log("registry scan: %d dir(s) from %s, %d app(s) registered",
found, g_apps_dir, g_napps);
if (verbose) {
for (int i = 0; i < g_napps; i++) {
struct ams_app *a = &g_apps[i];
emit_log(" app %s name=%s exec=%s known=%d routes=%d",
a->pkg, a->name, a->exec, a->known, a->nroute);
for (int r = 0; r < a->nroute; r++)
emit_log(" route %s=%s", a->routes[r].act, a->routes[r].chain);
}
for (int i = 0; i < g_npickers; i++)
if (g_pickers[i].used)
emit_log(" picker %s -> %s (chain=%s)",
g_pickers[i].action, g_pickers[i].app_id, g_pickers[i].chain);
}
}
last_found = found; last_napps = g_napps; last_npick = g_npickers;
}
/* ---- App 表辅助 ---- */
static struct ams_app *app_find(const char *pkg)
{
if (!pkg) return NULL;
for (int i = 0; i < g_napps; i++)
if (!strcmp(g_apps[i].pkg, pkg)) return &g_apps[i];
return NULL;
}
/* get-or-create:launcher 上报可能先于注册表扫描(或 pkg 未扫到),追加建项。 */
static struct ams_app *app_get(const char *pkg)
{
struct ams_app *a = app_find(pkg);
if (a) return a;
if (g_napps >= AMS_MAX_APPS) return NULL;
a = &g_apps[g_napps++];
memset(a, 0, sizeof *a);
snprintf(a->pkg, sizeof a->pkg, "%s", pkg);
snprintf(a->name, sizeof a->name, "%s", pkg);
snprintf(a->exec, sizeof a->exec, "%s", "?");
return a;
}
/* ---- §6.2 二期:路由权威(resolve/declare/picker)---- */
/* 路由规则与 launcher resolve_chain 一致(FRAMEWORK_DESIGN §2):
* route 缺失(app 未声明任何路由)→ chain=activity、深度 1 兜底;
* 声明了路由但 activity 非 route 键 → -MEOW_ERR_NOENT。返回 0=成功。 */
static int amsd_resolve_chain(const char *app_id, const char *activity, char *out, int *depth)
{
if (!out || !depth) return MEOW_ERR_INVAL;
if (!activity) activity = "";
struct ams_app *a = app_find(app_id);
if (!a) return MEOW_ERR_NOENT;
if (a->nroute == 0) {
snprintf(out, MEOW_CHAIN_MAX, "%s", activity);
*depth = activity[0] ? 1 : 0;
return MEOW_OK;
}
for (int i = 0; i < a->nroute; i++) {
if (!strcmp(a->routes[i].act, activity)) {
snprintf(out, MEOW_CHAIN_MAX, "%s", a->routes[i].chain);
const char *c = a->routes[i].chain;
int d = c[0] ? 1 : 0;
for (; *c; c++) if (*c == ';') d++;
*depth = d;
return MEOW_OK;
}
}
return MEOW_ERR_NOENT;
}
/* 应用一组运行时 route entries(NUL 分隔 "id=chain")到 ams_app(并集覆盖,同 launcher)。 */
static void amsd_apply_routes(struct ams_app *a, const char *entries)
{
const char *p = entries;
while (*p) {
const char *eq = strchr(p, '=');
if (eq && eq > p) {
int idlen = (int)(eq - p);
if (idlen >= 16) idlen = 15;
char id[16];
memcpy(id, p, idlen); id[idlen] = 0;
const char *chain = eq + 1;
int ri = -1;
for (int i = 0; i < a->nroute; i++)
if (!strcmp(a->routes[i].act, id)) { ri = i; break; }
if (ri < 0) {
if (a->nroute >= AMS_MAX_ROUTES) { p += strlen(p) + 1; continue; }
ri = a->nroute++;
}
scopy(a->routes[ri].act, sizeof a->routes[ri].act, id);
scopy(a->routes[ri].chain, sizeof a->routes[ri].chain, chain);
}
p += strlen(p) + 1;
}
}
/* hex 解码(小写/大写 2 字符/字节)到 out[outsz],返回字节数(含尾部 NUL 由调用方处理)。 */
static int ams_hex_decode(const char *hex, char *out, int outsz)
{
int n = 0;
const char *p = hex;
while (p[0] && p[1] && n < outsz - 1) {
int hi = (p[0] >= '0' && p[0] <= '9') ? p[0] - '0'
: (p[0] >= 'a' && p[0] <= 'f') ? p[0] - 'a' + 10
: (p[0] >= 'A' && p[0] <= 'F') ? p[0] - 'A' + 10 : -1;
int lo = (p[1] >= '0' && p[1] <= '9') ? p[1] - '0'
: (p[1] >= 'a' && p[1] <= 'f') ? p[1] - 'a' + 10
: (p[1] >= 'A' && p[1] <= 'F') ? p[1] - 'A' + 10 : -1;
if (hi < 0 || lo < 0) break;
out[n++] = (char)((hi << 4) | lo);
p += 2;
}
out[n] = 0;
return n;
}
static void hist_push(struct ams_app *a, const char *kind, const char *detail,
long epoch, long up)
{
struct ams_ev *e = &a->hist[a->hist_idx];
snprintf(e->kind, sizeof e->kind, "%s", kind);
snprintf(e->detail, sizeof e->detail, "%s", detail);
e->epoch = epoch;
e->up = up;
a->hist_idx = (a->hist_idx + 1) % AMS_HIST;
if (a->hist_n < AMS_HIST) a->hist_n++;
}
/* ---- meow_crash 转发(SVC 格式,best-effort) ---- */
static void crash_forward(const char *pkg, const char *kind, int code, long epoch, long up)
{
char msg[256];
int l = snprintf(msg, sizeof msg, "SVC %s %s %d %ld %ld\n", pkg, kind, code, epoch, up);
if (l <= 0) return;
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return;
int fl = fcntl(fd, F_GETFL, 0);
if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, g_crash_sock, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) {
if (errno == EINPROGRESS || errno == EWOULDBLOCK) {
struct pollfd pf = { fd, POLLOUT, 0 };
int r = poll(&pf, 1, 150);
if (r <= 0 || !(pf.revents & POLLOUT)) { close(fd); return; }
} else { close(fd); return; }
}
ssize_t w = write(fd, msg, (size_t)l);
(void)w;
close(fd);
}
/* ---- stdout 捕获会话 ---- */
static void out_close(void)
{
if (g_out.fd >= 0) close(g_out.fd);
memset(&g_out, 0, sizeof g_out);
g_out.fd = -1;
}
static void out_open(const char *pkg, pid_t pid, long epoch)
{
out_close();
/* 路径:<card>/MeowOS/logs/<pkg>/stdout_<epoch>_<pid>.log(与 SDK 日志同目录) */
char logs[AMS_PATH_MAX], pdir[AMS_PATH_MAX], base[AMS_PATH_MAX];
if (snprintf(logs, sizeof logs, "%s/%s", g_card, AMS_LOGS_REL) >= (int)sizeof logs) return;
if (snprintf(pdir, sizeof pdir, "%s/%s", logs, pkg) >= (int)sizeof pdir) return;
(void)mkdir(logs, 0755);
(void)mkdir(pdir, 0755);
if (snprintf(base, sizeof base, "%s/stdout_%ld_%ld.log", pdir, epoch, (long)pid)
>= (int)sizeof base) return;
int fd = open(base, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd < 0) { emit_log("stdout open fail %s: %s", base, strerror(errno)); return; }
snprintf(g_out.pkg, sizeof g_out.pkg, "%s", pkg);
g_out.pid = pid;
g_out.fd = fd;
snprintf(g_out.base, sizeof g_out.base, "%s", base);
struct stat st;
g_out.bytes = (stat(base, &st) == 0) ? (long long)st.st_size : 0;
g_out.active = 1;
emit_log("stdout session open: %s (pid=%ld) -> %s", pkg, (long)pid, base);
}
/* 超 1MB 轮转:base -> base.1(覆盖),重开 base 续写。 */
static void out_rotate(void)
{
if (g_out.fd < 0) return;
close(g_out.fd);
char old[AMS_PATH_MAX];
if (snprintf(old, sizeof old, "%s.1", g_out.base) < (int)sizeof old)
(void)rename(g_out.base, old);
int fd = open(g_out.base, O_WRONLY | O_APPEND | O_CREAT, 0644);
g_out.fd = fd;
g_out.bytes = 0;
emit_log("stdout rotate -> %s.1", g_out.base);
}
static void out_append(const char *pkg, pid_t pid, long epoch, long up, const char *text)
{
if (!g_out.active || g_out.fd < 0) return;
if (strcmp(g_out.pkg, pkg) != 0 || g_out.pid != pid) return; /* 会话不匹配,忽略 */
char line[AMS_LINE_MAX];
int l = snprintf(line, sizeof line, "[%ld|up=%ld] %s\n", epoch, up, text ? text : "");
if (l <= 0) return;
if (g_out.bytes + l > (long long)AMS_FILE_MAX) out_rotate();
ssize_t w = write(g_out.fd, line, (size_t)l);
if (w > 0) g_out.bytes += w;
}
/* ---- launcher IPC 转发(xchg 一帧,读回一帧) ---- */
static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
uint8_t *rbuf, uint32_t rcap)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
/* 收发加超时:launcher 刚刚重启/主循环未就绪(接受但不回包)时,amsd 的阻塞
* read 会永远挂起,冻结整个单线程守护。设 SO_SNDTIMEO/SO_RCVTIMEO 让单次转发
* 有界,超时即返回失败 → ams_request_ctl 退避重连,绝不无限阻塞。
* 注意:tv_usec 必须 < 1000000(否则 setsockopt 返回 EINVAL,超时静默失效)。 */
struct timeval tv;
tv.tv_sec = AMS_IPC_TIMEOUT_MS / 1000;
tv.tv_usec = (AMS_IPC_TIMEOUT_MS % 1000) * 1000;
(void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
(void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, g_ipc_sock, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { close(fd); return -1; }
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC, rmagic, rlen;
uint32_t reqid = 0; /* amsd→launcher 控制请求无需回显 */
uint16_t ver = MEOW_IPC_VERSION, rver, rtype;
memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2);
memcpy(hdr + 6, &type, 2); memcpy(hdr + 8, &reqid, 4);
memcpy(hdr + 12, &len, 4);
size_t off = 0;
while (off < sizeof hdr) {
ssize_t w = write(fd, hdr + off, sizeof hdr - off);
if (w <= 0) { close(fd); return -1; }
off += (size_t)w;
}
if (len && payload) {
size_t p = 0;
while (p < len) {
ssize_t w = write(fd, (const uint8_t *)payload + p, len - p);
if (w <= 0) { close(fd); return -1; }
p += (size_t)w;
}
}
off = 0;
while (off < sizeof hdr) {
ssize_t r = read(fd, hdr + off, sizeof hdr - off);
if (r <= 0) { close(fd); return -1; }
off += (size_t)r;
}
memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2);
memcpy(&rtype, hdr + 6, 2); memcpy(&rlen, hdr + 12, 4);
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) { close(fd); return -1; }
if (rlen > rcap) rlen = rcap;
off = 0;
while (off < rlen) {
ssize_t r = read(fd, rbuf + off, rlen - off);
if (r <= 0) { close(fd); return -1; }
off += (size_t)r;
}
close(fd);
return (int)rtype;
}
/* 向 launcher 申请 start/kill。返回 0=launcher 回 code(a_out 输出);-1=launcher 不通。
* launcher 重启窗口(svcd/sysui 重启 launcher、或 amsd 先行启动)内连接会暂不可达/超时:
* 这里做「指数式短退避重试」——launcher 数秒内回来即自动恢复转发(无需人工重发)。 */
static int ams_request_ctl(uint16_t type, const void *payload, uint32_t len, uint32_t *code)
{
uint8_t rbuf[64];
for (int attempt = 0; attempt < AMS_IPC_RETRY; attempt++) {
int rt = ipc_xchg(type, payload, len, rbuf, sizeof rbuf);
if (rt == MSG_ACK) {
m_ack_t a;
memcpy(&a, rbuf, sizeof a);
if (code) *code = a.code;
return 0;
}
if (rt == MSG_ERR) return MEOW_ERR_INTERNAL;
/* 一次连接失败/超时(launcher 未就绪)→ 短退避再试;仍失败则返回 -1。 */
if (attempt + 1 < AMS_IPC_RETRY)
usleep((useconds_t)(AMS_IPC_BACKOFF_MS * (attempt + 1)) * 1000);
}
return -1;
}
/* ---- 事件处理(launcher 上报) ---- */
static void handle_launch(char **t, int nt)
{
if (nt < 4) return;
const char *pkg = t[0], *act = t[1], *chain = "-";
pid_t pid;
long epoch, up;
if (nt == 5) {
/* 兼容旧格式(M2.90 固件 launcher 实证):EV LAUNCH <pkg> <act> <pid> <epoch> <up>
* ——无 chain 字段。不兼容会把 epoch 错当 pid(am status 显示假 pid、
* `am kill` 兜底直发 SIGTERM 打错进程),故按 nt==5 识别旧布局。 */
pid = (pid_t)atoi(t[2]);
epoch = atol(t[3]);
up = atol(t[4]);
} else {
chain = t[2];
pid = (pid_t)atoi(t[3]);
epoch = (nt > 4) ? atol(t[4]) : (long)time(NULL);
up = (nt > 5) ? atol(t[5]) : read_uptime_sec();
}
struct ams_app *a = app_get(pkg);
if (!a) return;
a->state = AMS_FG;
a->pid = pid;
snprintf(a->activity, sizeof a->activity, "%s", strcmp(act, "-") ? act : "");
snprintf(a->chain, sizeof a->chain, "%s", strcmp(chain, "-") ? chain : "");
a->launch_ms = (long long)now_ms();
a->launch_epoch = epoch;
a->launch_up = up;
a->launches++;
a->last_kind = 0; a->last_code = 0; a->last_dur_ms = 0;
char detail[192];
snprintf(detail, sizeof detail, "act=%s chain=%.64s pid=%ld", a->activity, a->chain, (long)pid);
hist_push(a, "launch", detail, epoch, up);
emit_log("LAUNCH %s %s %s pid=%ld", pkg, a->activity, a->chain, (long)pid);
out_open(pkg, pid, epoch);
}
static void handle_exit(char **t, int nt)
{
if (nt < 3) return;
const char *pkg = t[0];
const char *kind = t[1]; /* exit | sig */
int code = atoi(t[2]);
long long dur = (nt > 3) ? atoll(t[3]) : 0;
long epoch = (nt > 4) ? atol(t[4]) : (long)time(NULL);
long up = (nt > 5) ? atol(t[5]) : read_uptime_sec();
struct ams_app *a = app_find(pkg);
if (!a) { if (g_out.active && !strcmp(g_out.pkg, pkg)) out_close(); return; }
int abnormal = (strcmp(kind, "sig") == 0) || (strcmp(kind, "exit") == 0 && code != 0);
a->state = AMS_STOPPED;
a->pid = 0;
a->last_kind = (strcmp(kind, "sig") == 0) ? 2 : (code == 0 ? 0 : 1);
a->last_code = code;
a->last_dur_ms = dur;
if (abnormal) {
a->crashes++;
crash_forward(pkg, kind, code, epoch, up); /* 双保险:并入 meow_crash 日志 */
}
char detail[192];
if (abnormal)
snprintf(detail, sizeof detail, "%s=%d dur=%lldms crash=%d", kind, code, dur, a->crashes);
else
snprintf(detail, sizeof detail, "%s=%d dur=%lldms", kind, code, dur);
hist_push(a, abnormal ? "crash" : "exit", detail, epoch, up);
emit_log("EXIT %s %s=%d dur=%lld abnormal=%d", pkg, kind, code, dur, abnormal);
if (g_out.active && !strcmp(g_out.pkg, pkg)) out_close();
}
static void handle_out(char **t, int nt)
{
if (nt < 3) return;
const char *pkg = t[0];
pid_t pid = (pid_t)atoi(t[1]);
long epoch = atol(t[2]);
long up = atol(t[3]);
/* 拼接剩余 tokens(text 可含空格) */
char text[AMS_LINE_MAX];
text[0] = '\0';
int off = 0;
for (int i = 4; i < nt; i++) {
int w = snprintf(text + off, sizeof text - off, "%s%s", off ? " " : "", t[i]);
if (w < 0) break;
off += w;
if ((size_t)off >= sizeof text - 1) { off = (int)sizeof text - 1; break; }
}
text[off] = '\0';
out_append(pkg, pid, epoch, up, text);
}
/* ---- §6.2 二期:picker 编排(amsd 路由权威) ---- */
static struct ams_picker *picker_find(const char *action)
{
if (!action || !action[0]) return NULL;
for (int i = 0; i < g_npickers; i++)
if (g_pickers[i].used && !strcmp(g_pickers[i].action, action)) return &g_pickers[i];
return NULL;
}
static void pending_record(uint32_t req_id, const char *action, int code, const char *value)
{
for (int i = 0; i < AMS_MAX_PENDING; i++) {
struct ams_pending *p = &g_pending[i];
if (!p->active) continue;
if (p->req_id != req_id) continue;
if (action[0] && strcmp(p->action, action) != 0) continue;
p->done = 1;
p->code = code;
scopy(p->value, sizeof p->value, value);
emit_log("picker result: req=%u action=%s code=%d value=%s", req_id, p->action, code, p->value);
struct ams_app *h = app_find(p->app_id);
if (h) {
char detail[192];
snprintf(detail, sizeof detail, "req=%u caller=%s act=%s code=%d val=%.96s",
req_id, p->caller, action, code, value);
hist_push(h, "picker_result", detail, (long)time(NULL), read_uptime_sec());
}
return;
}
emit_log("picker result: no pending req=%u (dropped)", req_id);
}
/* picker 启动:解析 action → handler;alloc/更新 pending;经 launcher MSG_AMS_START(picker) 拉起。
* write_resp:写响应到 resp(cmd_picker 用;PICKER_CALL 事件传 NULL 不写)。返回 0=ok启动。 */
static int picker_start(const char *caller, uint32_t req_id, const char *action,
char *resp, int rn)
{
struct ams_picker *pk = picker_find(action);
if (!pk) {
if (resp) snprintf(resp, rn, "ERR no picker handler for action: %s\n", action ? action : "");
emit_log("AMS picker %s -> no handler", action ? action : "");
return -1;
}
struct ams_app *h = app_find(pk->app_id);
if (!h) {
if (resp) snprintf(resp, rn, "ERR handler app not registered: %s\n", pk->app_id);
emit_log("AMS picker %s -> handler %s not registered", action, pk->app_id);
return -1;
}
/* 找/建 pending 槽(同 req_id 去重;满则覆盖最旧 done) */
struct ams_pending *p = NULL;
for (int i = 0; i < AMS_MAX_PENDING; i++)
if (g_pending[i].active && g_pending[i].req_id == req_id) { p = &g_pending[i]; break; }
if (!p) {
for (int i = 0; i < AMS_MAX_PENDING; i++)
if (!g_pending[i].active || g_pending[i].done) { p = &g_pending[i]; break; }
}
if (!p) p = &g_pending[0]; /* 全占满→覆盖 0(最旧) */
memset(p, 0, sizeof *p);
p->active = 1;
p->req_id = req_id;
scopy(p->caller, sizeof p->caller, caller ? caller : "");
scopy(p->action, sizeof p->action, action ? action : "");
scopy(p->app_id, sizeof p->app_id, pk->app_id);
/* 裸启动 handler(activity 空;picker 上下文经 MSG_AMS_START 扩载荷注入 env) */
m_ams_start_t s;
memset(&s, 0, sizeof s);
scopy(s.app_id, sizeof s.app_id, pk->app_id);
if (pk->chain[0]) scopy(s.activity, sizeof s.activity, pk->chain);
s.picker = 1;
s.picker_req_id = req_id;
scopy(s.picker_action, sizeof s.picker_action, pk->action);
scopy(s.picker_caller, sizeof s.picker_caller, caller ? caller : "");
uint32_t code = MEOW_ERR_INTERNAL;
int rt = ams_request_ctl(MSG_AMS_START, &s, sizeof s, &code);
if (rt != 0) {
p->active = 0;
if (resp) snprintf(resp, rn, "ERR launcher offline\n");
emit_log("AMS picker %s -> launcher offline", action);
return -1;
}
if (code != MEOW_OK) {
if (resp) snprintf(resp, rn, "ERR launcher refused: %s (code=%u)\n", pk->app_id, code);
emit_log("AMS picker %s -> launcher refused code=%u", action, code);
return -1;
}
if (resp) snprintf(resp, rn, "OK pending %u %s\n", req_id, pk->app_id);
emit_log("AMS picker %s -> %s (req=%u, caller=%s)", action, pk->app_id, req_id, caller ? caller : "");
return 0;
}
/* `am picker <action>`:alloc req_id(CLI 无 caller),启动,回 OK pending。 */
static int cmd_picker(const char *action, char *resp, int rn)
{
if (!action || !action[0]) { snprintf(resp, rn, "ERR picker needs <action>\n"); return 1; }
uint32_t req_id = g_pick_req_seq++;
if (g_pick_req_seq == 0) g_pick_req_seq = 1;
picker_start("", req_id, action, resp, rn); /* 内部写 OK pending / ERR */
return 1;
}
/* `picker-status [reqid]`:列正在/已决 picker。 */
static int cmd_picker_status(const char *reqstr, char *resp, int rn)
{
int l = snprintf(resp, rn, "# amd picker status\n");
int found = 0;
for (int i = 0; i < AMS_MAX_PENDING; i++) {
struct ams_pending *p = &g_pending[i];
if (!p->active) continue;
if (reqstr && reqstr[0] && (uint32_t)atoi(reqstr) != p->req_id) continue;
found = 1;
const char *state = p->done ? "done" : "pending";
int m = snprintf(resp + l, rn - l, "PICK %u %s %s %d %s\n",
p->req_id, p->action, p->app_id, p->done ? p->code : -1,
p->done ? p->value : state);
if (m > 0) l += m;
}
if (!found) { int m = snprintf(resp + l, rn - l, "(none)\n"); if (m > 0) l += m; }
return l;
}
/* `am rescan`:手动触发注册表重扫(卡刚挂载/热装 App 后立即可见,不等 30s 周期)。 */
static int cmd_rescan(char *resp, int rn)
{
registry_scan(1);
return snprintf(resp, rn, "OK rescan %d app(s)\n", g_napps);
}
/* `am resolve <pkg> <act>`:查 amsd 权威路由表。 */
static int cmd_resolve(const char *pkg, const char *act, char *resp, int rn)
{
if (!pkg || !pkg[0] || !act || !act[0]) {
snprintf(resp, rn, "ERR resolve needs <pkg> <activity>\n");
return 1;
}
char chain[MEOW_CHAIN_MAX];
int depth = 0;
int rc = amsd_resolve_chain(pkg, act, chain, &depth);
if (rc != 0) {
snprintf(resp, rn, "ERR resolve %s/%s failed (%d)\n", pkg, act, rc);
return 1;
}
snprintf(resp, rn, "CHAIN %s %s %d %s\n", pkg, act, depth, chain);
return 1;
}
/* ---- am CLI 命令处理 ---- */
static int cmd_status(char *resp, int rn)
{
int l = snprintf(resp, rn, "# amd status (running apps)\n");
int found = 0;
for (int i = 0; i < g_napps; i++) {
struct ams_app *a = &g_apps[i];
if (a->state == AMS_STOPPED || a->pid <= 0) continue;
found = 1;
unsigned long long dur = a->launch_ms ? (now_ms() - (unsigned long long)a->launch_ms) : 0;
const char *st = (a->state == AMS_FG) ? "fg" : (a->state == AMS_BG) ? "bg" : "stop";
int m = snprintf(resp + l, rn - l, "RUN %s %s %ld %llu %s\n",
a->pkg, st, (long)a->pid, (unsigned long long)dur,
a->activity);
if (m > 0) l += m;
}
if (!found) { int m = snprintf(resp + l, rn - l, "(none running)\n"); if (m > 0) l += m; }
return l;
}
static int cmd_list(char *resp, int rn)
{
int l = snprintf(resp, rn, "# amd register+state\n");
for (int i = 0; i < g_napps; i++) {
struct ams_app *a = &g_apps[i];
const char *st = (a->state == AMS_FG) ? "fg" : (a->state == AMS_BG) ? "bg" : "stop";
const char *lk = (a->last_kind == 2) ? "sig" : (a->last_kind == 1) ? "exit" : "clean";
int m = snprintf(resp + l, rn - l, "APP %s %s %s %ld %d %d %s %d\n",
a->pkg, a->name, st, (long)a->pid, a->launches, a->crashes, lk, a->last_code);
if (m > 0) l += m;
}
return l;
}
static int cmd_history(const char *pkg, char *resp, int rn)
{
int l = snprintf(resp, rn, "# amd history %s\n", pkg && pkg[0] ? pkg : "(all)");
if (pkg && pkg[0]) {
struct ams_app *a = app_find(pkg);
if (!a) { int m = snprintf(resp + l, rn - l, "(no such app %s)\n", pkg); if (m > 0) l += m; return l; }
for (int k = 0; k < a->hist_n; k++) {
int idx = (a->hist_idx - a->hist_n + k + AMS_HIST) % AMS_HIST;
struct ams_ev *e = &a->hist[idx];
int m = snprintf(resp + l, rn - l, "EVT %ld %ld %s %s %s\n",
e->epoch, e->up, e->kind, a->pkg, e->detail);
if (m > 0) l += m;
}
} else {
for (int i = 0; i < g_napps; i++) {
struct ams_app *a = &g_apps[i];
for (int k = 0; k < a->hist_n; k++) {
int idx = (a->hist_idx - a->hist_n + k + AMS_HIST) % AMS_HIST;
struct ams_ev *e = &a->hist[idx];
int m = snprintf(resp + l, rn - l, "EVT %ld %ld %s %s %s\n",
e->epoch, e->up, e->kind, a->pkg, e->detail);
if (m > 0) l += m;
}
}
}
return l;
}
static int cmd_start(const char *spec, char *resp, int rn)
{
/* spec: <pkg> or <pkg>/<activity> */
char pkg[MEOW_APP_ID_MAX], act[MEOW_ACT_MAX];
snprintf(pkg, sizeof pkg, "%s", spec);
act[0] = '\0';
char *slash = strchr(pkg, '/');
if (slash) { *slash = 0; snprintf(act, sizeof act, "%s", slash + 1); }
struct ams_app *a = app_find(pkg);
if (!a) {
int m = snprintf(resp, rn, "ERR pkg not registered: %s\n", pkg);
return m > 0 ? m : 0;
}
m_ams_start_t s;
memset(&s, 0, sizeof s);
snprintf(s.app_id, sizeof s.app_id, "%s", pkg);
if (act[0]) snprintf(s.activity, sizeof s.activity, "%s", act);
uint32_t code = MEOW_ERR_INTERNAL;
int rt = ams_request_ctl(MSG_AMS_START, &s, sizeof s, &code);
char detail[192];
if (rt == 0 && code == MEOW_OK) {
hist_push(a, "ams_start", "intent (forwarded)", (long)time(NULL), read_uptime_sec());
snprintf(detail, sizeof detail, "ok forwarded");
emit_log("AMS start %s%s forwarded to launcher", pkg, act[0] ? "/" : "");
} else if (rt == 0 && code == (uint32_t)(-MEOW_ERR_BUSY)) {
/* launcher ACK ack.code 携带正幅度(busy=8,见 util.c `rc<0?-rc:MEOW_OK`),非二补。 */
snprintf(detail, sizeof detail, "busy (app running)");
emit_log("AMS start %s -> BUSY (app running)", pkg);
} else {
snprintf(detail, sizeof detail, "launcher offline");
emit_log("AMS start %s -> launcher offline", pkg);
}
snprintf(resp, rn, "%s %s/%s\n", strstr(detail, "ok forwarded") ? "OK" : "ERR",
pkg, act[0] ? act : "(default)");
return (int)strlen(resp);
}
static int cmd_kill(const char *pkg, char *resp, int rn)
{
struct ams_app *a = app_find(pkg);
if (!a) { int m = snprintf(resp, rn, "ERR pkg not registered: %s\n", pkg); return m > 0 ? m : 0; }
if (a->state == AMS_STOPPED || a->pid <= 0) {
int m = snprintf(resp, rn, "ERR not running: %s\n", pkg);
return m > 0 ? m : 0;
}
m_ams_kill_t k;
memset(&k, 0, sizeof k);
snprintf(k.app_id, sizeof k.app_id, "%s", pkg);
uint32_t code = 0;
int rt = ams_request_ctl(MSG_AMS_KILL, &k, sizeof k, &code);
if (rt != 0 && a->pid > 0) {
/* 兜底:launcher 不在,直接 SIGTERM(与 launcher 的 SIGTERM 语义一致) */
kill(a->pid, SIGTERM);
emit_log("AMS kill %s -> direct SIGTERM (launcher offline)", pkg);
} else if (rt != 0) {
return snprintf(resp, rn, "ERR launcher offline: %s\n", pkg);
} else if (code != MEOW_OK) {
emit_log("AMS kill %s -> launcher refused code=%u", pkg, code);
return snprintf(resp, rn, "ERR launcher refused: %s (code=%u)\n", pkg, code);
}
hist_push(a, "ams_kill", "terminate (via launcher)", (long)time(NULL), read_uptime_sec());
int m = snprintf(resp, rn, "OK terminate %s\n", pkg);
emit_log("AMS kill %s -> terminate", pkg);
return m > 0 ? m : 0;
}
/* 把一「行」分发。命令 → 生成响应返回 1;事件 → 返回 0。 */
static int dispatch_line(char *line, char *resp, int rn)
{
int l = (int)strlen(line);
while (l > 0 && (line[l - 1] == '\n' || line[l - 1] == '\r')) line[--l] = '\0';
if (l == 0) return 0;
/* 事件:EV <TYPE> <tokens...> */
if (strncmp(line, "EV ", 3) == 0) {
char *tok[16];
int nt = 0;
char *save = NULL, *cur = line + 3;
for (int i = 0; i < (int)(sizeof tok / sizeof tok[0]); i++) {
char *tk = strtok_r(i == 0 ? cur : NULL, " ", &save);
if (!tk) break;
tok[nt++] = tk;
}
if (nt < 1) return 0;
if (!strcmp(tok[0], "LAUNCH")) { handle_launch(tok + 1, nt - 1); }
else if (!strcmp(tok[0], "EXIT")) { handle_exit(tok + 1, nt - 1); }
else if (!strcmp(tok[0], "OUT")) { handle_out(tok + 1, nt - 1); }
return 0;
}
/* §6.2 二期事件(launcher → amsd 一次性转发,无响应):DECLARE / PICKER_CALL / PICKER_RESULT */
if (strncmp(line, "DECLARE ", 8) == 0 || strncmp(line, "PICKER_", 7) == 0) {
char *tok[8];
int nt = 0;
char *save = NULL;
for (int i = 0; i < (int)(sizeof tok / sizeof tok[0]); i++) {
char *tk = strtok_r(i == 0 ? line : NULL, " ", &save);
if (!tk) break;
tok[nt++] = tk;
}
if (nt >= 1) {
if (!strcmp(tok[0], "DECLARE") && nt >= 4) {
struct ams_app *a = app_get(tok[1]);
char entries[MEOW_IPC_CARGO];
if (a) {
int bl = ams_hex_decode(tok[3], entries, sizeof entries);
(void)bl;
amsd_apply_routes(a, entries);
emit_log("DECLARE %s n=%s routes=%d", tok[1], tok[2], a->nroute);
}
} else if (!strcmp(tok[0], "PICKER_CALL") && nt >= 4) {
uint32_t req = (uint32_t)atoi(tok[2]);
(void)picker_start(tok[1], req, tok[3], NULL, 0);
} else if (!strcmp(tok[0], "PICKER_RESULT") && nt >= 5) {
uint32_t req = (uint32_t)atoi(tok[1]);
int code = atoi(tok[3]);
char value[256];
(void)ams_hex_decode(tok[4], value, sizeof value);
pending_record(req, tok[2], code, value);
}
}
return 0;
}
/* 命令:status/list/history/start/kill/resolve/picker/picker-status/rescan */
char *save = NULL;
char *cmd = strtok_r(line, " ", &save);
if (!cmd) return 0;
char *arg = strtok_r(NULL, " ", &save);
if (!strcmp(cmd, "status")) return cmd_status(resp, rn) > 0;
if (!strcmp(cmd, "list")) return cmd_list(resp, rn) > 0;
if (!strcmp(cmd, "rescan")) return cmd_rescan(resp, rn) > 0;
if (!strcmp(cmd, "history")) {
if (arg && arg[0]) cmd_history(arg, resp, rn);
else cmd_history(NULL, resp, rn);
return 1;
}
if (!strcmp(cmd, "start")) {
if (!arg) { snprintf(resp, rn, "ERR start needs <pkg>[/<act>]\n"); return 1; }
cmd_start(arg, resp, rn);
return 1;
}
if (!strcmp(cmd, "kill")) {
if (!arg) { snprintf(resp, rn, "ERR kill needs <pkg>\n"); return 1; }
cmd_kill(arg, resp, rn);
return 1;
}
if (!strcmp(cmd, "resolve")) {
if (!arg) { snprintf(resp, rn, "ERR resolve needs <pkg> <activity>\n"); return 1; }
char *act = strtok_r(NULL, " ", &save);
return cmd_resolve(arg, act, resp, rn) > 0;
}
if (!strcmp(cmd, "picker")) {
if (!arg) { snprintf(resp, rn, "ERR picker needs <action>\n"); return 1; }
cmd_picker(arg, resp, rn);
return 1;
}
if (!strcmp(cmd, "picker-status")) {
return cmd_picker_status(arg, resp, rn) > 0;
}
snprintf(resp, rn, "ERR unknown cmd: %s\n", cmd);
return 1;
}
/* ---- 连接处理(读可用字节,处理完整行,命令回响应) ---- */
/* 读入可用字节并处理所有「完整行」(以 \n 结尾);未完结的尾巴保留在 buf(跨 poll 累积)。
* 命令行 → 响应累积到 c->resp(返回后由调用方写回)。
* 返回 1=读到 EOF(对端已关闭写端,应随后收尾关闭);0=还有后续。 */
static int conn_read(struct ams_conn *c)
{
int eof = 0;
while (c->n < (int)sizeof c->buf - 1) {
ssize_t r = read(c->fd, c->buf + c->n, sizeof c->buf - 1 - c->n);
if (r == 0) { eof = 1; break; }
if (r < 0) break; /* EAGAIN / EINTR 等:还有后续 */
c->n += (int)r;
}
c->buf[c->n] = '\0';
/* 从 buf 头部逐条切「完整行」,完成 >=1 行后 memmove 保留未完结尾巴。 */
size_t consumed = 0;
size_t buflen = (size_t)c->n;
while (consumed < buflen) {
char *nl = memchr(c->buf + consumed, '\n', buflen - consumed);
if (!nl) break; /* 剩余为未完结尾巴,保留 */
*nl = '\0';
size_t linelen = (size_t)(nl - (c->buf + consumed));
if (linelen > 0) {
int got = dispatch_line(c->buf + consumed, c->resp + c->resp_n,
(int)sizeof c->resp - c->resp_n);
if (got) c->resp_n += (int)strlen(c->resp + c->resp_n);
}
consumed += linelen + 1;
}
/* 保留未完结尾巴(无 \n)到 buf 头部;丢弃已消费部分。 */
if (consumed > 0 && consumed < buflen) {
memmove(c->buf, c->buf + consumed, buflen - consumed);
c->n = (int)(buflen - consumed);
c->buf[c->n] = '\0';
} else if (consumed >= buflen) {
c->n = 0; c->buf[0] = '\0';
}
return eof;
}
/* 连接关闭(EOF)后:处理可能残留的最后一行(无 \n),再写回累积响应。 */
static void conn_finish(struct ams_conn *c)
{
if (c->n > 0) {
c->buf[c->n] = '\0';
int got = dispatch_line(c->buf, c->resp + c->resp_n, (int)sizeof c->resp - c->resp_n);
if (got) c->resp_n += (int)strlen(c->resp + c->resp_n);
c->n = 0;
}
if (c->resp_n > 0) {
ssize_t w = write(c->fd, c->resp, (size_t)c->resp_n);
(void)w;
c->resp_n = 0;
}
}
/* ---- 信号 / socket ---- */
static void sig_handler(int sig)
{
g_shutdown = 1;
char ch = 'T';
ssize_t w = write(g_sigpipe[1], &ch, 1);
(void)w;
(void)sig;
}
static int setup_signals(void)
{
if (pipe(g_sigpipe) != 0) return -1;
int rf = fcntl(g_sigpipe[0], F_GETFL, 0);
if (rf >= 0) fcntl(g_sigpipe[0], F_SETFL, rf | O_NONBLOCK);
int wf = fcntl(g_sigpipe[1], F_GETFL, 0);
if (wf >= 0) fcntl(g_sigpipe[1], F_SETFL, wf | O_NONBLOCK);
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = sig_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
signal(SIGPIPE, SIG_IGN);
return 0;
}
static int setup_socket(void)
{
unlink(g_sock);
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) { fprintf(stderr, "[meow_amsd] socket: %s\n", strerror(errno)); return -1; }
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) {
fprintf(stderr, "[meow_amsd] bind %s: %s\n", g_sock, strerror(errno));
close(fd);
return -1;
}
(void)chmod(g_sock, 0666);
if (listen(fd, 8) != 0) { fprintf(stderr, "[meow_amsd] listen: %s\n", strerror(errno)); close(fd); return -1; }
g_listen_fd = fd;
return 0;
}
static int run_daemon(void)
{
if (setup_signals() != 0) return 1;
if (setup_socket() != 0) return 1;
boot_ts(g_bootts, sizeof g_bootts);
log_open_card();
log_cleanup_old();
registry_scan(1); /* 启动首扫:卡未挂载扫空也照常常驻,主循环 3s 快重试兜底 */
fprintf(stderr, "[meow_amsd] up: sock=%s ipc=%s apps=%s card=%s bootts=%s log=%s\n",
g_sock, g_ipc_sock, g_apps_dir, g_card, g_bootts,
g_on_card ? "card" : AMS_TMP_LOG);
struct pollfd pfds[2 + AMS_MAX_CONNS];
struct ams_conn conns[AMS_MAX_CONNS];
memset(conns, 0, sizeof conns);
for (int i = 0; i < AMS_MAX_CONNS; i++) conns[i].fd = -1;
int nfds = 2;
unsigned long long last_cleanup = 0, last_rescan = 0;
while (!g_shutdown) {
/* 每圈清零:谨防上一轮已关闭连接残留的 POLLHUP/POLLERR 落在 pfds[2+i] 上。
* 若不清,accept 后同一轮就用「陈旧 revents」处理新连接——误判其 HUP/ERR
* 而提前 close(am CLI 收到 SIGPIPE、命令被丢弃),launcher 重启后 EV 事件
* 连接密集必触发,表现为 `am start` 不再转发到 launcher。 */
memset(pfds, 0, sizeof pfds);
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;
for (int i = 0; i < nfds - 2; i++) {
pfds[2 + i].fd = conns[i].fd;
pfds[2 + i].events = POLLIN;
pfds[2 + i].revents = 0;
}
int r = poll(pfds, (nfds_t)nfds, AMS_TICK_MS);
if (r < 0) {
if (errno == EINTR) continue;
fprintf(stderr, "[meow_amsd] poll: %s\n", strerror(errno));
break;
}
if (g_shutdown) break;
if (pfds[0].revents & POLLIN) {
int cfd = accept(g_listen_fd, NULL, NULL);
if (cfd >= 0) {
int fl = fcntl(cfd, F_GETFL, 0);
if (fl >= 0) fcntl(cfd, F_SETFL, fl | O_NONBLOCK);
if (nfds - 2 < AMS_MAX_CONNS) {
conns[nfds - 2].fd = cfd;
conns[nfds - 2].n = 0;
conns[nfds - 2].resp_n = 0;
nfds++;
} else close(cfd);
}
}
if (pfds[1].revents & POLLIN) {
char tmp[64];
while (read(g_sigpipe[0], tmp, sizeof tmp) > 0) {}
}
for (int i = 0; i < nfds - 2; i++) {
struct ams_conn *c = &conns[i];
int ev = pfds[2 + i].revents;
int done = 0;
if (ev & (POLLIN | POLLHUP)) {
if (conn_read(c)) { conn_finish(c); done = 1; } /* EOF:处理残行+回响应后关 */
}
if (!done && (ev & (POLLHUP | POLLERR | POLLNVAL))) { conn_finish(c); done = 1; }
/* 命令行 → 响应已就绪:写回并关闭(am 单命令即回)。 */
if (c->resp_n > 0) {
ssize_t w = write(c->fd, c->resp, (size_t)c->resp_n);
(void)w;
c->resp_n = 0;
done = 1;
}
if (done) {
close(c->fd);
c->fd = -1;
for (int j = i; j < nfds - 3; j++) conns[j] = conns[j + 1];
nfds--;
i--;
}
}
if (now_ms() - last_cleanup >= (unsigned long long)AMS_CLEANUP_MS) {
last_cleanup = now_ms();
poll_card();
log_cleanup_old();
}
/* 注册表自愈重扫:扫描源从未成功打开(开机早于卡挂载扫空,M2.90 真机实证
* 「registry scan: MeowAPPs not reachable」后注册表永久为空 → am start 报
* pkg not registered)→ 3s 快重试直到扫到;常态 30s 合并重扫(卡热插拔 /
* App 热安装自愈,对齐 launcher 的应用列表目录热重载)。合并式重扫不清
* 运行时状态,见 registry_scan 注释。 */
{
unsigned long long nm = now_ms();
unsigned long long iv = g_scan_ok ? (unsigned long long)AMS_RESCAN_MS
: (unsigned long long)AMS_RESCAN_RETRY_MS;
if (nm - last_rescan >= iv) { last_rescan = nm; registry_scan(0); }
}
}
if (g_listen_fd >= 0) close(g_listen_fd);
unlink(g_sock);
out_close();
if (g_log_fd >= 0) close(g_log_fd);
fprintf(stderr, "[meow_amsd] shutdown\n");
return 0;
}
int main(int argc, char **argv)
{
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (!strcmp(a, "--sock") && i + 1 < argc) safe_strcpy(g_sock, sizeof g_sock, argv[++i]);
else if (!strcmp(a, "--ipc-sock") && i + 1 < argc) safe_strcpy(g_ipc_sock, sizeof g_ipc_sock, argv[++i]);
else if (!strcmp(a, "--apps") && i + 1 < argc) safe_strcpy(g_apps_dir, sizeof g_apps_dir, argv[++i]);
else if (!strcmp(a, "--card") && i + 1 < argc) safe_strcpy(g_card, sizeof g_card, argv[++i]);
else if (!strcmp(a, "--crash-sock") && i + 1 < argc) safe_strcpy(g_crash_sock, sizeof g_crash_sock, argv[++i]);
else if (!strcmp(a, "-h") || !strcmp(a, "--help")) {
fprintf(stderr, "usage: %s [--sock <p>] [--ipc-sock <p>] [--apps <dir>] "
"[--card <p>] [--crash-sock <p>]\n", argv[0]);
return 0;
} else if (a[0] == '-') { fprintf(stderr, "bad option %s\n", a); return 2; }
}
const char *e;
if ((e = getenv("MEOW_AMS_SOCK")) && e[0]) safe_strcpy(g_sock, sizeof g_sock, e);
if ((e = getenv("MEOW_IPC_SOCK")) && e[0]) safe_strcpy(g_ipc_sock, sizeof g_ipc_sock, e);
if ((e = getenv("MEOW_AMSD_APPS")) && e[0]) safe_strcpy(g_apps_dir, sizeof g_apps_dir, e);
if ((e = getenv("MEOW_AMSD_CARD")) && e[0]) safe_strcpy(g_card, sizeof g_card, e);
if ((e = getenv("MEOW_CRASH_SOCK")) && e[0]) safe_strcpy(g_crash_sock, sizeof g_crash_sock, e);
setvbuf(stdout, NULL, _IONBF, 0);
return run_daemon(); /* svcd 空参拉起 → 前台常驻 */
}