/* mp_server.c — HTTP 服务器/路由 + 静态托管 + main(单线程 + poll 串行)。 */ #include "mp.h" static char g_webroot[512]; static int g_port = DEFAULT_PORT; void req_free(req_t *r) { free(r->body); r->body = NULL; r->body_len = 0; } int read_request(int sock, req_t *r) { char *buf = malloc(MAX_HTTP_HEADER); if (!buf) return -1; size_t hn = 0; int deadline_ms = 5000; ptrdiff_t hend = -1; for (;;) { struct pollfd pf = { sock, POLLIN, 0 }; int pr = poll(&pf, 1, deadline_ms); if (pr <= 0) { free(buf); return -1; } ssize_t rd = read(sock, buf + hn, MAX_HTTP_HEADER - 1 - hn); if (rd <= 0) { free(buf); return -1; } hn += (size_t)rd; buf[hn] = 0; hend = find_header_end(buf, hn); if (hend >= 0) break; if (hn >= MAX_HTTP_HEADER - 2) { free(buf); return -1; } } /* 头结束在 hend,body 起点在 hend+4 */ char *hdr = buf; char *line_end = strstr(hdr, "\r\n"); if (!line_end) { free(buf); return -1; } *line_end = 0; char method[8], pathraw[512], proto[16]; if (sscanf(hdr, "%7s %511s %15s", method, pathraw, proto) < 2) { free(buf); return -1; } snprintf(r->method, sizeof r->method, "%s", method); split_path_query(pathraw, r->path, sizeof r->path, r->query, sizeof r->query); size_t clen = 0; char *hp = line_end + 2; while (hp < buf + hend && strncmp(hp, "\r\n", 2)) { char *he = strstr(hp, "\r\n"); if (!he || he > buf + hend) break; *he = 0; if (!strncasecmp(hp, "Content-Length:", 15)) clen = (size_t)strtoul(hp + 15, NULL, 10); else if (!strncasecmp(hp, "Content-Type:", 13)) { char *v = hp + 13; while (*v == ' ') v++; snprintf(r->content_type, sizeof r->content_type, "%s", v); } else if (!strncasecmp(hp, "Authorization:", 14)) { char *v = hp + 14; while (*v == ' ') v++; snprintf(r->authorization, sizeof r->authorization, "%s", v); } else if (!strncasecmp(hp, "X-MeowToken:", 12)) { char *v = hp + 12; while (*v == ' ') v++; snprintf(r->x_meow_token, sizeof r->x_meow_token, "%s", v); } else if (!strncasecmp(hp, "Cookie:", 7)) { char *v = hp + 7; while (*v == ' ') v++; snprintf(r->cookie, sizeof r->cookie, "%s", v); } hp = he + 2; } char *body_start = buf + hend + 4; /* body 实际起点 */ size_t buffered = (hn > (size_t)(body_start - buf)) ? (hn - (size_t)(body_start - buf)) : 0; /* 上传(multipart)走更严上限:14MB 内存约束下不整读超 MAX_UPLOAD_BODY。超限 → -2(413)。 * 字体库 tar.gz 端点例外:gbk/full 字库 png+idx 可能 10-20MB,放宽到 MAX_FONT_UPLOAD(32MB)。 */ size_t cap = MAX_HTTP_BODY; if (strncasecmp(r->content_type, "multipart/form-data", 19) == 0) cap = (!strcmp(r->path, "/api/fonts/import")) ? MAX_FONT_UPLOAD : MAX_UPLOAD_BODY; if (clen > cap) { free(buf); return -2; } if (clen == 0) { free(buf); r->body = NULL; r->body_len = 0; return 0; } char *body = malloc(clen + 1); if (!body) { free(buf); return -1; } size_t got = 0; if (buffered > 0) { size_t take = buffered < clen ? buffered : clen; memcpy(body, body_start, take); got = take; } free(buf); while (got < clen) { struct pollfd pf = { sock, POLLIN, 0 }; int pr = poll(&pf, 1, 5000); if (pr <= 0) { free(body); return -1; } ssize_t rd = read(sock, body + got, clen - got); if (rd <= 0) { free(body); return -1; } got += (size_t)rd; } body[clen] = 0; r->body = body; r->body_len = clen; return 0; } int write_full(int sock, const void *b, size_t n) { const char *p = (const char *)b; size_t off = 0; while (off < n) { ssize_t w = write(sock, p + off, n - off); if (w <= 0) return -1; off += (size_t)w; } return 0; } int send_head(int sock, int status, const char *ctype, long long clen, const char *extra) { char h[512]; const char *sts = "OK"; switch (status) { case 200: sts = "OK"; break; case 201: sts = "Created"; break; case 202: sts = "Accepted"; break; case 400: sts = "Bad Request"; break; case 401: sts = "Unauthorized"; break; case 403: sts = "Forbidden"; break; case 404: sts = "Not Found"; break; case 405: sts = "Method Not Allowed"; break; case 409: sts = "Conflict"; break; case 413: sts = "Payload Too Large"; break; case 415: sts = "Unsupported Media Type"; break; case 422: sts = "Unprocessable Entity"; break; case 500: sts = "Internal Server Error"; break; case 501: sts = "Not Implemented"; break; case 503: sts = "Service Unavailable"; break; default: sts = "OK"; break; } snprintf(h, sizeof h, "HTTP/1.1 %d %s\r\nServer: MeowPanel/%s\r\nContent-Type: %s\r\nContent-Length: %lld\r\nConnection: close\r\n%s%s\r\n", status, sts, PANEL_VERSION, ctype, clen, extra ? extra : "", extra ? "\r\n" : ""); return write_full(sock, h, strlen(h)); } int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen) { if (send_head(sock, status, ctype, (long long)bodylen, NULL) < 0) return -1; if (bodylen && write_full(sock, body, bodylen) < 0) return -1; return 0; } int send_ok(int sock, const char *data) { static const char prefix[] = "{\"ok\":true,\"data\":"; size_t pl = sizeof prefix - 1; /* 18 */ size_t dl = strlen(data); size_t tot = pl + dl + 1; char *b = malloc(tot + 1); if (!b) { const char *e = "{\"ok\":false,\"error\":{\"code\":\"INTERNAL\",\"message\":\"内存不足\"}}"; return send_raw(sock, 500, "application/json; charset=utf-8", e, strlen(e)); } memcpy(b, prefix, pl); memcpy(b + pl, data, dl); b[pl + dl] = '}'; b[pl + dl + 1] = 0; int rc = send_raw(sock, 200, "application/json; charset=utf-8", b, tot); free(b); return rc; } int send_err(int sock, int status, const char *code, const char *msg) { sbuf_t s = {0}; sA(&s, "{\"ok\":false,\"error\":{\"code\":"); sA_str(&s, code ? code : "INTERNAL"); sA(&s, ",\"message\":"); sA_str(&s, msg ? msg : ""); sA(&s, "}}"); int rc = send_raw(sock, status, "application/json; charset=utf-8", s.b, s.n); sbuf_free(&s); return rc; } int send_json(int sock, int status, const char *data) { if (status == 200) return send_ok(sock, data); return send_raw(sock, status, "application/json; charset=utf-8", data, strlen(data)); } int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename) { int fd = open(abspath, O_RDONLY); if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "文件不存在"); struct stat st; if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); } char extra[256]; if (filename) snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", filename); else extra[0] = 0; if (send_head(sock, status, ctype, (long long)st.st_size, extra[0] ? extra : NULL) < 0) { close(fd); return -1; } char buf[8192]; ssize_t n; while ((n = read(fd, buf, sizeof buf)) > 0) { if (write_full(sock, buf, (size_t)n) < 0) { close(fd); return -1; } } close(fd); return 0; } const char *web_root(void) { if (g_webroot[0]) return g_webroot; char exe[512]; ssize_t n = readlink("/proc/self/exe", exe, sizeof exe - 1); if (n <= 0) { snprintf(g_webroot, sizeof g_webroot, "."); return g_webroot; } exe[n] = 0; char *slash = strrchr(exe, '/'); if (slash) *slash = 0; snprintf(g_webroot, sizeof g_webroot, "%s", exe[0] ? exe : "."); return g_webroot; } const char *mime_for(const char *path) { const char *ext = strrchr(path, '.'); if (!ext) return "application/octet-stream"; if (!strcasecmp(ext, ".html")) return "text/html; charset=utf-8"; if (!strcasecmp(ext, ".css")) return "text/css; charset=utf-8"; if (!strcasecmp(ext, ".js")) return "application/javascript; charset=utf-8"; if (!strcasecmp(ext, ".json")) return "application/json; charset=utf-8"; if (!strcasecmp(ext, ".png")) return "image/png"; if (!strcasecmp(ext, ".ico")) return "image/x-icon"; if (!strcasecmp(ext, ".svg")) return "image/svg+xml"; return "application/octet-stream"; } int serve_static(int sock, const char *path) { const char *root = web_root(); char rel[512]; /* / → index.html;/assets/ → ;/ → (meowpanel.css/js 等) */ if (!strcmp(path, "/")) snprintf(rel, sizeof rel, "index.html"); else if (!strncmp(path, "/assets/", 8)) snprintf(rel, sizeof rel, "%s", path + 8); else snprintf(rel, sizeof rel, "%s", path + 1); if (strstr(rel, "..") || rel[0] == '/') return send_err(sock, 403, "FORBIDDEN", "路径逃逸"); /* 只允许安全文件(防读到二进制/任意文件) */ char full[1024]; snprintf(full, sizeof full, "%s/%s", root, rel); /* 敏感名拒绝 */ const char *base = strrchr(full, '/'); base = base ? base + 1 : full; if (!strcmp(base, "meowpanel") || !strcmp(base, "meowpanel_host") || !strcmp(base, "meowpanel.session") ) return send_err(sock, 403, "FORBIDDEN", "禁止访问可执行/会话文件"); FILE *f = fopen(full, "rb"); if (!f) return send_err(sock, 404, "NOT_FOUND", "资源不存在"); fclose(f); return send_file(sock, 200, mime_for(full), full, NULL); } int handle(int sock, req_t *r) { const char *p = r->path; if (strncmp(p, "/api/", 5) != 0) { if (strcmp(r->method, "GET") != 0 && strcmp(r->method, "HEAD") != 0) return send_err(sock, 405, "METHOD_NOT_ALLOWED", "静态资源仅支持 GET"); return serve_static(sock, p); } /* ---- 可选鉴权门禁(启用后除 /api/auth 系列外全 /api/ 端点需会话) ---- */ if (require_auth(sock, r) != 1) return 0; /* ---- 鉴权端点(登录/设置/登出/状态:公开——访问面板即需登录,须先经此建立会话) ---- */ if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/login")) return api_auth_login(sock, r); if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/setup")) return api_auth_setup(sock, r); if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/logout")) return api_auth_logout(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/auth/status")) return api_auth_status(sock, r); /* ---- 性能/状态 ---- */ if (!strcmp(r->method, "GET") && (!strcmp(p, "/api/status") || !strcmp(p, "/api/perf"))) return api_perf(sock, p); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/system")) return api_system(sock); /* ---- 字体库(导入/导出/列表) ---- */ if (!strcmp(r->method, "POST") && !strcmp(p, "/api/fonts/import")) return api_fonts_import(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts")) return api_fonts_list(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts/export")) return api_fonts_export(sock, r); /* ---- 音频状态(meow_audiod 快照,只读;含 rms/peak/lvlseq 电平键;monitor=实时监控日志尾读) ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio")) return api_audio(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio/monitor")) return api_audio_monitor(sock, r->query); /* ---- 批1 只读增强:功耗/进程服务/首页聚合(均走 PIN 鉴权读级) ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power")) return api_power(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/policy")) return api_power_policy_get(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/history")) return api_power_history(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/events")) return api_power_events(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/ams")) return api_ams(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/ams/bg")) return api_ams_bg(sock); /* 批④ 后台活动列表(只读) */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/svcd")) return api_svcd(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/overview")) return api_overview(sock); if (!strcmp(r->method, "GET")) { char seg[256]; if (path_segment("/api/ams/", p, seg, sizeof seg) && seg[0]) return api_ams_history(sock, seg); /* GET /api/ams/:pkg → am history(崩溃根因) */ } /* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/bt")) return api_net_bt(sock); /* F1-③ 蓝牙只读详情(0xE6 读级聚合) */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/log")) return api_net_log(sock); /* ---- 实时画面(阶段 C) ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/screen.jpeg")) return api_screen_jpeg(sock, r); /* ---- 系统动作 ---- */ if (!strcmp(r->method, "POST")) { if (!strcmp(p, "/api/system/reboot")) return api_system_action(sock, "reboot", r); if (!strcmp(p, "/api/system/shutdown")) return api_system_action(sock, "shutdown", r); if (!strcmp(p, "/api/system/ota")) return api_system_action(sock, "ota", r); if (!strcmp(p, "/api/system/pin-unlock-once")) return api_pin_unlock_once(sock); if (!strcmp(p, "/api/debug/report_bad_json")) { /* 前端 NOT_JSON 自动回传浏览器收到的原始字节(抓间歇性破包) */ FILE *df = fopen("/tmp/overview_client_bad.txt", "w"); if (df && r->body && r->body_len) fwrite(r->body, 1, r->body_len, df); if (df) fclose(df); panel_log("debug: client bad json captured %zu bytes -> /tmp/overview_client_bad.txt", (size_t)r->body_len); return send_json(sock, 200, "{\"captured\":true}"); } if (!strcmp(p, "/api/settings")) return api_settings_batch(sock, r); if (!strcmp(p, "/api/settings/restore")) return api_settings_restore(sock); if (!strcmp(p, "/api/theme")) return api_theme_put(sock, r); if (!strcmp(p, "/api/files/upload")) return api_files_upload(sock, r); if (!strcmp(p, "/api/files/mkdir")) return api_files_mkdir(sock, r); if (!strcmp(p, "/api/files/rename")) return api_files_rename(sock, r); if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r); if (!strcmp(p, "/api/notifications/dismiss_all") || !strcmp(p, "/api/notifications/clear")) return api_notification_dismiss_all(sock); if (!strcmp(p, "/api/net/dns/flush")) return api_net_dns_flush(sock); if (!strcmp(p, "/api/net/policy")) return api_net_policy_set(sock, r->body, r->body_len); if (!strcmp(p, "/api/power/policy")) return api_power_policy_set(sock, r->body, r->body_len); if (!strcmp(p, "/api/audio/route")) return api_audio_route(sock, r->body, r->body_len); if (!strcmp(p, "/api/audio/volume")) return api_audio_volume(sock, r->body, r->body_len); if (!strcmp(p, "/api/key")) return api_key_batch(sock, r); if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r); if (!strcmp(p, "/api/input/gesture")) return api_input_gesture(sock, r); char seg[256]; if (path_segment("/api/key/", p, seg, sizeof seg)) return api_key_single(sock, atoi(seg), r); /* POST /api/ams//start|kill|killfg(管启/停;require_auth 已护,kill 前台前端二次确认; * killfg=前台救命强制终止:am kill 优先,launcher 拒→直接 pid 信号,前端二次确认+标注前台) */ if (!strncmp(p, "/api/ams/", 9)) { size_t pl = strlen(p); if (pl > 5 && !strcmp(p + pl - 6, "/start")) { char pkg[256]; if (path_segment("/api/ams/", p, pkg, sizeof pkg)) return api_ams_ctl(sock, pkg, 0); } else if (pl > 7 && !strcmp(p + pl - 7, "/killfg")) { char pkg[256]; if (path_segment("/api/ams/", p, pkg, sizeof pkg)) return api_ams_kill_fg(sock, pkg); } else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) { char pkg[256]; if (path_segment("/api/ams/", p, pkg, sizeof pkg)) return api_ams_ctl(sock, pkg, 1); } } } /* ---- 设置 ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/settings")) return api_settings_get_all(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/theme")) return api_theme_get(sock); if (!strcmp(r->method, "PUT")) { char seg[256]; if (path_segment("/api/settings/", p, seg, sizeof seg)) return api_settings_put(sock, seg, r); } if (!strcmp(r->method, "GET")) { char seg[256]; if (path_segment("/api/settings/", p, seg, sizeof seg)) return api_settings_get_any(sock, seg); } /* ---- 文件 ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/search")) return api_files_search(sock, r); /* F1-④ 搜索(只读) */ if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r); /* ---- 应用 ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/apps")) return api_apps_list(sock); if (!strcmp(r->method, "GET")) { char seg[256]; if (path_segment("/api/apps/", p, seg, sizeof seg)) { /* :pkg/export 结尾(path_segment 已把 seg 截为 pkg,这里检查原 path 尾缀) */ size_t pl = strlen(p); int is_export = (pl >= 7 && !strcmp(p + pl - 7, "/export")); if (is_export) return api_apps_export(sock, seg); return api_apps_info(sock, seg); } } if (!strcmp(r->method, "DELETE")) { char seg[256]; if (path_segment("/api/apps/", p, seg, sizeof seg)) return api_apps_remove(sock, seg, r); } /* ---- 日志 ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs")) return api_logs_list(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs/crash")) return api_logs_crash(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs/read")) return api_logs_read(sock, r); /* ---- 通知 ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/notifications")) return api_notifications(sock); if (!strcmp(r->method, "DELETE")) { char seg[256]; if (path_segment("/api/notifications/", p, seg, sizeof seg)) { unsigned nid = (unsigned)strtoul(seg, NULL, 10); return api_notification_delete(sock, nid); } } return send_err(sock, 404, "NOT_FOUND", "端点不存在"); } int read_port_config(void) { /* 配置文件 meowpanel.conf(web 根目录),格式 key=value,取 port */ char path[1024]; snprintf(path, sizeof path, "%s/meowpanel.conf", web_root()); FILE *f = fopen(path, "r"); if (f) { char line[160]; while (fgets(line, sizeof line, f)) { char *eq = strchr(line, '='); if (!eq) continue; char *k = line; while (*k == ' ' || *k == '\t') k++; if (!strncmp(k, "port", 4) && k[4] == '=') g_port = atoi(eq + 1); } fclose(f); } const char *env = getenv("MEOWPORT"); if (env && env[0]) g_port = atoi(env); if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT; return g_port; } void handle_conn(int sock) { req_t r; memset(&r, 0, sizeof r); int rr = read_request(sock, &r); if (rr != 0) { if (rr == -2) send_err(sock, 413, "PAYLOAD_TOO_LARGE", "请求体过大(超过上传上限)"); close(sock); return; } handle(sock, &r); req_free(&r); close(sock); } int panel_selftest(void) { const char *trunc = "流媒体客户\xE7"; /* 事故里被腰斩的显示名(16B,末字节为「端」首字节) */ const char *oknat = "流媒体客户端"; /* 完整合法名(18B,回归确认不误伤) */ size_t n = strlen(trunc); /* 1a) utf8_sanitize:截断串必被改写,长度保持,产物合法 UTF-8 */ char san[64]; if (!utf8_sanitize(san, trunc, n)) { printf("SELFTEST FAIL: utf8_sanitize 未改写截断串\n"); return 1; } if (strlen(san) != n) { printf("SELFTEST FAIL: utf8_sanitize 长度变化 %zu->%zu\n", n, strlen(san)); return 1; } if (!utf8_valid_n(san, strlen(san))) { printf("SELFTEST FAIL: utf8_sanitize 产物含非法字节\n"); return 1; } /* 1b) 完整合法名不得被改写(误伤检测) */ char san2[64]; if (utf8_sanitize(san2, oknat, strlen(oknat)) != 0 || strcmp(san2, oknat) != 0) { printf("SELFTEST FAIL: utf8_sanitize 误改合法 UTF-8\n"); return 1; } /* 2) sA_str 嵌入:产物合法 UTF-8 且 jv_parse 完整解析 */ sbuf_t s = {0}; sA(&s, "{\"pkg\":"); sA_str(&s, "com.example.trunc"); sA(&s, ",\"name\":"); sA_str(&s, trunc); sA(&s, ",\"state\":"); sA_str(&s, "running"); sA(&s, "}"); if (!s.b) { printf("SELFTEST FAIL: sA_str 输出空\n"); return 1; } if (!utf8_valid_n(s.b, s.n)) { printf("SELFTEST FAIL: sA_str 产物含非法字节\n"); sbuf_free(&s); return 1; } jv_t *r = jv_parse(s.b, (int)s.n); if (!r) { printf("SELFTEST FAIL: sA_str 产物 JSON 无法解析\n"); sbuf_free(&s); return 1; } jv_t *nm = jv_obj_get(r, "name"); if (!nm || nm->type != J_STR) { printf("SELFTEST FAIL: sA_str 产物缺 name\n"); jv_free(r); sbuf_free(&s); return 1; } jv_free(r); sbuf_free(&s); /* 3) ams_fill 桩:模拟 APP 行 parse 出的截断名(join_tokens 后)→ apps 数组可解析 */ sbuf_t a = {0}; sA(&a, "{\"available\":true,\"count\":1,\"apps\":[{\"pkg\":"); sA_str(&a, "com.example.trunc"); sA(&a, ",\"name\":"); sA_str(&a, trunc); sA(&a, ",\"state\":"); sA_str(&a, "running"); sA(&a, ",\"pid\":123}]}"); if (!a.b || !utf8_valid_n(a.b, a.n)) { printf("SELFTEST FAIL: ams_fill 桩产物含非法字节/为空\n"); sbuf_free(&a); return 1; } jv_t *ar = jv_parse(a.b, (int)a.n); if (!ar) { printf("SELFTEST FAIL: ams_fill 桩产物 JSON 无法解析\n"); sbuf_free(&a); return 1; } jv_free(ar); sbuf_free(&a); /* 4) net_policy_parse:喂含坏字节域名(截断「段」字)+ allow/deny 混合 → 产物合法 JSON、无非法字节。 * 域名/实体名都走 sA_str_n 消毒(坏字节→'?'),故解析器产物必为合法 JSON。 */ { static const char ptext[] = "# 策略(NETMGR_DESIGN §6)log_level=debug enforce=dns_only(SOCK_DESTROY 在本内核为 no-op)\n" "app:com.example.trunc net=block domain=+good.example,-bad\xE7\n" "svc:meowpanel net=allow domain=+svc.local\n"; sbuf_t p = {0}; net_policy_parse(ptext, &p); if (!p.b || !utf8_valid_n(p.b, p.n)) { printf("SELFTEST FAIL: net_policy_parse 产物含非法字节/为空\n"); sbuf_free(&p); return 1; } jv_t *pr = jv_parse(p.b, (int)p.n); if (!pr) { printf("SELFTEST FAIL: net_policy_parse 产物 JSON 无法解析\n"); sbuf_free(&p); return 1; } jv_t *rules = jv_obj_get(pr, "rules"); if (!rules || rules->type != J_ARR || !rules->child) { printf("SELFTEST FAIL: net_policy_parse 缺 rules 数组\n"); jv_free(pr); sbuf_free(&p); return 1; } jv_t *enf = jv_obj_get(pr, "enforce"); if (!enf || enf->type != J_STR || !enf->str || strcmp(enf->str, "dns_only") != 0) { printf("SELFTEST FAIL: net_policy_parse enforce 未如实透传 dns_only\n"); jv_free(pr); sbuf_free(&p); return 1; } jv_t *pwe = jv_obj_get(pr, "policy_write_enabled"); /* guard③ 状态键必须为布尔,且拼读无括号错位 */ if (!pwe || pwe->type != J_BOOL) { printf("SELFTEST FAIL: net_policy_parse 缺/损坏 policy_write_enabled\n"); jv_free(pr); sbuf_free(&p); return 1; } jv_free(pr); sbuf_free(&p); } /* 5) ams_history_parse:喂 EVT 行 + 截断崩溃 detail(含坏字节)→ available:false(降级,绝不发非法 JSON)。 * detail 合法时 → 合法 JSON 可解析。 */ { char bad[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc 崩溃在函数 foo\xE7\n"; sbuf_t h = {0}; ams_history_parse(bad, &h, "com.example.trunc"); if (!h.b || strstr(h.b, "available\":false") == NULL) { printf("SELFTEST FAIL: ams_history_parse 截断 detail 未降级 available:false\n"); sbuf_free(&h); return 1; } sbuf_free(&h); char good[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc SIGSEGV at addr 0x10\nEVT 1786523500 124 ams_start com.example.trunc ok forwarded\n"; sbuf_t h2 = {0}; ams_history_parse(good, &h2, "com.example.trunc"); if (!h2.b || !utf8_valid_n(h2.b, h2.n) || !jv_parse(h2.b, (int)h2.n)) { printf("SELFTEST FAIL: ams_history_parse 合法 detail 无法解析/含非法字节\n"); sbuf_free(&h2); return 1; } sbuf_free(&h2); } /* 6) ams_bg_parse:喂含 bg/fg 混合 state 的 am list 文本 → 仅 bg 条目、合法 JSON, * cpu_pct/rss_kb 为 null(读不到/首采样)或数字——绝不编造不一致字段。 */ { char list[] = "# amd register+state\n" "APP os.meow.a 名A bg 1 1 0 clean 0\n" "APP os.meow.b 名B fg 9999992 1 0 clean 0\n" "APP os.meow.c 名C bg 9999991 2 1 exit 1\n"; sbuf_t b = {0}; ams_bg_parse(list, &b); if (!b.b || !utf8_valid_n(b.b, b.n)) { printf("SELFTEST FAIL: ams_bg_parse 产物非法字节/为空\n"); sbuf_free(&b); return 1; } jv_t *br = jv_parse(b.b, (int)b.n); if (!br) { printf("SELFTEST FAIL: ams_bg_parse 产物 JSON 无法解析\n"); sbuf_free(&b); return 1; } jv_t *bapps = jv_obj_get(br, "apps"); if (!bapps || bapps->type != J_ARR) { printf("SELFTEST FAIL: ams_bg_parse 缺 apps 数组\n"); jv_free(br); sbuf_free(&b); return 1; } int bcnt = 0; for (jv_t *e = bapps->child; e; e = e->next) { bcnt++; jv_t *st = jv_obj_get(e, "state"); if (!st || st->type != J_STR || strcmp(st->str, "bg") != 0) { printf("SELFTEST FAIL: ams_bg_parse 条目 state 非 bg\n"); jv_free(br); sbuf_free(&b); return 1; } jv_t *cp = jv_obj_get(e, "cpu_pct"); jv_t *rs = jv_obj_get(e, "rss_kb"); if (!cp || (cp->type != J_NULL && cp->type != J_NUM)) { printf("SELFTEST FAIL: ams_bg_parse cpu_pct 非 null/数字\n"); jv_free(br); sbuf_free(&b); return 1; } if (!rs || (rs->type != J_NULL && rs->type != J_NUM)) { printf("SELFTEST FAIL: ams_bg_parse rss_kb 非 null/数字\n"); jv_free(br); sbuf_free(&b); return 1; } } if (bcnt != 2) { printf("SELFTEST FAIL: ams_bg_parse bg 条目应 2 个,实际 %d\n", bcnt); jv_free(br); sbuf_free(&b); return 1; } jv_free(br); sbuf_free(&b); } /* 7) net_locked_entity:防自锁实体精确匹配(svc:/proc:/裸名);非关键实体放行(guard①)。 */ { struct { const char *e; int want; } c[] = { { "svc:dropbear", 1 }, { "proc:netmgrd", 1 }, { "udhcpc", 1 }, { "svc:sysui", 1 }, { "svc:wpa_supplicant", 1 }, { "svcd", 1 }, { "init", 1 }, { "app:com.example", 0 }, { "svc:avahi", 0 }, { "proc:sh", 0 }, { "app:com.netmgrd.helper", 0 }, /* 含子串但不等于裸名,不得误判 */ { "", 0 }, { NULL, 0 } }; for (int i = 0; c[i].e; i++) { int got = net_locked_entity(c[i].e); if (got != c[i].want) { printf("SELFTEST FAIL: net_locked_entity(%s)=%d 期望 %d\n", c[i].e, got, c[i].want); return 1; } } } /* 8) policy 默认关 gate(guard③):无设置文件 → panel_policy_write_enabled()==0(默认关); * settings_write_one("panel.policy_enabled","1") 后 ==1(写临时 settings 文件验证,测完清理)。 */ { const char *saved = g_settings_file; char tmpf[] = "/tmp/meowpanel_policy_gate_XXXXXX"; int tfd = mkstemp(tmpf); if (tfd < 0) { printf("SELFTEST FAIL: mkstemp 失败\n"); return 1; } close(tfd); g_settings_file = tmpf; int before = panel_policy_write_enabled(); if (settings_write_one("panel.policy_enabled", "1") != 0) { printf("SELFTEST FAIL: 写 panel.policy_enabled 失败\n"); unlink(tmpf); g_settings_file = saved; return 1; } int after = panel_policy_write_enabled(); unlink(tmpf); g_settings_file = saved; if (before != 0 || after != 1) { printf("SELFTEST FAIL: policy gate 默认关/开启 %d->%d\n", before, after); return 1; } } /* 9) power_policy_append_flag:对象 JSON 剥尾拼入 policy_write_enabled 后仍可解析、键为布尔、原字段保留 * ——防括号错位重演(overview 教训)。测试在无 panel.policy_enabled 的临时 settings 下进行(期望 false=默认关)。 */ { const char *saved = g_settings_file; char tmpf[] = "/tmp/meowpanel_policy_append_XXXXXX"; int tfd = mkstemp(tmpf); if (tfd >= 0) { close(tfd); g_settings_file = tmpf; } sbuf_t p = {0}; if (power_policy_append_flag(&p, "{\"profile\":\"balanced\",\"enacted\":true") != 0) { printf("SELFTEST FAIL: power_policy_append_flag 拼接失败\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; } if (!utf8_valid_n(p.b, p.n) || !strstr(p.b, "\"policy_write_enabled\":false")) { printf("SELFTEST FAIL: power_policy_append_flag 产物异常\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; } jv_t *pr = jv_parse(p.b, (int)p.n); if (!pr) { printf("SELFTEST FAIL: power_policy_append_flag 产物 JSON 无法解析\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; } jv_t *pwe = jv_obj_get(pr, "policy_write_enabled"); if (!pwe || pwe->type != J_BOOL || pwe->bval != 0) { printf("SELFTEST FAIL: power_policy_append_flag policy_write_enabled 非 false(默认关)\n"); jv_free(pr); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; } jv_t *pfl = jv_obj_get(pr, "profile"); if (!pfl || pfl->type != J_STR || strcmp(pfl->str, "balanced") != 0) { printf("SELFTEST FAIL: power_policy_append_flag 丢失原字段\n"); jv_free(pr); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; } jv_free(pr); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } } /* 10) net_bt_aggregate(F1-③):0xE6 五读文本聚合 → 合法 JSON + 字段断言; * 设备名含截断坏字节 → sA_str_n 消毒为 '?' 且产物仍合法;子文本 NULL → 空数组;status NULL → available:false。 */ { sbuf_t b = {0}; const char *st = "bt available=1 powered=1 address=4C:BC:98:B0:98:DB paired=2 connected=1"; const char *pa = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 流媒体客户\xE7\n"; const char *co = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n"; const char *a2 = "A2DP 33:DE:2E:21:AD:30 codec=sbc rate=44100 channels=2 bits=16 acquired=1 role=src\n"; const char *dv = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 17OP15\n"; if (net_bt_aggregate(st, pa, co, a2, dv, &b) != 0) { printf("SELFTEST FAIL: net_bt_aggregate 正常文本返回非 0\n"); sbuf_free(&b); return 1; } if (!b.b || !utf8_valid_n(b.b, b.n) || !json_strict_balanced(b.b, b.n)) { printf("SELFTEST FAIL: net_bt_aggregate 产物非法字节/括号错位\n"); sbuf_free(&b); return 1; } jv_t *br = jv_parse(b.b, (int)b.n); if (!br) { printf("SELFTEST FAIL: net_bt_aggregate 产物 JSON 无法解析\n"); sbuf_free(&b); return 1; } jv_t *pw = jv_obj_get(br, "powered"); if (!pw || pw->type != J_BOOL || !pw->bval) { printf("SELFTEST FAIL: net_bt_aggregate powered 非 true\n"); jv_free(br); sbuf_free(&b); return 1; } jv_t *pr = jv_obj_get(br, "paired"); if (!pr || pr->type != J_ARR || !pr->child || !pr->child->next || pr->child->next->next) { printf("SELFTEST FAIL: net_bt_aggregate paired 应 2 条\n"); jv_free(br); sbuf_free(&b); return 1; } jv_t *cn = jv_obj_get(br, "connected"); if (!cn || cn->type != J_ARR || !cn->child || cn->child->next) { printf("SELFTEST FAIL: net_bt_aggregate connected 应 1 条\n"); jv_free(br); sbuf_free(&b); return 1; } jv_t *a2p = jv_obj_get(br, "a2dp"); if (!a2p || a2p->type != J_ARR || !a2p->child) { printf("SELFTEST FAIL: net_bt_aggregate 缺 a2dp 条目\n"); jv_free(br); sbuf_free(&b); return 1; } jv_t *cd = jv_obj_get(a2p->child, "codec"); if (!cd || cd->type != J_STR || !cd->str || strcmp(cd->str, "sbc") != 0) { printf("SELFTEST FAIL: net_bt_aggregate a2dp codec 非 sbc\n"); jv_free(br); sbuf_free(&b); return 1; } jv_t *aq = jv_obj_get(a2p->child, "acquired"); if (!aq || aq->type != J_BOOL || !aq->bval) { printf("SELFTEST FAIL: net_bt_aggregate a2dp acquired 非 true\n"); jv_free(br); sbuf_free(&b); return 1; } jv_free(br); sbuf_free(&b); /* status NULL → available:false;子文本 NULL → 空数组 */ sbuf_t b2 = {0}; net_bt_aggregate(NULL, NULL, NULL, NULL, NULL, &b2); if (!b2.b || strstr(b2.b, "\"available\":false") == NULL) { printf("SELFTEST FAIL: net_bt_aggregate status NULL 未降级 available:false\n"); sbuf_free(&b2); return 1; } sbuf_free(&b2); sbuf_t b3 = {0}; net_bt_aggregate(st, NULL, NULL, NULL, NULL, &b3); jv_t *b3r = b3.b ? jv_parse(b3.b, (int)b3.n) : NULL; jv_t *b3p = b3r ? jv_obj_get(b3r, "paired") : NULL; if (!b3p || b3p->type != J_ARR || b3p->child) { printf("SELFTEST FAIL: net_bt_aggregate 子文本 NULL 应为空数组\n"); if (b3r) jv_free(b3r); sbuf_free(&b3); return 1; } jv_free(b3r); sbuf_free(&b3); } /* 11) files_search_json(F1-④):沙盒卡根递归搜索(限深/限量/类型过滤/rel 回显)→ 合法 JSON + 计数断言; * 白名单外起点 → -1;name 空 → -3;非法 type → -4。 */ { char tmpl[] = "/tmp/mp_search_XXXXXX"; char *root = mkdtemp(tmpl); if (!root) { printf("SELFTEST FAIL: mkdtemp 失败\n"); return 1; } char saved_root[1024]; snprintf(saved_root, sizeof saved_root, "%s", g_tf_root_buf); snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", root); char d1[1200], d2[1200], f1[1200], f2[1200], f3[1200]; snprintf(d1, sizeof d1, "%s/MeowOS", root); snprintf(d2, sizeof d2, "%s/MeowOS/sub", root); snprintf(f1, sizeof f1, "%s/MeowOS/Test Song.mp3", root); snprintf(f2, sizeof f2, "%s/MeowOS/another.txt", root); snprintf(f3, sizeof f3, "%s/MeowOS/sub/x Song2.log", root); int setup_ok = (mkdir(d1, 0755) == 0 && mkdir(d2, 0755) == 0); const char *mk[3] = { f1, f2, f3 }; for (int i = 0; i < 3 && setup_ok; i++) { FILE *f = fopen(mk[i], "w"); if (!f) { setup_ok = 0; break; } fputs("x", f); fclose(f); } if (!setup_ok) { printf("SELFTEST FAIL: 搜索沙盒构建失败\n"); snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root); rm_recursive(root); rmdir(root); return 1; } int fails = 0; sbuf_t s = {0}; if (files_search_json(d1, "song", NULL, 0, &s) != 0) { printf("SELFTEST FAIL: files_search_json 正常搜索返回非 0\n"); fails = 1; } jv_t *sr = (!fails && s.b) ? jv_parse(s.b, (int)s.n) : NULL; if (!fails && (!sr || !utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n))) { printf("SELFTEST FAIL: files_search_json 产物非法\n"); fails = 1; } jv_t *cnt = (!fails && sr) ? jv_obj_get(sr, "count") : NULL; if (!fails && (!cnt || cnt->type != J_NUM || cnt->inum != 2)) { printf("SELFTEST FAIL: files_search_json song 应中 2 条(递归命中子目录)\n"); fails = 1; } jv_t *its = (!fails && sr) ? jv_obj_get(sr, "items") : NULL; jv_t *rel0 = (its && its->child) ? jv_obj_get(its->child, "rel") : NULL; if (!fails && (!rel0 || rel0->type != J_STR || !rel0->str || strncmp(rel0->str, "/MeowOS/", 8) != 0)) { printf("SELFTEST FAIL: files_search_json rel 未按卡根相对回显\n"); fails = 1; } if (sr) jv_free(sr); sbuf_free(&s); /* type=dir 过滤:建 Songs 目录应仅中目录 */ char d3[1200]; snprintf(d3, sizeof d3, "%s/MeowOS/Songs", root); if (!fails && mkdir(d3, 0755) != 0) { printf("SELFTEST FAIL: 沙盒 Songs 目录创建失败\n"); fails = 1; } sbuf_t s2 = {0}; if (!fails && files_search_json(d1, "song", "dir", 0, &s2) == 0) { jv_t *sr2 = jv_parse(s2.b, (int)s2.n); jv_t *c2 = sr2 ? jv_obj_get(sr2, "count") : NULL; jv_t *i2 = sr2 ? jv_obj_get(sr2, "items") : NULL; jv_t *t0 = (i2 && i2->child) ? jv_obj_get(i2->child, "type") : NULL; if (!c2 || c2->type != J_NUM || c2->inum != 1 || !t0 || t0->type != J_STR || strcmp(t0->str, "dir") != 0) { printf("SELFTEST FAIL: files_search_json type=dir 应仅中 1 个目录\n"); fails = 1; } if (sr2) jv_free(sr2); } else if (!fails) { printf("SELFTEST FAIL: files_search_json type=dir 搜索失败\n"); fails = 1; } sbuf_free(&s2); /* 无匹配 → count 0;白名单外 → -1;空 name → -3;非法 type → -4 */ sbuf_t s3 = {0}; if (!fails && files_search_json(d1, "zzzz", NULL, 0, &s3) == 0) { if (!strstr(s3.b, "\"count\":0")) { printf("SELFTEST FAIL: files_search_json 无匹配应 count 0\n"); fails = 1; } } else if (!fails) { printf("SELFTEST FAIL: files_search_json 无匹配搜索失败\n"); fails = 1; } sbuf_free(&s3); sbuf_t s4 = {0}; if (!fails && files_search_json("/etc", "x", NULL, 0, &s4) != -1) { printf("SELFTEST FAIL: files_search_json 白名单外未拒\n"); fails = 1; } if (!fails && files_search_json(d1, "", NULL, 0, &s4) != -3) { printf("SELFTEST FAIL: files_search_json 空 name 未拒\n"); fails = 1; } if (!fails && files_search_json(d1, "x", "bogus", 0, &s4) != -4) { printf("SELFTEST FAIL: files_search_json 非法 type 未拒\n"); fails = 1; } sbuf_free(&s4); snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root); rm_recursive(root); rmdir(root); if (fails) return 1; } printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 端点池 + 蓝牙 0xE6 聚合 + 文件搜索 → 合法 JSON + 无非法字节;防自锁实体 + policy 门禁 默认关/开启 校验通过\n"); return 0; } int main(int argc, char **argv) { /* 参数:-p PORT(可选)。默认端口 8080。 */ int port = DEFAULT_PORT; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--selftest")) return panel_selftest(); if (!strcmp(argv[i], "-p") && i + 1 < argc) port = atoi(argv[++i]); else if (!strcmp(argv[i], "--sock-perf") && i + 1 < argc) g_sock_perf = argv[++i]; else if (!strcmp(argv[i], "--sock-power") && i + 1 < argc) g_sock_power = argv[++i]; else if (!strcmp(argv[i], "--sock-launcher") && i + 1 < argc) g_sock_launcher = argv[++i]; else if (!strcmp(argv[i], "--sock-audio") && i + 1 < argc) g_sock_audio = argv[++i]; else if (!strcmp(argv[i], "--sock-net") && i + 1 < argc) g_sock_net = argv[++i]; else if (!strcmp(argv[i], "--sock-pm") && i + 1 < argc) g_sock_pm = argv[++i]; else if (!strcmp(argv[i], "--sock") && i + 1 < argc) { g_sock_perf = argv[++i]; g_sock_power = g_sock_perf; } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { printf("MeowPanel backend %s\n", PANEL_VERSION); printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P] [--sock-launcher P] [--sock-audio P] [--sock-net P] [--sock-pm P]\n"); return 0; } } init_tf_paths(); /* 先定卡根/派生路径(env 可覆盖,宿主测试用) */ const char *env = getenv("MEOW_IPC_PERF_SOCK"); if (env && env[0]) g_sock_perf = env; env = getenv("MEOW_IPC_POWER_SOCK"); if (env && env[0]) g_sock_power = env; env = getenv("MEOW_IPC_LAUNCHER_SOCK"); if (env && env[0]) g_sock_launcher = env; env = getenv("MEOW_AUDIO_SOCK"); if (env && env[0]) g_sock_audio = env; /* 与 audiod CLI 同 env(宿主/qemu 测试用) */ env = getenv("MEOW_NET_SOCK"); if (env && env[0]) g_sock_net = env; /* 与 netmgrd/meow_net 同 env(宿主/qemu 测试用) */ env = getenv("MEOW_PM_SOCK"); if (env && env[0]) g_sock_pm = env; /* 与 powermgr/meow_power 同 env(宿主/qemu 测试用) */ env = getenv("MEOW_AUTH_TTL"); if (env && env[0]) g_auth_ttl = (time_t)atol(env); /* 会话 TTL 覆盖(宿主测试用) */ env = getenv("MEOW_SETTINGS_FILE"); if (env && env[0]) g_settings_file = env; /* 设置文件覆盖(宿主测试用;设备默认 /usr/data/meow/settings.conf) */ (void)read_port_config(); /* 应用 meowpanel.conf 的 port(env/-p 优先) */ /* 优先级:-p 已设则保留;否则用 conf/env(read_port_config 会写 g_port) */ if (port != DEFAULT_PORT) g_port = port; else g_port = read_port_config(); /* 再按 env MEOWPORT 覆盖 conf(-p 最高) */ if (port == DEFAULT_PORT) { const char *ep = getenv("MEOWPORT"); if (ep && ep[0]) g_port = atoi(ep); } if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT; signal(SIGPIPE, SIG_IGN); web_root(); panel_log("MeowPanel %s listening on port %d (web root %s)", PANEL_VERSION, g_port, web_root()); int lfd = socket(AF_INET, SOCK_STREAM, 0); if (lfd < 0) { panel_log("socket fail: %s", strerror(errno)); return 1; } int on = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on); struct sockaddr_in addr; memset(&addr, 0, sizeof addr); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons((uint16_t)g_port); if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0) { panel_log("bind :%d fail: %s", g_port, strerror(errno)); close(lfd); return 1; } if (listen(lfd, 16) < 0) { panel_log("listen fail: %s", strerror(errno)); close(lfd); return 1; } for (;;) { int c = accept(lfd, NULL, NULL); if (c < 0) { if (errno == EINTR) continue; continue; } handle_conn(c); } return 0; }