/* mp_ams.c — App 运行表域:am list 解析 + amd 历史 + start/kill 控制。 */ #include "mp.h" const char *ams_bin(void) { const char *e = getenv("MEOW_AMS_BIN"); if (e && e[0]) return e; if (!access(AMS_BIN, X_OK)) return AMS_BIN; return AMS_BIN_CARD; } void ams_fill(sbuf_t *s) { char out[16384]; char cmd[300]; snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin()); int code = run_cmd(cmd, out, sizeof out); if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; } panel_strip_ansi(out); char *toks[256]; sbuf_t apps = {0}; int first = 1, count = 0; char *ln = out; while (*ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; if (strncmp(ln, "APP ", 4) == 0) { int n = txt_tokens(ln, toks, 256); if (n >= 9) { /* 布局:APP * tok[0]="APP" 前缀;自右向左固定:state=tok[n-6], pid=n-5, launches=n-4, crashes=n-3, * last_kind=n-2, last_code=n-1;pkg=tok[1],name=join(tok[2..n-6))。 */ char nm[128]; join_tokens(toks, 2, n - 6, nm, sizeof nm); if (!first) sA(&apps, ","); first = 0; count++; sA(&apps, "{\"pkg\":"); sA_str(&apps, toks[1]); sA(&apps, ",\"name\":"); sA_str(&apps, nm); sA(&apps, ",\"state\":"); sA_str(&apps, toks[n-6]); sA(&apps, ",\"pid\":%d", atoi(toks[n-5])); sA(&apps, ",\"launches\":%d", atoi(toks[n-4])); sA(&apps, ",\"crashes\":%d", atoi(toks[n-3])); sA(&apps, ",\"last_kind\":"); sA_str(&apps, toks[n-2]); sA(&apps, ",\"last_code\":%d}", atoi(toks[n-1])); } } if (nl) { *nl = '\n'; ln = nl + 1; } else break; } sA(s, "{\"available\":true,\"count\":%d,\"apps\":[%s]", count, apps.b ? apps.b : ""); sbuf_free(&apps); ams_fg_fill(s); /* 追加 `,"fg":{..},"running":[..]`(首页前台 App 高亮;additive) */ sA(s, "}"); } void ams_fg_fill(sbuf_t *s) { char out[16384]; char cmd[300]; snprintf(cmd, sizeof cmd, "%s status 2>/dev/null", ams_bin()); int code = run_cmd(cmd, out, sizeof out); if (code != 0 || !strstr(out, "RUN")) { sA(s, ",\"fg\":null,\"running\":[]"); return; } panel_strip_ansi(out); char *toks[256]; char fg_pkg[128] = "", fg_act[160] = ""; int fg_pid = 0; sbuf_t run = {0}; int first = 1; char *ln = out; while (*ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; if (strncmp(ln, "RUN ", 4) == 0) { int n = txt_tokens(ln, toks, 256); if (n >= 6) { /* RUN pkg state pid dur activity... */ char act[160]; join_tokens(toks, 5, n, act, sizeof act); const char *st = toks[2]; if (!first) sA(&run, ","); first = 0; sA(&run, "{\"pkg\":"); sA_str(&run, toks[1]); sA(&run, ",\"state\":"); sA_str(&run, st); sA(&run, ",\"pid\":%d", atoi(toks[3])); sA(&run, ",\"activity\":"); sA_str(&run, act); sA(&run, "}"); if (strcmp(st, "fg") == 0 && !fg_pkg[0]) { snprintf(fg_pkg, sizeof fg_pkg, "%s", toks[1]); snprintf(fg_act, sizeof fg_act, "%s", act); fg_pid = atoi(toks[3]); } } } if (nl) { *nl = '\n'; ln = nl + 1; } else break; } sA(s, ",\"fg\":"); if (fg_pkg[0]) { sA(s, "{\"pkg\":"); sA_str(s, fg_pkg); sA(s, ",\"activity\":"); sA_str(s, fg_act); sA(s, ",\"pid\":%d}", fg_pid); } else sA(s, "null"); sA(s, ",\"running\":[%s]", run.b ? run.b : ""); sbuf_free(&run); } int api_ams(int sock) { sbuf_t s = {0}; ams_fill(&s); int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}"); sbuf_free(&s); return rc; } int ams_pkg_ok(const char *pkg) { if (!pkg || !pkg[0]) return 0; if (strlen(pkg) >= MEOW_APP_ID_MAX) return 0; for (const char *p = pkg; *p; p++) { unsigned char c = (unsigned char)*p; if (c == '/' || c == ':' || c == ' ' || c == '\t' || c == '\n' || c == '\r') return 0; if (c < 0x21 || c > 0x7e) return 0; /* 包名仅 ASCII 可见字符 */ } return 1; } void ams_history_parse(char *out, sbuf_t *s, const char *pkg) /* out 被 panel_strip_ansi 就地改 */ { if (!utf8_valid_n(out, strlen(out))) { sA(s, "{\"available\":false}"); return; } /* 崩溃 detail 坏字节 → 降级 */ panel_strip_ansi(out); char *toks[256]; sbuf_t ev = {0}; int first = 1, count = 0; char *ln = out; while (*ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; if (strncmp(ln, "EVT ", 4) == 0) { int n = txt_tokens(ln, toks, 256); if (n >= 5) { /* EVT epoch up kind pkg detail... */ char det[256]; join_tokens(toks, 5, n, det, sizeof det); if (!first) sA(&ev, ","); first = 0; count++; sA(&ev, "{\"ts\":%ld,\"uptime_sec\":%ld,\"kind\":", atol(toks[1]), atol(toks[2])); sA_str(&ev, toks[3]); sA(&ev, ",\"detail\":"); sA_str(&ev, det); sA(&ev, "}"); } } if (nl) { *nl = '\n'; ln = nl + 1; } else break; } sA(s, "{\"available\":true,\"pkg\":"); sA_str(s, pkg); sA(s, ",\"count\":%d,\"events\":[%s]}", count, ev.b ? ev.b : ""); sbuf_free(&ev); } void ams_history_fill(sbuf_t *s, const char *pkg) { char out[16384]; char cmd[320]; snprintf(cmd, sizeof cmd, "%s history %s 2>/dev/null", ams_bin(), pkg); int code = run_cmd(cmd, out, sizeof out); if (code != 0 || !strstr(out, "EVT")) { sA(s, "{\"available\":false}"); return; } ams_history_parse(out, s, pkg); } int api_ams_history(int sock, const char *pkg) { if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); sbuf_t s = {0}; ams_history_fill(&s, pkg); int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}"); sbuf_free(&s); return rc; } int api_ams_ctl(int sock, const char *pkg, int kill) { if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); char out[256]; char cmd[320]; snprintf(cmd, sizeof cmd, "%s %s %s 2>/dev/null", ams_bin(), kill ? "kill" : "start", pkg); int code = run_cmd(cmd, out, sizeof out); if (code == 0 && strstr(out, "OK ")) { sbuf_t s = {0}; sA(&s, "{\"accepted\":true,\"op\":\"%s\",\"pkg\":", kill ? "kill" : "start"); sA_str(&s, pkg); sA(&s, ",\"message\":"); sA_str(&s, out); sA(&s, "}"); int rc2 = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc2; } /* am CLI 以文本报告失败(ERR ...);按内容映射 HTTP 码(如实,绝不掩盖)。 */ const char *detail = out[0] ? out : (kill ? "am kill 失败" : "am start 失败"); if (strstr(detail, "not registered")) return send_err(sock, 404, "NOT_FOUND", detail); if (strstr(detail, "busy")) return send_err(sock, 409, "CONFLICT", detail); if (strstr(detail, "not running")) return send_err(sock, 409, "CONFLICT", detail); if (strstr(detail, "launcher offline")) return send_err(sock, 503, "AMS_UNAVAILABLE", "amsd/launcher 不可达"); return send_err(sock, 500, "INTERNAL", detail); } /* ---- 批④ 后台活动列表:/api/ams/bg(只读) ---- * 数据源=amsd FG/BG 注册表(am list 的 `state=bg` 行)+ 每进程 pcpu/rss(读 /proc/, * 与 launcher bg_page 的 bgm_proc_sample 同源同口径,单线程静态差分缓存取 CPU 占用)。 * CPU/内存诚实原则:pid 读不到 / 首采样无差分基线 → 对应字段为 null(前端显示「—」),绝不编造。 */ #define BG_CACHE_MAX 8 static long g_bg_pid[BG_CACHE_MAX]; /* /proc//stat utime+stime 差分缓存(单线程串行持有) */ static unsigned long long g_bg_tick[BG_CACHE_MAX]; /* 采样单进程 CPU/内存:*pcpu<0=未知(无基线/读不到)、*rss_kb<0=未知。 */ static void bg_proc_sample(int pid, double *pcpu, long *rss_kb) { *pcpu = -1.0; *rss_kb = -1; if (pid <= 0) return; char path[64], line[1024]; /* 内存:VmRSS(KB) */ snprintf(path, sizeof path, "/proc/%d/status", pid); if (read_small_file(path, line, sizeof line) >= 0) { char *p = strstr(line, "VmRSS:"); if (p) { p = strchr(p, ' '); if (p) { while (*p == ' ' || *p == '\t') p++; if (*p) *rss_kb = atol(p); } } } /* CPU:/proc//stat 的 utime(14)+stime(15) 差分 / hz(%) */ snprintf(path, sizeof path, "/proc/%d/stat", pid); if (read_small_file(path, line, sizeof line) >= 0) { char *rp = strrchr(line, ')'); if (rp) { char *p = rp + 2; unsigned long long ut = 0, st = 0; for (int f = 0; f < 14 && *p; f++) { while (*p == ' ' || *p == '\t') p++; if (!*p) break; char *end; unsigned long long v = strtoull(p, &end, 10); if (end == p) { p++; continue; } p = end; if (f == 11) ut = v; /* utime */ if (f == 12) st = v; /* stime */ } unsigned long long cur = ut + st; static double hz = 0.0; if (hz <= 0) { hz = (double)sysconf(_SC_CLK_TCK); if (hz <= 0) hz = 100.0; } int slot = -1, had = 0; unsigned long long prev = 0; for (int i = 0; i < BG_CACHE_MAX; i++) if (g_bg_pid[i] == pid) { had = 1; slot = i; prev = g_bg_tick[i]; break; } if (!had) { for (int i = 0; i < BG_CACHE_MAX; i++) if (g_bg_pid[i] == 0) { slot = i; break; } if (slot < 0) slot = 0; } if (slot >= 0) { g_bg_pid[slot] = pid; g_bg_tick[slot] = cur; } if (had) { *pcpu = 100.0 * (double)(cur - prev) / hz; if (*pcpu < 0) *pcpu = 0; } } } } /* 发射单条 APP 行 JSON(复用于 bg/fg 列表;带 state + is_fg + cpu_pct/rss_kb)。 * toks 为已切分的 `APP ...` 行(n>=9);*first 控制数组元素逗号(调用方初始化为 1)。 * cpu_pct/rss_kb 诚实原则:pid 读不到/不作为可用进程 → null(前端显「—」),绝不编造。 */ static void ams_entry_emit(sbuf_t *out, char **toks, int n, int *first) { const char *st = toks[n - 6]; /* 自右向左:state=tok[n-6], pid=n-5(与 ams_fill 同布局) */ char nm[128]; join_tokens(toks, 2, n - 6, nm, sizeof nm); int pid = atoi(toks[n - 5]); double pcpu = -1.0; long rss = -1; if (pid > 0) bg_proc_sample(pid, &pcpu, &rss); if (!*first) sA(out, ","); *first = 0; sA(out, "{\"pkg\":"); sA_str(out, toks[1]); sA(out, ",\"name\":"); sA_str(out, nm); sA(out, ",\"pid\":%d", pid); sA(out, ",\"state\":"); sA_str(out, st); sA(out, ",\"is_fg\":%s", strcmp(st, "fg") == 0 ? "true" : "false"); sA(out, ",\"cpu_pct\":"); if (pcpu < 0) sA(out, "null"); else sA(out, "%.1f", pcpu); sA(out, ",\"rss_kb\":"); if (rss < 0) sA(out, "null"); else sA(out, "%ld", rss); sA(out, "}"); } /* 解析已 strip 的 `am list` 全文 → bg 列表 JSON(state=bg 的 APP 行;向后兼容专用,selftest 覆盖)。 */ void ams_bg_parse(char *list_text, sbuf_t *s) { char *toks[256]; sbuf_t apps = {0}; int first = 1, count = 0; char *ln = list_text; while (*ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; if (strncmp(ln, "APP ", 4) == 0) { int n = txt_tokens(ln, toks, 256); if (n >= 9) { const char *st = toks[n - 6]; if (strcmp(st, "bg") == 0) { count++; ams_entry_emit(&apps, toks, n, &first); } } } if (nl) { *nl = '\n'; ln = nl + 1; } else break; } sA(s, "{\"available\":true,\"count\":%d,\"ts\":%ld,\"apps\":[%s]}", count, (long)time(NULL), apps.b ? apps.b : ""); sbuf_free(&apps); } /* am list 全文 → bg+fg 双列表 JSON。`apps`(bg)保持向后兼容(count=bg 数量); * 追加 `fg` + `fg_count`(前台 App,前端救命分组)。state=stop 无进程,不在本页(proc 页可见)。 */ void ams_bg_fill(sbuf_t *s) { char out[16384]; char cmd[300]; snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin()); int code = run_cmd(cmd, out, sizeof out); if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; } panel_strip_ansi(out); char *toks[256]; sbuf_t bg = {0}, fg = {0}; int bg_first = 1, fg_first = 1, bg_count = 0, fg_count = 0; char *ln = out; while (*ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; if (strncmp(ln, "APP ", 4) == 0) { int n = txt_tokens(ln, toks, 256); if (n >= 9) { const char *st = toks[n - 6]; if (strcmp(st, "bg") == 0) { bg_count++; ams_entry_emit(&bg, toks, n, &bg_first); } else if (strcmp(st, "fg") == 0) { fg_count++; ams_entry_emit(&fg, toks, n, &fg_first); } } } if (nl) { *nl = '\n'; ln = nl + 1; } else break; } sA(s, "{\"available\":true,\"count\":%d,\"ts\":%ld,\"apps\":[%s]", bg_count, (long)time(NULL), bg.b ? bg.b : ""); sA(s, ",\"fg_count\":%d,\"fg\":[%s]}", fg_count, fg.b ? fg.b : ""); sbuf_free(&bg); sbuf_free(&fg); } int api_ams_bg(int sock) { sbuf_t s = {0}; ams_bg_fill(&s); int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}"); sbuf_free(&s); return rc; } /* 从 am list 解析某 pkg 的当前 pid(未注册/无进程 → 0)。 */ static int ams_pkg_pid(const char *pkg) { char out[16384]; char cmd[300]; snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin()); if (run_cmd(cmd, out, sizeof out) != 0 || !strstr(out, "APP")) return 0; panel_strip_ansi(out); char *toks[256]; char *ln = out; while (*ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; if (strncmp(ln, "APP ", 4) == 0) { int n = txt_tokens(ln, toks, 256); if (n >= 9 && strcmp(toks[1], pkg) == 0) { int pid = atoi(toks[n - 5]); if (nl) *nl = '\n'; return pid; } } if (nl) { *nl = '\n'; ln = nl + 1; } else break; } return 0; } /* /proc//stat 进程是否已消失/僵尸:-1=已消失,1=僵尸,0=存活。 */ static int proc_zombie(int pid) { char path[64], line[256]; snprintf(path, sizeof path, "/proc/%d/stat", pid); if (read_small_file(path, line, sizeof line) < 0) return -1; char *rp = strrchr(line, ')'); if (!rp) return -1; const char *p = rp + 2; while (*p == ' ' || *p == '\t') p++; return (*p == 'Z') ? 1 : 0; } /* 前台强制终止(救命):优先走 `am kill`(launcher 干净 SIGTERM 序列,accepted);若 launcher 拒 * (framework / 无前台会话 → "launcher refused"),回退:解析 am list 拿当前 pid → 直接 SIGTERM, * 宽限后仍存活走 SIGKILL(与 launcher 杀序列同语义)。这是用户明示「前台卡死救命」的强通道—— * 前端已二次确认 + 标注「前台」。require_auth 在路由层已护。 */ int api_ams_kill_fg(int sock, const char *pkg) { if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); /* ① 干净通道:am kill(launcher 执行 SIGTERM 序列)。 */ { char out[256]; char cmd[320]; snprintf(cmd, sizeof cmd, "%s kill %s 2>/dev/null", ams_bin(), pkg); int code = run_cmd(cmd, out, sizeof out); if (code == 0 && strstr(out, "OK ")) { sbuf_t s = {0}; sA(&s, "{\"accepted\":true,\"op\":\"kill-fg\",\"pkg\":"); sA_str(&s, pkg); sA(&s, ",\"force\":false,\"message\":"); sA_str(&s, out); sA(&s, "}"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } panel_log("ams kill-fg %s via am refused: %s -> direct signal fallback", pkg, out[0] ? out : "(empty)"); } /* ② 回退:直接按 pid 信号(绕过 launcher 门控)。 */ int pid = ams_pkg_pid(pkg); if (pid <= 0) return send_err(sock, 409, "CONFLICT", "该 App 当前无进程可终止"); kill(pid, SIGTERM); int killed_hard = 0; for (int i = 0; i < 5; i++) { /* SIGTERM 宽限 ~0.6s(与 launcher 杀序列同语义) */ struct timespec ts = {0, 120 * 1000 * 1000}; nanosleep(&ts, NULL); int z = proc_zombie(pid); if (z < 0 || z == 1) break; /* 消失/僵尸 → 已终止 */ } if (proc_zombie(pid) == 0) { kill(pid, SIGKILL); killed_hard = 1; } char msg[192]; snprintf(msg, sizeof msg, "force terminated pid %d via direct signal%s", pid, killed_hard ? " (SIGKILL 强杀)" : " (SIGTERM)"); sbuf_t s = {0}; sA(&s, "{\"accepted\":true,\"op\":\"kill-fg\",\"pkg\":"); sA_str(&s, pkg); sA(&s, ",\"force\":%s,\"pid\":%d,\"message\":", killed_hard ? "true" : "false", pid); sA_str(&s, msg); sA(&s, "}"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; }