1518 lines
61 KiB
C
1518 lines
61 KiB
C
/* util.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
|
||
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
|
||
#include "meow_launcher_internal.h"
|
||
|
||
/* M2.x 应用日志目录简化:一次性迁移 logs/apps/<id>/logs → logs/<id>(定义见下,log_open_card 先调用)。 */
|
||
static void log_migrate_apps(void);
|
||
|
||
int log_fd = -1; /* file log fd on the TF card */
|
||
|
||
char log_dir[256] = ""; /* "MeowOS/logs" on card, or "" */
|
||
|
||
char log_file[256] = ""; /* full path of the boot log file */
|
||
|
||
|
||
/* M2.85 内存护栏:读 /proc/meminfo 的 MemAvailable(kB)。失败返回 -1。
|
||
* 供 launcher 在低内存时对大字库请求降档(OOM 防线)。 */
|
||
long sys_mem_available_kb(void)
|
||
{
|
||
FILE *f = fopen("/proc/meminfo", "r");
|
||
if (!f) return -1;
|
||
char line[256];
|
||
long kb = -1;
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (sscanf(line, "MemAvailable: %ld kB", &kb) == 1) break;
|
||
}
|
||
fclose(f);
|
||
return kb;
|
||
}
|
||
|
||
|
||
static int card_available(void)
|
||
{
|
||
/* TF card mounted? */
|
||
FILE *f = fopen(MOUNTS, "r");
|
||
if (!f) return 0;
|
||
char line[256];
|
||
int found = 0;
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (strstr(line, " /usr/data/mnt/sd_0 ")) { found = 1; break; }
|
||
}
|
||
fclose(f);
|
||
if (!found) return 0;
|
||
/* writable logs dir? */
|
||
mkdir(LOG_DIR_CARD, 0755);
|
||
return access(LOG_DIR_CARD, W_OK) == 0;
|
||
}
|
||
|
||
|
||
static void boot_timestamp(char *out, size_t n)
|
||
{
|
||
/* reuse the timestamp cached for this boot session (/tmp is cleared on
|
||
* reboot) so repeated launcher starts in one boot share one log file */
|
||
int fd = open("/tmp/meow_bootts", O_RDONLY);
|
||
if (fd >= 0) {
|
||
int r = read(fd, out, (int)n - 1);
|
||
close(fd);
|
||
if (r > 0) { out[r] = 0; return; }
|
||
}
|
||
time_t now = time(NULL);
|
||
time_t up = 0;
|
||
fd = open("/proc/uptime", O_RDONLY);
|
||
if (fd >= 0) {
|
||
char buf[64];
|
||
int r = read(fd, buf, sizeof buf - 1);
|
||
close(fd);
|
||
if (r > 0) {
|
||
buf[r] = 0;
|
||
char *p = strchr(buf, ' ');
|
||
if (p) *p = 0;
|
||
up = (time_t)atof(buf);
|
||
}
|
||
}
|
||
time_t boot = (time_t)((double)now - (double)up + 0.5);
|
||
struct tm tm;
|
||
localtime_r(&boot, &tm);
|
||
strftime(out, n, "meow_%Y%m%d_%H%M%S", &tm);
|
||
/* cache for the rest of this boot session */
|
||
fd = open("/tmp/meow_bootts", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) {
|
||
write(fd, out, strlen(out));
|
||
close(fd);
|
||
}
|
||
}
|
||
|
||
|
||
unsigned long long now_ms(void)
|
||
{
|
||
struct timespec ts;
|
||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
return (unsigned long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||
}
|
||
|
||
|
||
/* M2.18 Bug 1: 无符号回绕保护的「空闲时长」。
|
||
* launch_app()/confirm_reboot_player() 是阻塞调用,主循环 for(;;) 顶部的 t=now_ms()
|
||
* 在 app 运行期间被卡住而停在启动前旧值;app 退出后 last_input_ms 被置为 app 退出
|
||
* 时刻(较大值)。若该圈 any_input==0(如触摸释放单独落在下一圈、音量/播放边沿发生在
|
||
* 更早圈),line「if (any_input) last_input_ms = t」不执行 → 检查处 t < last_input_ms,
|
||
* (unsigned long long)(t - last_input_ms) 无符号相减回绕成 2^64-Δ 的极大值,恒 >= 30s
|
||
* → 误判「空闲超时」→ 立即 meow_panel_off()。r3proii 日志铁证:auto screen-off after
|
||
* 30s idle (t=141578 last=145688),仅 122ms 即被误判为已空闲 30s。凡判定空闲一律经此
|
||
* 函数取差:t < last(t 过期)时返回 0,绝不回绕。 */
|
||
unsigned long long idle_ms_since(unsigned long long t, unsigned long long last)
|
||
{
|
||
return (t >= last) ? (t - last) : 0;
|
||
}
|
||
|
||
|
||
/* remove oldest log files until total size <= limit (called at boot) */
|
||
void log_cleanup_old(void)
|
||
{
|
||
struct dirent *de;
|
||
DIR *d = opendir(LOG_DIR_CARD);
|
||
if (!d) return;
|
||
/* collect meow_*.log with mtime + size */
|
||
struct logfile { char name[64]; long long size; long long mtime; } files[64];
|
||
int n = 0;
|
||
while ((de = readdir(d)) && n < 64) {
|
||
if (strncmp(de->d_name, "meow_", 5)) continue;
|
||
char path[320];
|
||
snprintf(path, sizeof path, "%s/%s", LOG_DIR_CARD, de->d_name);
|
||
struct stat st;
|
||
if (stat(path, &st) || !S_ISREG(st.st_mode)) continue;
|
||
files[n].mtime = (long long)st.st_mtime;
|
||
files[n].size = (long long)st.st_size;
|
||
snprintf(files[n].name, sizeof files[n].name, "%s", de->d_name);
|
||
n++;
|
||
}
|
||
closedir(d);
|
||
long long total = 0;
|
||
for (int i = 0; i < n; i++) total += files[i].size;
|
||
/* simple selection sort by mtime ascending (oldest first) */
|
||
for (int i = 0; i < n; i++)
|
||
for (int j = i + 1; j < n; j++)
|
||
if (files[j].mtime < files[i].mtime) {
|
||
struct logfile t = files[i];
|
||
files[i] = files[j]; files[j] = t;
|
||
}
|
||
int deleted = 0;
|
||
for (int i = 0; i < n && total > (long long)LOG_SIZE_LIMIT; i++) {
|
||
char path[320];
|
||
snprintf(path, sizeof path, "%s/%s", LOG_DIR_CARD, files[i].name);
|
||
if (unlink(path) == 0) { total -= files[i].size; deleted++; }
|
||
}
|
||
if (deleted) fprintf(stderr, "log_cleanup: deleted %d old log(s), total now %lld\n",
|
||
deleted, total);
|
||
}
|
||
|
||
|
||
/* try to open the card log file; returns 0 on success */
|
||
int log_open_card(void)
|
||
{
|
||
if (log_fd >= 0) return 0;
|
||
if (!card_available()) return -1;
|
||
log_migrate_apps(); /* M2.x 一次性迁移:logs/apps/<id>/logs → logs/<id>(幂等,见下) */
|
||
char ts[64];
|
||
boot_timestamp(ts, sizeof ts);
|
||
snprintf(log_dir, sizeof log_dir, "%s", LOG_DIR_CARD);
|
||
snprintf(log_file, sizeof log_file, "%s/%s.log", LOG_DIR_CARD, ts);
|
||
int fd = open(log_file, O_WRONLY | O_APPEND | O_CREAT, 0644);
|
||
if (fd < 0) return -1;
|
||
log_fd = fd;
|
||
return 0;
|
||
}
|
||
|
||
|
||
/* M2.x 应用日志目录简化:mkdir -p(幂等,已存在 EEXIST 视为成功)。返回 0=成功 -1=失败。
|
||
* 供 launcher 为每 App 建新布局日志目录 logs/<id>/ 用(沿用 SDK 同款逐分量 mkdir)。 */
|
||
int meow_mkdir_p(const char *path)
|
||
{
|
||
char tmp[256];
|
||
size_t len, i;
|
||
if (!path || !path[0] || strlen(path) >= sizeof tmp) return -1;
|
||
len = strlen(path);
|
||
memcpy(tmp, path, len + 1);
|
||
if (tmp[len - 1] == '/') tmp[len - 1] = '\0'; /* 去尾斜杠 */
|
||
for (i = 1; i <= strlen(tmp); i++) {
|
||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||
char ch = tmp[i];
|
||
tmp[i] = '\0';
|
||
if (tmp[0] && mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1;
|
||
tmp[i] = ch;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
|
||
/* M2.x 一次性迁移(幂等,代码可后续移除):旧布局 LOG_DIR_CARD/apps/<id>/logs/<file>.log
|
||
* 并入新布局 LOG_DIR_CARD/<id>/<file>.log,然后删除旧 apps/ 整树。卡可用时随 log_open_card
|
||
* 跑一次;apps/ 已不存在则直接返回。同卡同 fs,文件用 rename() 移动(目标同名被覆盖——迁移
|
||
* 内只挪旧文件,不写新日志)。总容量上限纪律不变:应用日志仍由 SDK 按 App 50MB 清理,boot 日志
|
||
* 仍走 log_cleanup_old()。 */
|
||
static void log_rm_rf(const char *path)
|
||
{
|
||
struct stat st;
|
||
if (stat(path, &st) != 0) return;
|
||
if (S_ISDIR(st.st_mode)) {
|
||
DIR *d = opendir(path);
|
||
if (d) {
|
||
struct dirent *e;
|
||
while ((e = readdir(d)) != NULL) {
|
||
if (e->d_name[0] == '.' && (e->d_name[1] == '\0' ||
|
||
(e->d_name[1] == '.' && e->d_name[2] == '\0'))) continue;
|
||
char sub[512];
|
||
snprintf(sub, sizeof sub, "%s/%s", path, e->d_name);
|
||
log_rm_rf(sub);
|
||
}
|
||
closedir(d);
|
||
}
|
||
rmdir(path);
|
||
} else {
|
||
unlink(path);
|
||
}
|
||
}
|
||
|
||
static void log_migrate_apps(void)
|
||
{
|
||
char old_root[512], old_dir[512], old_logs[512], new_dir[512];
|
||
DIR *d, *dl, *dd;
|
||
struct dirent *e;
|
||
|
||
snprintf(old_root, sizeof old_root, "%s/apps", LOG_DIR_CARD);
|
||
d = opendir(old_root);
|
||
if (!d) return; /* 旧路径不存在,无需迁移 */
|
||
|
||
while ((e = readdir(d)) != NULL) {
|
||
struct stat st;
|
||
if (e->d_name[0] == '.') continue;
|
||
snprintf(old_dir, sizeof old_dir, "%s/%s", old_root, e->d_name);
|
||
if (stat(old_dir, &st) != 0 || !S_ISDIR(st.st_mode)) continue;
|
||
|
||
snprintf(new_dir, sizeof new_dir, "%s/%s", LOG_DIR_CARD, e->d_name);
|
||
if (meow_mkdir_p(new_dir) != 0) continue; /* 新目录建不了,跳过该 App */
|
||
|
||
/* 旧布局 apps/<id>/logs/<file>.log → logs/<id>/<file>.log */
|
||
snprintf(old_logs, sizeof old_logs, "%s/logs", old_dir);
|
||
dl = opendir(old_logs);
|
||
if (dl) {
|
||
struct dirent *el;
|
||
while ((el = readdir(dl)) != NULL) {
|
||
struct stat stl;
|
||
char src[512], dst[512];
|
||
if (el->d_name[0] == '.') continue;
|
||
snprintf(src, sizeof src, "%s/%s", old_logs, el->d_name);
|
||
if (stat(src, &stl) != 0 || !S_ISREG(stl.st_mode)) continue;
|
||
snprintf(dst, sizeof dst, "%s/%s", new_dir, el->d_name);
|
||
rename(src, dst);
|
||
}
|
||
closedir(dl);
|
||
}
|
||
|
||
/* 早期设计若直接把 .log 散在 apps/<id>/ 下,一并并入(跳过已处理的 logs/ 子目录) */
|
||
dd = opendir(old_dir);
|
||
if (dd) {
|
||
struct dirent *ed;
|
||
while ((ed = readdir(dd)) != NULL) {
|
||
struct stat stf;
|
||
char src[512], dst[512];
|
||
if (ed->d_name[0] == '.') continue;
|
||
if (strcmp(ed->d_name, "logs") == 0) continue;
|
||
snprintf(src, sizeof src, "%s/%s", old_dir, ed->d_name);
|
||
if (stat(src, &stf) != 0 || !S_ISREG(stf.st_mode)) continue;
|
||
snprintf(dst, sizeof dst, "%s/%s", new_dir, ed->d_name);
|
||
rename(src, dst);
|
||
}
|
||
closedir(dd);
|
||
}
|
||
|
||
log_rm_rf(old_dir);
|
||
}
|
||
closedir(d);
|
||
rmdir(old_root); /* apps/ 清空则删;非空则忽略 */
|
||
fprintf(stderr, "log_migrate: logs/apps/* -> logs/<id> done\n");
|
||
}
|
||
|
||
|
||
/* flush the /tmp boot buffer into the file log (once the card mounts) */
|
||
void log_poll_card(void)
|
||
{
|
||
if (log_fd >= 0) return;
|
||
if (log_open_card() != 0) return;
|
||
/* move the /tmp boot buffer into the card log, then drop it */
|
||
int fd = open(BOOT_BUF_TMP, O_RDONLY);
|
||
if (fd >= 0) {
|
||
char chunk[1024];
|
||
ssize_t n;
|
||
while ((n = read(fd, chunk, sizeof chunk)) > 0) {
|
||
if (write(log_fd, chunk, n) < 0) { close(fd); log_fd = -1; return; }
|
||
}
|
||
close(fd);
|
||
unlink(BOOT_BUF_TMP);
|
||
logmsg("boot buffer moved into %s", log_file);
|
||
} else {
|
||
logmsg("card log open: %s", log_file);
|
||
}
|
||
}
|
||
|
||
|
||
/* bounded append to the /tmp boot buffer (no card log open yet) */
|
||
static void boot_buf_put(const char *line, int len)
|
||
{
|
||
struct stat st;
|
||
if (stat(BOOT_BUF_TMP, &st) == 0 && st.st_size > BOOT_BUF_MAX)
|
||
unlink(BOOT_BUF_TMP); /* roll the buffer (boot-stage only) */
|
||
int fd = open(BOOT_BUF_TMP, O_WRONLY | O_APPEND | O_CREAT, 0644);
|
||
if (fd < 0) return;
|
||
if (write(fd, line, len) < 0) { /* best effort */ }
|
||
close(fd);
|
||
}
|
||
|
||
|
||
void logmsg(const char *fmt, ...)
|
||
{
|
||
char buf[512];
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
vsnprintf(buf, sizeof buf, fmt, ap);
|
||
va_end(ap);
|
||
char line[576];
|
||
int l = snprintf(line, sizeof line, "%llu %s\n", now_ms(), buf);
|
||
if (log_fd >= 0) {
|
||
if (write(log_fd, line, l) < 0) log_fd = -1; /* card gone -> buffer */
|
||
}
|
||
if (log_fd < 0) boot_buf_put(line, l);
|
||
fprintf(stderr, "%llu %s\n", now_ms(), buf);
|
||
}
|
||
|
||
int screen_off_timeout = SCREEN_OFF_TIMEOUT_DEFAULT;
|
||
|
||
int screen_off_mode = SCREEN_OFF_MODE_DEFAULT;
|
||
|
||
int power_longpress_menu_ms = PWR_MENU_MS_DEFAULT;
|
||
|
||
int power_forceoff_ms = PWR_FORCE_MS_DEFAULT;
|
||
|
||
int refresh_rate_cfg = REFRESH_RATE_CFG_DEFAULT; /* 目标档 60/30/15(上限,default 60) */
|
||
|
||
int refresh_always_max = 0; /* 1=恒 max 不降档 */
|
||
|
||
static void meow_dirty_reset(void);
|
||
|
||
|
||
void put_px(int x, int y, uint32_t color)
|
||
{
|
||
if (x < 0 || x >= fb_xres || y < 0 || y >= fb_yres) return;
|
||
unsigned char *p = fb + y * fb_line + x * (fb_bpp / 8);
|
||
if (fb_bpp == 16) {
|
||
uint16_t c = ((color >> 8) & 0xF800) | ((color >> 5) & 0x07E0) |
|
||
((color >> 3) & 0x001F);
|
||
*(uint16_t *)p = c;
|
||
} else {
|
||
*(uint32_t *)p = (color << 8) | 0xFF;
|
||
}
|
||
}
|
||
|
||
|
||
void fill_rect(int x0, int y0, int w, int h, uint32_t color)
|
||
{
|
||
for (int y = y0; y < y0 + h; y++)
|
||
for (int x = x0; x < x0 + w; x++)
|
||
put_px(x, y, color);
|
||
}
|
||
|
||
|
||
/* 5x7 font (same table as hello.c) */
|
||
static const unsigned char font[][8] = {
|
||
{0x0E,0x11,0x13,0x15,0x19,0x11,0x0E,0x00},
|
||
{0x04,0x0C,0x04,0x04,0x04,0x04,0x0E,0x00},
|
||
{0x0E,0x11,0x01,0x02,0x04,0x08,0x1F,0x00},
|
||
{0x1F,0x02,0x04,0x02,0x01,0x11,0x0E,0x00},
|
||
{0x02,0x06,0x0A,0x12,0x1F,0x02,0x02,0x00},
|
||
{0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E,0x00},
|
||
{0x06,0x08,0x10,0x1E,0x11,0x11,0x0E,0x00},
|
||
{0x1F,0x01,0x02,0x04,0x08,0x08,0x08,0x00},
|
||
{0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E,0x00},
|
||
{0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C,0x00},
|
||
{0x0E,0x11,0x11,0x1F,0x11,0x11,0x11,0x00},
|
||
{0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E,0x00},
|
||
{0x0E,0x11,0x10,0x10,0x10,0x11,0x0E,0x00},
|
||
{0x1C,0x12,0x11,0x11,0x11,0x12,0x1C,0x00},
|
||
{0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F,0x00},
|
||
{0x1F,0x10,0x10,0x1E,0x10,0x10,0x10,0x00},
|
||
{0x0E,0x11,0x10,0x17,0x11,0x11,0x0F,0x00},
|
||
{0x11,0x11,0x11,0x1F,0x11,0x11,0x11,0x00},
|
||
{0x0E,0x04,0x04,0x04,0x04,0x04,0x0E,0x00},
|
||
{0x07,0x02,0x02,0x02,0x02,0x12,0x0C,0x00},
|
||
{0x11,0x12,0x14,0x18,0x14,0x12,0x11,0x00},
|
||
{0x10,0x10,0x10,0x10,0x10,0x10,0x1F,0x00},
|
||
{0x11,0x1B,0x15,0x15,0x11,0x11,0x11,0x00},
|
||
{0x11,0x19,0x15,0x13,0x11,0x11,0x11,0x00},
|
||
{0x0E,0x11,0x11,0x11,0x11,0x11,0x0E,0x00},
|
||
{0x1E,0x11,0x11,0x1E,0x10,0x10,0x10,0x00},
|
||
{0x0E,0x11,0x11,0x11,0x15,0x12,0x0D,0x00},
|
||
{0x1E,0x11,0x11,0x1E,0x14,0x12,0x11,0x00},
|
||
{0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E,0x00},
|
||
{0x1F,0x04,0x04,0x04,0x04,0x04,0x04,0x00},
|
||
{0x11,0x11,0x11,0x11,0x11,0x11,0x0E,0x00},
|
||
{0x11,0x11,0x11,0x11,0x11,0x0A,0x04,0x00},
|
||
{0x11,0x11,0x11,0x15,0x15,0x1B,0x11,0x00},
|
||
{0x11,0x11,0x0A,0x04,0x0A,0x11,0x11,0x00},
|
||
{0x11,0x11,0x11,0x0A,0x04,0x04,0x04,0x00},
|
||
{0x1F,0x01,0x02,0x04,0x08,0x10,0x1F,0x00},
|
||
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
|
||
{0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00},
|
||
{0x04,0x04,0x04,0x04,0x04,0x00,0x04,0x00},
|
||
{0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00},
|
||
{0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00},
|
||
};
|
||
|
||
|
||
static int glyph_index(char c)
|
||
{
|
||
if (c >= '0' && c <= '9') return c - '0';
|
||
if (c >= 'A' && c <= 'Z') return 10 + (c - 'A');
|
||
if (c >= 'a' && c <= 'z') return 10 + (c - 'a');
|
||
if (c == ' ') return 36;
|
||
if (c == '.') return 37;
|
||
if (c == '!') return 38;
|
||
if (c == '-' || c == '_') return 39;
|
||
return 36;
|
||
}
|
||
|
||
|
||
static void draw_glyph(int x, int y, char c, int scale, uint32_t color)
|
||
{
|
||
const unsigned char *g = font[glyph_index(c)];
|
||
for (int row = 0; row < 7; row++) {
|
||
unsigned char bits = g[row];
|
||
for (int col = 0; col < 5; col++) {
|
||
if (bits & (0x10 >> col)) {
|
||
for (int dy = 0; dy < scale; dy++)
|
||
for (int dx = 0; dx < scale; dx++)
|
||
put_px(x + col * scale + dx, y + row * scale + dy, color);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
void draw_text(int x, int y, const char *s, int scale, uint32_t color)
|
||
{
|
||
while (*s) {
|
||
draw_glyph(x, y, *s, scale, color);
|
||
x += 6 * scale;
|
||
s++;
|
||
}
|
||
}
|
||
|
||
|
||
/* 24-bit RGB → RGB565(与 put_px/meow_font 一致) */
|
||
uint16_t rgb565(uint32_t c)
|
||
{
|
||
return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F));
|
||
}
|
||
|
||
|
||
/* 统一文本绘制:字库就绪用 meow_font(真字体/中文),否则回退 5x7 点阵。
|
||
* size 为字库字号(28=状态栏/大标题,20=正文/按钮);回退时按 scale 近似。 */
|
||
void ui_draw(int x, int y, const char *s, uint32_t color, int size)
|
||
{
|
||
if (mfont_ready) {
|
||
meow_font_drawtext((uint16_t *)fb, x, y, s, rgb565(color), size);
|
||
} else {
|
||
draw_text(x, y, s, (size >= 16) ? 2 : 1, color);
|
||
}
|
||
}
|
||
|
||
|
||
/* 文本宽度(字库 or 回退估算),用于右对齐/居中 */
|
||
int ui_width(const char *s, int size)
|
||
{
|
||
if (mfont_ready) return meow_font_width_text(s, size);
|
||
return (int)strlen(s) * 6 * ((size >= 16) ? 2 : 1);
|
||
}
|
||
|
||
|
||
/* 文本高度(字库行高 or 回退 7×scale 估算),用于垂直居中 / 行高预算。
|
||
* (拆分后无调用点,保留接口;unused 满足 -Wall 0 warning) */
|
||
static int __attribute__((unused)) ui_text_height(int size)
|
||
{
|
||
if (mfont_ready) return meow_font_line_height(size);
|
||
return 7 * ((size >= 16) ? 2 : 1);
|
||
}
|
||
|
||
|
||
/* ================================================================== */
|
||
/* A-1/A-2 渲染桥:launcher 手动脏模型 → SDK 脏追踪,呈现走 meow_ui_flush() */
|
||
/* launcher 的绘制(put_px/fill_rect/meow_font/dim_screen)直接把像素画进 */
|
||
/* SDK 的 back_buf(fb),在此仅把「哪些区域变了」桥进 SDK 的脏追踪; */
|
||
/* present 不再由 launcher 做,统一 meow_ui_flush() = SDK present(内含 */
|
||
/* vsync + 帧率门控 + 原子 blit 脏区)。launcher 不再自带 meow_present/ */
|
||
/* refresh_governor —— 帧率调度已归口 SDK(PERF_SCHEDULER 显示子域)。 */
|
||
/* ================================================================== */
|
||
|
||
/* 标记全屏脏:桥进 SDK 的整帧脏,下次 meow_ui_flush() 整帧 blit。 */
|
||
void meow_dirty_full(void) { meow_gfx_mark_full(); }
|
||
|
||
|
||
/* 清空脏集:SDK present 上屏后自清;launcher 侧无需再清(保留接口兼容)。
|
||
* (拆分后无调用点,保留钩子;unused 满足 -Wall 0 warning) */
|
||
static void __attribute__((unused)) meow_dirty_reset(void) { }
|
||
|
||
|
||
/* 标记一个矩形脏:桥进 SDK 脏追踪(相交合并/超限转全屏由 SDK dirty_add 处理)。 */
|
||
void meow_dirty_add(int x, int y, int w, int h) { meow_gfx_mark_dirty(x, y, w, h); }
|
||
|
||
|
||
/* 标识符字符(含 '.',用于防止 json_str 误命中更长键的一部分) */
|
||
static int ident_ch(char c)
|
||
{
|
||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||
(c >= '0' && c <= '9') || c == '_' || c == '.';
|
||
}
|
||
|
||
|
||
/* 极简 JSON/扁平值提取:在一行里找 key,读取其值(跳过 ':'/'=' 与可选引号)。
|
||
* 支持 JSON `"name": "Hello"` 与扁平 `framework=true`。返回拷贝到 out 的字符数。
|
||
* 返回 0=未命中/空。 */
|
||
int json_str(const char *buf, const char *key, char *out, int olen)
|
||
{
|
||
out[0] = 0;
|
||
if (!buf || !key || olen <= 0) return 0;
|
||
const char *p = strstr(buf, key);
|
||
if (!p) return 0;
|
||
if (p > buf && ident_ch((unsigned char)p[-1])) return 0; /* 键前缀需独立 */
|
||
p += strlen(key);
|
||
if (*p == '"') p++; /* JSON 键自身右引号:跳到 ':'/'=' */
|
||
while (*p && (*p == ':' || *p == '=')) p++;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (*p == '"') p++; /* 值左引号(JSON 字符串值) */
|
||
int i = 0;
|
||
while (*p && *p != '"' && *p != ',' && *p != '\n' && *p != '\r' && i < olen - 1)
|
||
out[i++] = *p++;
|
||
out[i] = 0;
|
||
while (i > 0 && (out[i - 1] == ' ' || out[i - 1] == '\t')) out[--i] = 0;
|
||
return i;
|
||
}
|
||
|
||
|
||
/* state: 0 ok / 1 no storage card / 2 no usable apps */
|
||
int card_present(void)
|
||
{
|
||
FILE *f = fopen(MOUNTS, "r");
|
||
if (!f) return 0;
|
||
char line[256];
|
||
int found = 0;
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (strstr(line, " /usr/data/mnt/sd_0 ")) { found = 1; break; }
|
||
}
|
||
fclose(f);
|
||
return found;
|
||
}
|
||
|
||
|
||
/* ================================================================== */
|
||
/* MeowOS 系统拔营(M2.2 批次):TF 卡挂载 + WiFi 联网 */
|
||
/* 官方环境由 hiby_player / sys_server 负责这两件事;MeowOS 作为主系统 */
|
||
/* 时无播放器,必须由 launcher 主动接管。失败不阻塞主界面,后台周期重试。 */
|
||
/* ================================================================== */
|
||
int sd_mounted(void)
|
||
{
|
||
return card_present();
|
||
}
|
||
|
||
|
||
/* TF 卡设备节点是否出现(任一常见节点即视为在卡) */
|
||
int sd_device_present(void)
|
||
{
|
||
return access("/dev/mmcblk0", F_OK) == 0 || access("/dev/mmcblk0p1", F_OK) == 0;
|
||
}
|
||
|
||
|
||
/* 主动挂载 TF 卡到 /usr/data/mnt/sd_0。返回 1=已挂载,0=尚未(调用方轮询重试)。
|
||
* 依次尝试 /dev/mmcblk0、/dev/mmcblk0p1,先 -t exfat 再自动探测;卡未插入返回 0。 */
|
||
int ensure_card_mounted(void)
|
||
{
|
||
if (sd_mounted()) return 1;
|
||
if (!sd_device_present()) return 0;
|
||
const char *nodes[2] = { "/dev/mmcblk0", "/dev/mmcblk0p1" };
|
||
for (int i = 0; i < 2; i++) {
|
||
if (access(nodes[i], F_OK) != 0) continue;
|
||
char cmd[160];
|
||
snprintf(cmd, sizeof cmd, "mkdir -p /usr/data/mnt/sd_0; "
|
||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin mount -t exfat %s /usr/data/mnt/sd_0 2>/dev/null",
|
||
nodes[i]);
|
||
if (system(cmd) == 0 && sd_mounted()) { logmsg("sd_0 mounted: %s", nodes[i]); return 1; }
|
||
snprintf(cmd, sizeof cmd, "mkdir -p /usr/data/mnt/sd_0; "
|
||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin mount %s /usr/data/mnt/sd_0 2>/dev/null",
|
||
nodes[i]);
|
||
if (system(cmd) == 0 && sd_mounted()) { logmsg("sd_0 mounted (auto): %s", nodes[i]); return 1; }
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
|
||
int read_file_int(const char *path)
|
||
{
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
char buf[16];
|
||
int n = read(fd, buf, sizeof buf - 1);
|
||
close(fd);
|
||
if (n <= 0) return -1;
|
||
buf[n] = 0;
|
||
return atoi(buf);
|
||
}
|
||
|
||
|
||
/* ---- 通知 dump 回调(通知#6 转储 TF 卡;做成真)----
|
||
* meow_notif 在 TTL 到点(meow_notif_tick,§8 10s)调用此回调归档一条通知,随后释放槽。
|
||
* 本回调把 meow_notif 结构体转成一行 CSV 追加写入 <卡>/MeowOS/notifications/notifications.log
|
||
* (含 mkdir -p 与按总大小滚动截断、保留最新 10MB),落盘实现在 meow_notif.c 的
|
||
* meow_notif_dump_tf()。无卡/不可写仅打点降级(NOTIFICATION_SYSTEM.md §5/§8 + MEWSPEC §5.3)。 */
|
||
static void notif_dump_tf_placeholder(const meow_notif_t *n)
|
||
{
|
||
meow_notif_dump_tf(n);
|
||
}
|
||
|
||
|
||
/* ---- 通知图标回退回调(用户定案:icon 为空 → 用该 app 默认图标)----
|
||
* post 的 app_icon 为空时调用(meow_notif.c 的 notif_resolve_icon),按 app_id 在 app
|
||
* 注册表(scan_apps 静态 app.json 的 icon)里找该 app 的图标(name/path 或空)。 */
|
||
static int notif_app_icon_fallback(const char *app_id, char *out, size_t outsz)
|
||
{
|
||
if (!app_id || !app_id[0] || !out || outsz == 0) return 0;
|
||
struct meow_acct *a = meow_acct_find(app_id);
|
||
if (a && a->icon[0]) { snprintf(out, outsz, "%s", a->icon); return 1; }
|
||
return 0; /* 无注册/无图标 → 保持空(通知中心 UI 再回退) */
|
||
}
|
||
|
||
void settings_load(void)
|
||
{
|
||
meow_settings_init(); /* 幂等:注册默认 + 读 settings.conf */
|
||
meow_notif_init(); /* M2a:通知存储初始化(幂等,读 notify.* 配额) */
|
||
meow_notif_set_dump_cb(notif_dump_tf_placeholder); /* 通知#6:挂 dump 回调 = TF 转储(meow_notif_dump_tf:一行 CSV 追加 + 10MB 滚动截断) */
|
||
meow_notif_set_icon_cb(notif_app_icon_fallback); /* icon 空 → 按 app_id 回退该 app 默认图标(app.json icon) */
|
||
if (!settings_cache_registered) {
|
||
meow_settings_add_listener(settings_sync_cache_listener);
|
||
settings_cache_registered = 1;
|
||
}
|
||
settings_sync_cache_from_backend();
|
||
/* M2.86 开机亮度竞态:launcher S70 早启动 /usr/data 未就绪 → 首次 init 读到的是纯默认值
|
||
* (display.brightness=101→clamp 100),apply_user_brightness 已挂起 meow_brightness_pending。
|
||
* 此处 settings 一旦真正从盘上读到(meow_settings_loaded()==1)即补应用真实用户亮度。
|
||
* 幂等:正常路径(已读到、无挂起)恒为 no-op;仅「未读到→已读到」转换时触发一次。 */
|
||
if (meow_settings_loaded() && meow_brightness_pending) {
|
||
apply_user_brightness();
|
||
logmsg("deferred user brightness applied after settings ready");
|
||
}
|
||
logmsg("settings: vol=%d(max=%d,step=%d) refresh=%d(always_max=%d) off=%ds mode=%s",
|
||
settings_volume, settings_volume_max, settings_volume_step,
|
||
refresh_rate_cfg, refresh_always_max, screen_off_timeout,
|
||
screen_off_mode ? "black" : "panel");
|
||
}
|
||
|
||
meow_sys_state g_sys;
|
||
|
||
char g_meowos_version[16] = MEOWOS_VERSION_DEF;
|
||
|
||
static void load_meowos_version(void)
|
||
{
|
||
FILE *f = fopen(MEOWOS_VERSION_FILE, "r");
|
||
if (!f) { logmsg("meowos_version: %s missing, use default %s", MEOWOS_VERSION_FILE, g_meowos_version); return; }
|
||
char buf[32];
|
||
if (fgets(buf, sizeof buf, f)) {
|
||
char *e = buf + strlen(buf);
|
||
while (e > buf && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ')) e--;
|
||
*e = 0;
|
||
if (buf[0]) snprintf(g_meowos_version, sizeof g_meowos_version, "%s", buf);
|
||
}
|
||
fclose(f);
|
||
logmsg("meowos_version: read %s = '%s'", MEOWOS_VERSION_FILE, g_meowos_version);
|
||
}
|
||
|
||
|
||
/* 读一份 sysfs 字符串(剥尾随换行);失败回退 "" */
|
||
static void read_sys_str(const char *path, char *buf, size_t n)
|
||
{
|
||
buf[0] = 0;
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) return;
|
||
ssize_t r = read(fd, buf, n - 1);
|
||
close(fd);
|
||
if (r <= 0) { buf[0] = 0; return; }
|
||
buf[r] = 0;
|
||
while (r > 0 && (buf[r-1] == '\n' || buf[r-1] == '\r')) buf[--r] = 0;
|
||
}
|
||
|
||
|
||
/* 蓝牙实际上电状态(A/E:不只看 hci0 存在——节点常在但控制器可能 DOWN)。
|
||
* 本设备 /sys/class/bluetooth/hci0/powered 不存在(旧内核/BlueZ 5.54),
|
||
* 权威来源为 hciconfig -a hci0 的 UP/DOWN 行。费用较高(spawn),故缓存,
|
||
* 由 meow_sys_refresh()(进系统一次)/meow_sys_light()(轻刷)节流更新;
|
||
* 状态栏只读 g_sys.bt_on。 */
|
||
int hci_powered(void)
|
||
{
|
||
FILE *p = popen("hciconfig -a hci0 2>/dev/null", "r");
|
||
if (!p) return 0;
|
||
int up = 0;
|
||
char buf[256];
|
||
while (fgets(buf, sizeof buf, p)) {
|
||
/* 状态行含 "UP RUNNING" 或 "DOWN"。匹配单词 UP(避免误中 UART 等)。 */
|
||
char *s = buf, *tok;
|
||
while ((tok = strsep(&s, " \t\r\n")) != NULL) {
|
||
if (!strcmp(tok, "UP")) { up = 1; break; }
|
||
}
|
||
}
|
||
pclose(p);
|
||
return up;
|
||
}
|
||
|
||
/* 读 /proc/net/wireless 的 wlan0 level(dBm,如 -46);失败(文件不存在/无 wlan0 行/解析失败)返回
|
||
* WIFI_LVL_UNKNOWN。行格式(Linux wext)形如 " wlan0: 0000 64. -46. -256 0 ...";
|
||
* 接口名后第 3 个空白分隔数值 = level(注意末尾带 '.',需剥离)。状态栏 WiFi 信号分档喂给 SDK。 */
|
||
static int wifi_signal_level(void)
|
||
{
|
||
FILE *f = fopen("/proc/net/wireless", "r");
|
||
if (!f) return WIFI_LVL_UNKNOWN;
|
||
char line[256];
|
||
int lvl = WIFI_LVL_UNKNOWN;
|
||
while (fgets(line, sizeof line, f)) {
|
||
char *p = strstr(line, "wlan0:");
|
||
if (!p) continue;
|
||
p += 6; /* 跳过 "wlan0:" */
|
||
int tok = 0;
|
||
while (*p) {
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (!*p) break;
|
||
char *t = p;
|
||
while (*p && *p != ' ' && *p != '\t' && *p != '\n') p++;
|
||
char save = *p; *p = 0; /* 临时截断 token */
|
||
if (tok == 2) { /* 第 3 个数值(0 起) = level */
|
||
size_t n = strlen(t);
|
||
if (n && t[n - 1] == '.') t[n - 1] = 0; /* 去尾部 '.'(-46. → -46) */
|
||
lvl = atoi(t);
|
||
*p = save;
|
||
break;
|
||
}
|
||
*p = save;
|
||
tok++;
|
||
}
|
||
break; /* 只需 wlan0 行 */
|
||
}
|
||
fclose(f);
|
||
return lvl;
|
||
}
|
||
|
||
|
||
/* 全量重抓(进 MeowOS 时调用一次;阻塞可接受)。绝不放进 10ms 主循环。 */
|
||
void meow_sys_refresh(void)
|
||
{
|
||
load_meowos_version(); /* M2.47:读 /etc/meowos_version(单一数据源,每次刷新对齐) */
|
||
int b = read_file_int("/sys/class/power_supply/battery/capacity");
|
||
g_sys.batt = (b >= 0 && b <= 100) ? b : -1;
|
||
char st[16];
|
||
read_sys_str("/sys/class/power_supply/battery/status", st, sizeof st);
|
||
g_sys.charging = (strstr(st, "Charg") || strstr(st, "Full")) ? 1 : 0;
|
||
g_sys.batt_mv = read_file_int("/sys/class/power_supply/battery/voltage_now") / 1000;
|
||
|
||
/* WiFi:wifi_on=网卡管理态 up(射频开);wifi_connected=operstate up(已关联 AP)。
|
||
* M2.48:二者分工,状态栏才能区分「开未连接(WIFI_NO)」与「已连接(信号分档)」。 */
|
||
char op[16];
|
||
read_sys_str("/sys/class/net/wlan0/operstate", op, sizeof op);
|
||
g_sys.wifi_connected = !strcmp(op, "up") ? 1 : 0;
|
||
g_sys.wifi_on = wlan_radio_up();
|
||
g_sys.wifi_level = g_sys.wifi_connected ? wifi_signal_level() : WIFI_LVL_UNKNOWN;
|
||
g_sys.wifi_ip[0] = 0; g_sys.wifi_ssid[0] = 0;
|
||
if (g_sys.wifi_connected) {
|
||
/* 行形如 "2: wlan0 inet <device_ip>/24 brd ...",取 inet 后第一个斜杠前的 token。
|
||
* 用大缓冲读整行,再从局部 token 拷贝到 16B 小字段(避免 OOB)。 */
|
||
char big[256];
|
||
FILE *p = popen("ip -o addr show wlan0 2>/dev/null | grep 'inet '", "r");
|
||
if (p) {
|
||
if (fgets(big, sizeof big, p)) {
|
||
char *s = strstr(big, "inet ");
|
||
if (s) { s += 5; while (*s == ' ') s++; char *e = strchr(s, '/'); if (e) *e = 0;
|
||
snprintf(g_sys.wifi_ip, sizeof g_sys.wifi_ip, "%s", s); }
|
||
}
|
||
pclose(p);
|
||
}
|
||
/* SSID 尽力读(wpa_cli 若在;失败留空不外溢)。注意:wpa_cli 可能因 wpa_supplicant
|
||
* 未跑而挂起——用后台 + 短等待避免阻塞 UI;超时即放弃(SSID 仅面板用,状态栏不读)。 */
|
||
}
|
||
|
||
/* 蓝牙:hci0 存在性 + 上电状态 + M2.48 角色/连接(进系统全量重探一次) */
|
||
g_sys.bt_seen = access("/sys/class/bluetooth/hci0", F_OK) == 0;
|
||
g_sys.bt_on = g_sys.bt_seen ? hci_powered() : 0;
|
||
bt_role_refresh(1); /* 立即探测角色/连接态,供状态栏与设置页"蓝牙模式"行 */
|
||
|
||
/* 音量(settings;进 MeowOS 全量时把真实 DAC 同步到当前档)/ 亮度 */
|
||
g_sys.vol = settings_volume_get();
|
||
meow_dac_vol_write(settings_volume); /* M2.13:进 MeowOS 即把 CS43198 DAC 置为当前音量 */
|
||
int br = read_brightness();
|
||
g_sys.brightness = (br >= 0) ? br : 0;
|
||
|
||
/* 存储:/data 剩余 + TF 卡挂载容量 */
|
||
g_sys.data_free_kb = 0;
|
||
FILE *df = popen("df -k /data 2>/dev/null | awk 'NR==2{print $4}'", "r");
|
||
if (df) { char x[24]; if (fgets(x, sizeof x, df)) g_sys.data_free_kb = atol(x); pclose(df); }
|
||
g_sys.sd_mounted = sd_mounted();
|
||
g_sys.sd_free_kb = g_sys.sd_total_kb = 0;
|
||
if (g_sys.sd_mounted) {
|
||
FILE *df2 = popen("df -k /usr/data/mnt/sd_0 2>/dev/null | awk 'NR==2{print $4, $2}'", "r");
|
||
if (df2) { char x[48]; if (fgets(x, sizeof x, df2)) { sscanf(x, "%ld %ld", &g_sys.sd_free_kb, &g_sys.sd_total_kb); } pclose(df2); }
|
||
}
|
||
|
||
snprintf(g_sys.ver, sizeof g_sys.ver, "%s", g_meowos_version);
|
||
logmsg("sys snapshot: batt=%d%% chg=%d wifi=%d ssid=%s ip=%s bt=%d/%d vol=%d bri=%d "
|
||
"data_free=%ldKB sd=%d(%ld/%ldKB)",
|
||
g_sys.batt, g_sys.charging, g_sys.wifi_on, g_sys.wifi_ssid[0]?g_sys.wifi_ssid:"-",
|
||
g_sys.wifi_ip[0]?g_sys.wifi_ip:"-", g_sys.bt_on, g_sys.bt_seen,
|
||
g_sys.vol, g_sys.brightness, g_sys.data_free_kb, g_sys.sd_mounted,
|
||
g_sys.sd_free_kb, g_sys.sd_total_kb);
|
||
}
|
||
|
||
|
||
/* 轻刷(1s 定时,只更新时钟/电量/WiFi 信号 + 节流探测蓝牙角色——便宜,绝不碰负载/温度) */
|
||
void meow_sys_light(void)
|
||
{
|
||
int b = read_file_int("/sys/class/power_supply/battery/capacity");
|
||
if (b >= 0 && b <= 100) g_sys.batt = b;
|
||
char op[16];
|
||
read_sys_str("/sys/class/net/wlan0/operstate", op, sizeof op);
|
||
g_sys.wifi_connected = !strcmp(op, "up") ? 1 : 0;
|
||
g_sys.wifi_on = wlan_radio_up();
|
||
g_sys.wifi_level = g_sys.wifi_connected ? wifi_signal_level() : WIFI_LVL_UNKNOWN;
|
||
g_sys.vol = settings_volume_get(); /* 每进 MeowOS 重抓;此处同步内存值 */
|
||
bt_role_refresh(0); /* M2.48:节流探测蓝牙角色(约 5s 一次,不阻塞主循环) */
|
||
}
|
||
|
||
const int pm_cx[PM_NUM] = { PM_CARD_CX - PM_GAPX / 2, PM_CARD_CX + PM_GAPX / 2,
|
||
PM_CARD_CX - PM_GAPX / 2, PM_CARD_CX + PM_GAPX / 2, PM_CARD_CX };
|
||
|
||
|
||
static struct meow_ipc_msg ipc_ring[MEOW_IPC_RING];
|
||
|
||
static volatile uint32_t ipc_head = 0; /* 消费端(主循环)推进 */
|
||
|
||
static volatile uint32_t ipc_tail = 0; /* 生产端(IPC线程)推进 */
|
||
|
||
static void ipc_dispatch(struct meow_ipc_msg *m); /* 定义在下方,主循环排空用 */
|
||
|
||
static int ipc_event_fd = -1;
|
||
|
||
volatile int ipc_shutdown = 0;
|
||
|
||
static volatile int ipc_thread_ok = 0; /* bind+listen 成功 */
|
||
|
||
char ipc_sock_path[MEOW_IPC_SOCK_LEN] = MEOW_IPC_PATH;
|
||
|
||
static pthread_t ipc_pthread;
|
||
|
||
static struct meow_ipc_conn ipc_conns[MEOW_IPC_CONNS];
|
||
|
||
/* §10.5:待处理的 App 前台「展开通知中心」请求(ipc_dispatch 单写者置位;launch_app waitpid
|
||
* 循环 meow_notif_open_take 读取并清除)。仅当 g_app_session_active(App 前台会话)期间才置位,
|
||
* 否则回 ERR(无 App 前台,不展开)。 */
|
||
static m_notif_open_t g_notif_open;
|
||
static volatile int g_notif_open_pending = 0;
|
||
|
||
int meow_notif_open_take(m_notif_open_t *out)
|
||
{
|
||
if (!out) return 0;
|
||
if (!g_notif_open_pending) return 0;
|
||
*out = g_notif_open;
|
||
g_notif_open_pending = 0;
|
||
return 1;
|
||
}
|
||
|
||
/* 会话边界:进入新 App 前台会话前清残留,防旧会话未消费的 OPEN 被新会话误展开。 */
|
||
void meow_notif_open_clear(void)
|
||
{
|
||
g_notif_open_pending = 0;
|
||
memset(&g_notif_open, 0, sizeof g_notif_open);
|
||
}
|
||
|
||
|
||
/* 写一帧(16B 头 + 负载)。返回写入字节;<=0 视为失败。 */
|
||
static size_t ipc_send(int fd, uint16_t type, uint32_t req_id,
|
||
const void *payload, uint32_t len)
|
||
{
|
||
if (fd < 0) return 0;
|
||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||
uint32_t magic = MEOW_IPC_MAGIC;
|
||
uint16_t ver = MEOW_IPC_VERSION;
|
||
memcpy(hdr + 0, &magic, 4);
|
||
memcpy(hdr + 4, &ver, 2);
|
||
memcpy(hdr + 6, &type, 2);
|
||
memcpy(hdr + 8, &req_id, 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) return off;
|
||
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) break;
|
||
p += (size_t)w;
|
||
}
|
||
off += p;
|
||
}
|
||
return off;
|
||
}
|
||
|
||
|
||
/* 公开的 ipc_send 包装(media 模块经 ops 注入用;签名与 meow_media_ops.send 一致)。 */
|
||
size_t meow_ipc_send_pub(int fd, uint16_t type, uint32_t req_id,
|
||
const void *payload, uint32_t len)
|
||
{
|
||
return ipc_send(fd, type, req_id, payload, len);
|
||
}
|
||
|
||
|
||
/* 组装完的帧入队(SPSC 无锁,先写槽再推进 tail,__sync 发布)。 */
|
||
static void ipc_enqueue_frame(struct meow_ipc_conn *c)
|
||
{
|
||
uint16_t type;
|
||
uint32_t req_id, len;
|
||
memcpy(&type, c->hdr + 6, 2);
|
||
memcpy(&req_id, c->hdr + 8, 4);
|
||
len = c->pay_tot;
|
||
if ((ipc_tail - ipc_head) >= MEOW_IPC_RING) {
|
||
logmsg("ipc: ring full, drop frame type=0x%04x", type);
|
||
return;
|
||
}
|
||
uint32_t slot = ipc_tail % MEOW_IPC_RING;
|
||
struct meow_ipc_msg *m = &ipc_ring[slot];
|
||
m->type = type;
|
||
m->req_id = req_id;
|
||
m->fd = c->fd;
|
||
uint32_t copy = len < MEOW_IPC_CARGO ? len : MEOW_IPC_CARGO;
|
||
m->plen = (uint16_t)copy;
|
||
memcpy(m->payload, c->pay, copy);
|
||
__sync_synchronize();
|
||
ipc_tail++;
|
||
if (ipc_event_fd >= 0) { uint64_t one = 1; (void)write(ipc_event_fd, &one, sizeof one); }
|
||
}
|
||
|
||
|
||
/* 读一个连接的可用字节并组装成帧(>=1 完整帧即入队)。返回 0=ok(可能已入队),
|
||
* -1=连接关闭/坏帧(调用方应关闭并回收)。 */
|
||
static int ipc_conn_read(struct meow_ipc_conn *c)
|
||
{
|
||
for (;;) {
|
||
if ((unsigned)c->hdr_n < MEOW_IPC_HDR_LEN) {
|
||
ssize_t r = read(c->fd, c->hdr + c->hdr_n, MEOW_IPC_HDR_LEN - c->hdr_n);
|
||
if (r < 0) { if (errno == EAGAIN) return 0; return -1; }
|
||
if (r == 0) return -1;
|
||
c->hdr_n += (int)r;
|
||
if ((unsigned)c->hdr_n < MEOW_IPC_HDR_LEN) return 0;
|
||
uint32_t magic, len;
|
||
uint16_t ver;
|
||
memcpy(&magic, c->hdr, 4);
|
||
memcpy(&ver, c->hdr + 4, 2);
|
||
memcpy(&len, c->hdr + 12, 4);
|
||
if (magic != MEOW_IPC_MAGIC) { logmsg("ipc: bad magic %08x", magic); return -1; }
|
||
if (ver != MEOW_IPC_VERSION) { logmsg("ipc: bad version %u", ver); return -1; }
|
||
if (len > MEOW_IPC_MAX) { logmsg("ipc: frame len %u too long", len); return -1; }
|
||
c->pay_tot = len;
|
||
c->pay_n = 0;
|
||
if (len == 0) {
|
||
ipc_enqueue_frame(c);
|
||
c->hdr_n = 0;
|
||
continue;
|
||
}
|
||
}
|
||
if (c->pay_n < c->pay_tot) {
|
||
ssize_t r = read(c->fd, c->pay + c->pay_n, c->pay_tot - c->pay_n);
|
||
if (r < 0) { if (errno == EAGAIN) return 0; return -1; }
|
||
if (r == 0) return -1;
|
||
c->pay_n += (uint32_t)r;
|
||
if (c->pay_n < c->pay_tot) return 0;
|
||
}
|
||
ipc_enqueue_frame(c);
|
||
c->hdr_n = 0;
|
||
c->pay_n = 0;
|
||
}
|
||
}
|
||
|
||
|
||
static void *ipc_thread(void *arg)
|
||
{
|
||
(void)arg;
|
||
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||
if (lfd < 0) { logmsg("ipc: socket() fail %s", strerror(errno)); return NULL; }
|
||
unlink(ipc_sock_path);
|
||
struct sockaddr_un a;
|
||
memset(&a, 0, sizeof a);
|
||
a.sun_family = AF_UNIX;
|
||
strncpy(a.sun_path, ipc_sock_path, sizeof a.sun_path - 1);
|
||
if (bind(lfd, (struct sockaddr *)&a, sizeof a) < 0) {
|
||
logmsg("ipc: bind %s fail %s", ipc_sock_path, strerror(errno));
|
||
close(lfd);
|
||
return NULL;
|
||
}
|
||
chmod(ipc_sock_path, 0666);
|
||
if (listen(lfd, MEOW_IPC_BACKLOG) < 0) {
|
||
logmsg("ipc: listen fail %s", strerror(errno));
|
||
close(lfd);
|
||
return NULL;
|
||
}
|
||
fcntl(lfd, F_SETFL, O_NONBLOCK);
|
||
ipc_thread_ok = 1;
|
||
logmsg("ipc: listening on %s (backlog %d)", ipc_sock_path, MEOW_IPC_BACKLOG);
|
||
|
||
struct pollfd fds[MEOW_IPC_CONNS + 1];
|
||
int nfds = 1;
|
||
fds[0].fd = lfd; fds[0].events = POLLIN; fds[0].revents = 0;
|
||
while (!ipc_shutdown) {
|
||
int r = poll(fds, nfds, 400);
|
||
if (r < 0) { if (errno == EINTR) continue; break; }
|
||
if (r == 0) continue;
|
||
if (fds[0].revents & POLLIN) {
|
||
for (;;) {
|
||
int c = accept(lfd, NULL, NULL);
|
||
if (c < 0) break;
|
||
fcntl(c, F_SETFL, O_NONBLOCK);
|
||
if (nfds <= MEOW_IPC_CONNS) {
|
||
ipc_conns[nfds - 1].fd = c;
|
||
ipc_conns[nfds - 1].hdr_n = 0;
|
||
ipc_conns[nfds - 1].pay_n = 0;
|
||
ipc_conns[nfds - 1].pay_tot = 0;
|
||
fds[nfds].fd = c;
|
||
fds[nfds].events = POLLIN;
|
||
fds[nfds].revents = 0;
|
||
nfds++;
|
||
logmsg("ipc: conn %d accept (nfds=%d)", c, nfds - 1);
|
||
} else {
|
||
close(c);
|
||
logmsg("ipc: conn %d rejected (full)", c);
|
||
}
|
||
}
|
||
}
|
||
int i = 1;
|
||
while (i < nfds) {
|
||
int ev = fds[i].revents;
|
||
if (ev & (POLLIN | POLLHUP | POLLERR | POLLNVAL)) {
|
||
struct meow_ipc_conn *cc = &ipc_conns[i - 1];
|
||
if (ipc_conn_read(cc) < 0) {
|
||
logmsg("ipc: conn %d closed", cc->fd);
|
||
close(cc->fd);
|
||
for (int j = i; j < nfds - 1; j++) {
|
||
fds[j] = fds[j + 1];
|
||
ipc_conns[j - 1] = ipc_conns[j];
|
||
}
|
||
nfds--;
|
||
continue;
|
||
}
|
||
}
|
||
i++;
|
||
}
|
||
for (int j = 0; j < nfds; j++) fds[j].revents = 0;
|
||
}
|
||
for (int i = 1; i < nfds; i++) close(fds[i].fd);
|
||
close(lfd);
|
||
unlink(ipc_sock_path);
|
||
logmsg("ipc: thread exit");
|
||
return NULL;
|
||
}
|
||
|
||
|
||
/* 启动 IPC 线程(主循环任意时刻调一次)。返回 0=就绪,-1=线程创建失败。
|
||
* (bind/listen 失败记 ipc_thread_ok=0,随后 meow_ipc_process 仍安全轮询空队列。) */
|
||
int meow_ipc_init(void)
|
||
{
|
||
/* 注入媒体会话 ops:send=ipc_send 包装、log=logmsg(阶段D附录B)。 */
|
||
static meow_media_ops g_media_ops;
|
||
g_media_ops.send = meow_ipc_send_pub;
|
||
g_media_ops.log = logmsg;
|
||
meow_media_init(&g_media_ops);
|
||
|
||
ipc_event_fd = eventfd(0, EFD_NONBLOCK);
|
||
if (ipc_event_fd < 0)
|
||
logmsg("ipc: eventfd fail %s (10ms poll drains anyway)", strerror(errno));
|
||
if (pthread_create(&ipc_pthread, NULL, ipc_thread, NULL) != 0) {
|
||
logmsg("ipc: pthread_create fail");
|
||
return -1;
|
||
}
|
||
usleep(60000); /* 给 bind+listen 一点时间,log 就绪状态 */
|
||
if (ipc_thread_ok) logmsg("ipc: ready (eventfd=%d)", ipc_event_fd);
|
||
else logmsg("ipc: NOT ready (bind/listen failed, thread exiting)");
|
||
return ipc_thread_ok ? 0 : -1;
|
||
}
|
||
|
||
|
||
/* 主循环:非阻塞排空 eventfd + 处理入队帧(单写者改状态并回包)。
|
||
* 事件fd 仅为低延迟优化;若为 -1,10ms 轮询仍保证每圈排空。 */
|
||
void meow_ipc_process(void)
|
||
{
|
||
if (ipc_event_fd >= 0) {
|
||
uint64_t v;
|
||
while (read(ipc_event_fd, &v, sizeof v) == (ssize_t)sizeof v) {}
|
||
}
|
||
while (ipc_head != ipc_tail) {
|
||
struct meow_ipc_msg *m = &ipc_ring[ipc_head % MEOW_IPC_RING];
|
||
ipc_dispatch(m);
|
||
ipc_head++;
|
||
}
|
||
__sync_synchronize();
|
||
}
|
||
|
||
|
||
/* ---- AMSD_DESIGN §6.2 二期:launcher → amsd 转发工具(best-effort)---- */
|
||
|
||
/* hex 编码二进制/字符串到 out(小写 2 字符/字节)。返回编码字节数。 */
|
||
static int ams_hex_encode(const unsigned char *src, int slen, char *out, int outsz)
|
||
{
|
||
static const char *hx = "0123456789abcdef";
|
||
int n = 0;
|
||
for (int i = 0; i < slen && n + 2 < outsz; i++) {
|
||
out[n++] = hx[src[i] >> 4];
|
||
out[n++] = hx[src[i] & 15];
|
||
}
|
||
out[n] = 0;
|
||
return n;
|
||
}
|
||
|
||
/* m_act_declare_t.entries 为 count 个 NUL 结尾 "id=chain" 串的拼接;返回其字节长(含 NUL)。 */
|
||
static int declare_entries_len(const m_act_declare_t *d)
|
||
{
|
||
const char *p = d->entries;
|
||
for (int k = 0; k < d->count && *p; k++) {
|
||
while (*p) p++;
|
||
p++;
|
||
}
|
||
return (int)(p - d->entries);
|
||
}
|
||
|
||
/* 处理一条入队帧:HELLO→ACK、ACT_QUERY→ACT_RESULT/ERR、ACT_DECLARE→ACK、其他→ERR。 */
|
||
static void ipc_dispatch(struct meow_ipc_msg *m)
|
||
{
|
||
switch (m->type) {
|
||
case MSG_HELLO: {
|
||
m_hello_t h;
|
||
memset(&h, 0, sizeof h);
|
||
memcpy(&h, m->payload, m->plen < (uint16_t)sizeof h ? m->plen : (uint16_t)sizeof h);
|
||
h.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
meow_acct_hello(h.app_id, h.version, h.flags);
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = (uint32_t)g_naccts;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: HELLO app=%s ver=%u -> ACK naccts=%d", h.app_id, h.version, g_naccts);
|
||
break;
|
||
}
|
||
case MSG_ACT_DECLARE: {
|
||
m_act_declare_t d;
|
||
memset(&d, 0, sizeof d);
|
||
memcpy(&d, m->payload, m->plen < (uint16_t)sizeof d ? m->plen : (uint16_t)sizeof d);
|
||
d.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
meow_acct_declare(d.app_id, d.entries, d.count);
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = (uint32_t)g_naccts;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: ACT_DECLARE app=%s n=%u -> ACK", d.app_id, d.count);
|
||
/* AMSD_DESIGN §6.2 二期:运行时 declare 转发 amsd(权威表增量;amsd 不在→静默丢)。
|
||
* entries 载荷 hex 编码(含 NUL 分隔),amsd 解码后并集覆盖。 */
|
||
{
|
||
int elen = declare_entries_len(&d);
|
||
if (elen > 0) {
|
||
char hex[2 * MEOW_IPC_CARGO + 8];
|
||
(void)ams_hex_encode((const unsigned char *)d.entries, elen, hex, sizeof hex);
|
||
char fwd[2 * MEOW_IPC_CARGO + 80];
|
||
int fl = snprintf(fwd, sizeof fwd, "DECLARE %s %d %s\n", d.app_id, (int)d.count, hex);
|
||
if (fl > 0) meow_ams_send(fwd, fl);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case MSG_ACT_QUERY: {
|
||
m_act_query_t q;
|
||
memset(&q, 0, sizeof q);
|
||
memcpy(&q, m->payload, m->plen < (uint16_t)sizeof q ? m->plen : (uint16_t)sizeof q);
|
||
q.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
q.activity[MEOW_ACT_MAX - 1] = 0;
|
||
char chain[MEOW_CHAIN_MAX] = "";
|
||
int depth = 0;
|
||
int code = resolve_chain(q.app_id, q.activity, chain, &depth);
|
||
if (code == MEOW_OK) {
|
||
m_act_result_t r;
|
||
memset(&r, 0, sizeof r);
|
||
r.depth = (uint32_t)depth;
|
||
r.code = MEOW_OK;
|
||
size_t cl = strlen(chain);
|
||
if (cl >= MEOW_CHAIN_MAX) cl = MEOW_CHAIN_MAX - 1;
|
||
memcpy(r.chain, chain, cl);
|
||
r.chain_len = (uint16_t)cl;
|
||
ipc_send(m->fd, MSG_ACT_RESULT, m->req_id, &r, sizeof r);
|
||
logmsg("ipc: ACT_QUERY app=%s act=%s -> depth=%d chain=%s",
|
||
q.app_id, q.activity, depth, chain);
|
||
} else {
|
||
m_err_t e;
|
||
memset(&e, 0, sizeof e);
|
||
e.code = (uint32_t)code;
|
||
const char *msg = (code == MEOW_ERR_NOENT) ? "unregistered app/activity"
|
||
: "resolve failed";
|
||
e.msg_len = (uint16_t)strlen(msg);
|
||
memcpy(e.msg, msg, e.msg_len);
|
||
ipc_send(m->fd, MSG_ERR, m->req_id, &e, sizeof e);
|
||
logmsg("ipc: ACT_QUERY app=%s act=%s -> ERR(0x%x)",
|
||
q.app_id, q.activity, code);
|
||
}
|
||
break;
|
||
}
|
||
case MSG_ACT_FOCUS: {
|
||
m_act_focus_t f;
|
||
memset(&f, 0, sizeof f);
|
||
memcpy(&f, m->payload, m->plen < (uint16_t)sizeof f ? m->plen : (uint16_t)sizeof f);
|
||
f.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
f.activity[MEOW_ACT_MAX - 1] = 0;
|
||
meow_fg_act_set(f.app_id, f.activity); /* M2.17:当前顶层 activity(keep_awake 判定) */
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = (uint32_t)g_naccts;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: ACT_FOCUS app=%s act=%s -> ACK", f.app_id, f.activity);
|
||
break;
|
||
}
|
||
case MSG_ACT_BACK: {
|
||
meow_fg_act_back(); /* 回到根/无 activity:清 keep_awake */
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = (uint32_t)g_naccts;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: ACT_BACK -> ACK");
|
||
break;
|
||
}
|
||
/* M3 导航栈(FRAMEWORK_DESIGN §3):前台应用上报导航(REPORT)或请求退出(EXIT)。
|
||
* REPORT → 更新 nav_stack 深度/顶层;EXIT → 置退出请求,前台 waitpid 循环终止应用回主屏。
|
||
* 均在主循环单写者上下文执行(meow_ipc_process 排空处;launch_* 前台循环也调 meow_ipc_process)。 */
|
||
case MSG_NAV: {
|
||
m_nav_t n;
|
||
memset(&n, 0, sizeof n);
|
||
memcpy(&n, m->payload, m->plen < (uint16_t)sizeof n ? m->plen : (uint16_t)sizeof n);
|
||
n.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
n.current[MEOW_ACT_MAX - 1] = 0;
|
||
if (n.nav_type == MEOW_NAV_REPORT)
|
||
meow_nav_report(n.app_id, n.depth, n.current);
|
||
else
|
||
meow_nav_request_exit(); /* MEOW_NAV_EXIT(含未知值防御性按退出处理) */
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = (uint32_t)n.depth;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: NAV app=%s type=%u depth=%u top=\"%s\" -> ACK",
|
||
n.app_id, n.nav_type, n.depth, n.current);
|
||
break;
|
||
}
|
||
/* AMSD_DESIGN §4.3:amsd `am start`/`am kill` 控制请求。主循环单写者仅置位 pending
|
||
* (run_menu 前台循环 / launch_* waitpid 循环消费),不回执启动结果(一期 best-effort)。 */
|
||
case MSG_AMS_START: {
|
||
m_ams_start_t s;
|
||
memset(&s, 0, sizeof s);
|
||
memcpy(&s, m->payload, m->plen < (uint16_t)sizeof s ? m->plen : (uint16_t)sizeof s);
|
||
s.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
s.activity[MEOW_ACT_MAX - 1] = 0;
|
||
s.picker_action[23] = 0;
|
||
s.picker_caller[MEOW_APP_ID_MAX - 1] = 0;
|
||
/* §6.2 二期:Picker 上下文(picker=1 且 action 非空)→ 置位武装位,launch_app_chain 注入 env */
|
||
int rc;
|
||
if (s.picker)
|
||
rc = meow_ams_pending_start_set_ex(s.app_id, s.activity,
|
||
s.picker_action, s.picker_req_id, s.picker_caller);
|
||
else
|
||
rc = meow_ams_pending_start_set(s.app_id, s.activity);
|
||
m_ack_t a;
|
||
a.code = (uint32_t)(rc < 0 ? -rc : MEOW_OK); /* 0=OK;MEOW_ERR_BUSY=有前台 App */
|
||
a.status = 0;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: AMS_START app=%s act=\"%s\" picker=%u req=%u -> ACK code=%u",
|
||
s.app_id, s.activity, s.picker, s.picker_req_id, a.code);
|
||
break;
|
||
}
|
||
case MSG_AMS_KILL: {
|
||
m_ams_kill_t k;
|
||
memset(&k, 0, sizeof k);
|
||
memcpy(&k, m->payload, m->plen < (uint16_t)sizeof k ? m->plen : (uint16_t)sizeof k);
|
||
k.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
m_ack_t a;
|
||
if (g_app_session_active) {
|
||
meow_ams_pending_kill_set(k.app_id);
|
||
a.code = MEOW_OK;
|
||
} else {
|
||
a.code = (uint32_t)MEOW_ERR_NOENT; /* 无前台 App 可终止 */
|
||
}
|
||
a.status = 0;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: AMS_KILL app=%s -> ACK code=%u", k.app_id, a.code);
|
||
break;
|
||
}
|
||
/* AMSD_DESIGN §6.2 二期:picker 跨 App 调用(App A → launcher → amsd 路由)。
|
||
* launcher 仅转发 amsd + ACK 发起方(best-effort;结果经 MSG_PICKER_RESULT 回路记录)。
|
||
* 诚实出入:A 发送后即断开,amsd 结果暂时落记录(picker-status 可查),push 回 A 需
|
||
* 将来 MSG_REGISTER 会话通道(M4)。 */
|
||
case MSG_PICKER_CALL: {
|
||
m_picker_call_t p;
|
||
memset(&p, 0, sizeof p);
|
||
memcpy(&p, m->payload, m->plen < (uint16_t)sizeof p ? m->plen : (uint16_t)sizeof p);
|
||
p.caller[MEOW_APP_ID_MAX - 1] = 0;
|
||
p.action[23] = 0;
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = 0;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
char fwd[96];
|
||
int fl = snprintf(fwd, sizeof fwd, "PICKER_CALL %s %u %s\n", p.caller, p.req_id, p.action);
|
||
if (fl > 0) meow_ams_send(fwd, fl);
|
||
logmsg("ipc: PICKER_CALL caller=%s req=%u act=%s -> ACK", p.caller, p.req_id, p.action);
|
||
break;
|
||
}
|
||
/* AMSD_DESIGN §6.2 二期:picker 结果回传(handler App → launcher → amsd 记录)。
|
||
* value 可能含空格 → hex 编码,amsd 解码。 */
|
||
case MSG_PICKER_RESULT: {
|
||
m_picker_result_t r;
|
||
memset(&r, 0, sizeof r);
|
||
memcpy(&r, m->payload, m->plen < (uint16_t)sizeof r ? m->plen : (uint16_t)sizeof r);
|
||
r.caller[MEOW_APP_ID_MAX - 1] = 0;
|
||
r.action[23] = 0;
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = 0;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
char hex[512 + 8];
|
||
(void)ams_hex_encode((const unsigned char *)r.value, (int)strlen(r.value), hex, sizeof hex);
|
||
char fwd[96 + 512 + 8];
|
||
int fl = snprintf(fwd, sizeof fwd, "PICKER_RESULT %u %s %u %s\n",
|
||
r.req_id, r.action, r.code, hex);
|
||
if (fl > 0) meow_ams_send(fwd, fl);
|
||
logmsg("ipc: PICKER_RESULT req=%u act=%s code=%u value=\"%s\"", r.req_id, r.action, r.code, r.value);
|
||
break;
|
||
}
|
||
/* M2a:通知存储(NOTIFICATION_SYSTEM.md §4/§7/§8)。IPC 线程只入队,此处在主循环
|
||
* 单写者落存储 + 回 ACK(与 HELLO/QUERY 同一线程模型,绝不在 socket IO 阻塞)。 */
|
||
case MSG_NOTIFY: {
|
||
m_notify_t mn;
|
||
memset(&mn, 0, sizeof mn);
|
||
memcpy(&mn, m->payload, m->plen < (uint16_t)sizeof mn ? m->plen : (uint16_t)sizeof mn);
|
||
mn.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
mn.category[23] = 0;
|
||
mn.summary[63] = 0;
|
||
mn.body[95] = 0;
|
||
mn.app_icon[47] = 0;
|
||
mn.target_activity[MEOW_ACT_MAX - 1] = 0;
|
||
/* 映射到存储结构(freedesktop + MeowOS 扩展;actions/hints v1 空) */
|
||
meow_notif_t n;
|
||
memset(&n, 0, sizeof n);
|
||
n.id = mn.replaces_id; /* replaces_id==0 => 新建;>0 => 替换(不存在则新建) */
|
||
snprintf(n.app_id, sizeof n.app_id, "%s", mn.app_id);
|
||
snprintf(n.category, sizeof n.category, "%s", mn.category);
|
||
snprintf(n.summary, sizeof n.summary, "%s", mn.summary);
|
||
snprintf(n.body, sizeof n.body, "%s", mn.body);
|
||
snprintf(n.app_icon, sizeof n.app_icon, "%s", mn.app_icon);
|
||
snprintf(n.target_activity, sizeof n.target_activity, "%s", mn.target_activity);
|
||
n.expire_timeout = mn.expire_timeout;
|
||
n.progress = mn.progress;
|
||
n.urgency = mn.urgency; /* 0-3(低/普通/高/紧急);旧调用=0 → LOW(静默),新调用缺省 NORMAL */
|
||
n.ongoing = mn.ongoing;
|
||
n.media = mn.media;
|
||
uint32_t nid = 0;
|
||
int rc = meow_notif_post(&n, &nid);
|
||
m_ack_t a;
|
||
a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc;
|
||
a.status = nid; /* 返回分配/更新的通知 id */
|
||
ipc_send(m->fd, MSG_NOTIFY_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: NOTIFY app=%s cat=%s id=%u rc=%d -> ACK", mn.app_id, mn.category, nid, rc);
|
||
break;
|
||
}
|
||
case MSG_NOTIFY_DISMISS: {
|
||
uint32_t id = 0;
|
||
if (m->plen >= 4) memcpy(&id, m->payload, 4);
|
||
int rc = meow_notif_dismiss(id);
|
||
m_ack_t a;
|
||
a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc;
|
||
a.status = 0;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: NOTIFY_DISMISS id=%u rc=%d -> ACK", id, rc);
|
||
break;
|
||
}
|
||
case MSG_NOTIFY_PROGRESS: {
|
||
m_notify_progress_t p;
|
||
memset(&p, 0, sizeof p);
|
||
memcpy(&p, m->payload, m->plen < (uint16_t)sizeof p ? m->plen : (uint16_t)sizeof p);
|
||
/* 实时进度:仅改 launcher 内存活跃槽(不落盘);progress>=100 视为结束 → 里程碑落盘 */
|
||
int rc = meow_notif_update_progress(p.nid, p.progress);
|
||
m_ack_t a;
|
||
a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc;
|
||
a.status = 0;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: NOTIFY_PROGRESS nid=%u p=%d rc=%d -> ACK", p.nid, p.progress, rc);
|
||
break;
|
||
}
|
||
/* 查询当前内存活跃通知集(MeowPanel 实时化前置)。序列化在主循环出队后做(单写者),
|
||
* 绝不把通知存储/ring 读到 IPC 线程;应答经 MSG_NOTIFY_QUERY_RESULT 回 JSON 数组文本。 */
|
||
case MSG_NOTIFY_QUERY: {
|
||
char filter[MEOW_APP_ID_MAX] = "";
|
||
if (m->plen > 0) {
|
||
uint32_t cp = m->plen < MEOW_APP_ID_MAX - 1 ? m->plen : MEOW_APP_ID_MAX - 1;
|
||
memcpy(filter, m->payload, cp);
|
||
filter[MEOW_APP_ID_MAX - 1] = 0;
|
||
}
|
||
m_notify_query_result_t r;
|
||
memset(&r, 0, sizeof r);
|
||
int n = meow_notif_serialize_json(filter[0] ? filter : NULL, r.text, sizeof r.text);
|
||
r.code = (n >= 0) ? MEOW_OK : MEOW_ERR_INTERNAL;
|
||
r.text_len = (n >= 0) ? (uint16_t)n : 0;
|
||
ipc_send(m->fd, MSG_NOTIFY_QUERY_RESULT, m->req_id, &r, sizeof r);
|
||
logmsg("ipc: NOTIFY_QUERY app=%s -> %dB", filter[0] ? filter : "*", n);
|
||
break;
|
||
}
|
||
case MSG_NOTIFY_DISMISS_ALL: {
|
||
int rc = meow_notif_clear_active();
|
||
m_ack_t a;
|
||
a.code = (rc >= 0) ? MEOW_OK : (uint32_t)rc;
|
||
a.status = (uint32_t)(rc > 0 ? rc : 0); /* 清除条数 */
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: NOTIFY_DISMISS_ALL -> %d cleared", rc);
|
||
break;
|
||
}
|
||
/* §10.5:App 前台顶部下滑 → 请求展开通知中心叠加会话。仅在 App 前台会话(launch_app
|
||
* waitpid 循环)期间受理(置 pending 让该循环消费后展开叠加);否则回 ACK(status=BUSY)
|
||
* 让客户端知情但不展开(主循环/无 App 前台不展开)。App 不强依赖本 ACK(best-effort)。 */
|
||
case MSG_NOTIF_OPEN: {
|
||
m_notif_open_t o;
|
||
memset(&o, 0, sizeof o);
|
||
memcpy(&o, m->payload, m->plen < (uint16_t)sizeof o ? m->plen : (uint16_t)sizeof o);
|
||
o.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = 0;
|
||
if (g_app_session_active && !g_notif_open_pending) {
|
||
g_notif_open = o; /* 单写者置位;waitpid 循环 take 后展开 */
|
||
g_notif_open_pending = 1;
|
||
a.status = 1; /* 已受理入队 */
|
||
logmsg("ipc: NOTIF_OPEN app=%s oy=%u hz=%u -> queued", o.app_id, o.origin_y, o.hotzone);
|
||
} else {
|
||
a.status = 0xFFFFFFFFu; /* 无 App 前台 / 已有会话在跑:不展开 */
|
||
logmsg("ipc: NOTIF_OPEN app=%s oy=%u hz=%u -> ignored (no app session or busy)",
|
||
o.app_id, o.origin_y, o.hotzone);
|
||
}
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
break;
|
||
}
|
||
/* §10.5:App 告知结束/退出 → launcher 收尾兜底(清 pending/标记;非可靠链路,launcher 主要
|
||
* 靠 waitpid/kill(pid,0) 自感知)。best-effort,回 ACK。 */
|
||
case MSG_NOTIF_CLOSE: {
|
||
m_notif_close_t o;
|
||
memset(&o, 0, sizeof o);
|
||
memcpy(&o, m->payload, m->plen < (uint16_t)sizeof o ? m->plen : (uint16_t)sizeof o);
|
||
o.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
meow_notif_open_clear();
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = (uint32_t)o.reason;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: NOTIF_CLOSE app=%s reason=%u -> ACK", o.app_id, o.reason);
|
||
break;
|
||
}
|
||
case MSG_NOTIFY_LIST:
|
||
/* ↓ 守护→App 下发通知清单(重同步)。本帧不来自客户端(无请求类型),故不在此应答;
|
||
* 需用时由守护主动经 meow_notif_serialize_list() + ipc_send 下发(M5 通知中心接入)。 */
|
||
logmsg("ipc: NOTIFY_LIST (downlink) not dispatched here");
|
||
break;
|
||
/* 阶段D附录B:媒体会话(FRAMEWORK_DESIGN 附录 B)。META 声明 MF_PLAYBACK 的 App 上报
|
||
* 元数据/进度/状态 → 归并到 meow_media 会话(最近活跃=当前源);回 ACK。 */
|
||
case MSG_META: {
|
||
m_meta_t mm;
|
||
memset(&mm, 0, sizeof mm);
|
||
memcpy(&mm, m->payload, m->plen < (uint16_t)sizeof mm ? m->plen : (uint16_t)sizeof mm);
|
||
mm.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
mm.title[127] = 0; mm.artist[127] = 0; mm.album[127] = 0;
|
||
mm.artwork[191] = 0; mm.lyric[191] = 0;
|
||
int rc = meow_media_meta(&mm, m->fd);
|
||
m_ack_t a;
|
||
a.code = MEOW_OK; /* 会话策略:槽满降级最旧,不视为错误 */
|
||
a.status = (uint32_t)rc;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: META app=%s state=%u pos=%u/%u rc=%d -> ACK",
|
||
mm.app_id, mm.state, mm.pos_ms, mm.dur_ms, rc);
|
||
break;
|
||
}
|
||
/* MSG_PLAYCTL(↑ 控制器→launcher 转发):把 cmd/seek 下发到「最近活跃媒体会话」fd。
|
||
* 物理媒体键 / 宿主验证经此外发;通知中心卡片/播放页按钮走 meow_media_playctl 直发。 */
|
||
case MSG_PLAYCTL: {
|
||
m_playctl_t c;
|
||
memset(&c, 0, sizeof c);
|
||
memcpy(&c, m->payload, m->plen < (uint16_t)sizeof c ? m->plen : (uint16_t)sizeof c);
|
||
int rc = meow_media_playctl(c.cmd, c.seek_ms);
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = (uint32_t)rc; /* 0=已下发;-1=无源/发送失败(status=0xFFFFFFFF) */
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: PLAYCTL cmd=%u seek=%lld rc=%d -> ACK", c.cmd,
|
||
(long long)c.seek_ms, rc);
|
||
break;
|
||
}
|
||
default:
|
||
logmsg("ipc: unsupported frame type 0x%04x (req %u)", m->type, m->req_id);
|
||
{
|
||
m_err_t e;
|
||
memset(&e, 0, sizeof e);
|
||
e.code = MEOW_ERR_INVAL;
|
||
char msg[40];
|
||
snprintf(msg, sizeof msg, "unsupported type 0x%04x", m->type);
|
||
e.msg_len = (uint16_t)strlen(msg);
|
||
memcpy(e.msg, msg, e.msg_len);
|
||
ipc_send(m->fd, MSG_ERR, m->req_id, &e, sizeof e);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|