/** * framecache.c — Main entry point. HTTP API server + slot manager. * * Endpoints: * POST /slots Create slot * GET /slots List slots * GET /slots/:id Get slot detail * DELETE /slots/:id Destroy slot * GET /health Health check * * Uses libmicrohttpd for the HTTP layer (single-threaded, poll-based). */ #include "slot.h" #include "registry.h" #include #include #include #include #include #include #include #include #include #ifndef FC_PORT_DEFAULT #define FC_PORT_DEFAULT 7435 #endif /* ── tiny JSON helpers ─────────────────────────────────────────────── */ static int json_get_uint(const char *json, const char *key, uint32_t *out) { char pat[128]; snprintf(pat, sizeof pat, "\"%s\":", key); const char *p = strstr(json, pat); if (!p) return -1; p += strlen(pat); while (*p == ' ' || *p == '\t') p++; *out = (uint32_t)strtoul(p, NULL, 10); return 0; } static int json_get_str(const char *json, const char *key, char *out, size_t out_len) { char pat[128]; snprintf(pat, sizeof pat, "\"%s\":", key); const char *p = strstr(json, pat); if (!p) return -1; p += strlen(pat); while (*p == ' ' || *p == '\t') p++; if (*p != '"') return -1; p++; size_t i = 0; while (*p && *p != '"' && i < out_len - 1) out[i++] = *p++; out[i] = '\0'; return 0; } /* ── HTTP request accumulator ──────────────────────────────────────── */ typedef struct { char *buf; size_t len; size_t cap; } req_body_t; static void req_body_free(req_body_t *r) { free(r->buf); r->buf = NULL; r->len = 0; r->cap = 0; } /* ── response helpers ──────────────────────────────────────────────── */ static enum MHD_Result respond(struct MHD_Connection *conn, unsigned int status, const char *body) { struct MHD_Response *r = MHD_create_response_from_buffer( strlen(body), (void *)body, MHD_RESPMEM_MUST_COPY); MHD_add_response_header(r, "Content-Type", "application/json"); MHD_add_response_header(r, "Access-Control-Allow-Origin", "*"); enum MHD_Result rc = MHD_queue_response(conn, status, r); MHD_destroy_response(r); return rc; } /* ── slot → JSON ───────────────────────────────────────────────────── */ static void slot_to_json(struct fc_slot *s, char *buf, size_t len) { fc_header_t *hdr = fc_slot_header(s); uint64_t wc = atomic_load(&hdr->write_cursor); uint64_t df = atomic_load(&hdr->dropped_frames); /* simple fps estimate — not perfect but good enough for status */ snprintf(buf, len, "{" "\"slot_id\":\"%s\"," "\"shm_path\":\"%s\"," "\"sem_name\":\"%s\"," "\"width\":%u," "\"height\":%u," "\"fps_num\":%u," "\"fps_den\":%u," "\"pixel_format\":\"UYVY422\"," "\"source_type\":\"%s\"," "\"frame_size\":%u," "\"ring_depth\":%u," "\"write_cursor\":%llu," "\"dropped_frames\":%llu" "}", fc_slot_id(s), fc_slot_shm_path(s), fc_slot_sem_name(s), hdr->width, hdr->height, hdr->fps_num, hdr->fps_den, hdr->source_type, hdr->frame_size, hdr->ring_depth, (unsigned long long)wc, (unsigned long long)df ); } /* ── request handler ───────────────────────────────────────────────── */ static enum MHD_Result handle_request( void *cls, struct MHD_Connection *conn, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls) { (void)cls; (void)version; /* First call: allocate body accumulator */ if (*con_cls == NULL) { req_body_t *rb = calloc(1, sizeof *rb); if (!rb) return MHD_NO; *con_cls = rb; return MHD_YES; } req_body_t *rb = (req_body_t *)*con_cls; /* Accumulate POST body */ if (*upload_data_size > 0) { size_t need = rb->len + *upload_data_size + 1; if (need > rb->cap) { rb->buf = realloc(rb->buf, need); rb->cap = need; } memcpy(rb->buf + rb->len, upload_data, *upload_data_size); rb->len += *upload_data_size; rb->buf[rb->len] = '\0'; *upload_data_size = 0; return MHD_YES; } enum MHD_Result rc; char resp[4096]; /* GET /health */ if (strcmp(method, "GET") == 0 && strcmp(url, "/health") == 0) { rc = respond(conn, MHD_HTTP_OK, "{\"status\":\"ok\"}"); goto done; } /* GET /slots * Worst case: FC_MAX_SLOTS (256) × ~2KB/entry ≈ 512KB. A 64KB stack buffer * would overflow at ~32 slots (and `pos` could pass `sizeof big`, making * `sizeof big - pos` underflow to a huge size_t). Heap-allocate a buffer * sized for the worst case and bound-check every append. */ if (strcmp(method, "GET") == 0 && strcmp(url, "/slots") == 0) { size_t cap = (size_t)FC_MAX_SLOTS * 2100 + 64; /* worst case + brackets */ char *big = malloc(cap); if (!big) { rc = respond(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "{\"error\":\"out of memory\"}"); goto done; } size_t pos = 0; if (pos < cap) big[pos++] = '['; int first = 1; for (int i = 0; i < FC_MAX_SLOTS; i++) { if (!g_registry[i].active) continue; char entry[2100]; slot_to_json(g_registry[i].slot, entry, sizeof entry); size_t elen = strlen(entry); /* +2 for possible comma + closing bracket, +1 for NUL */ if (pos + elen + 3 >= cap) break; /* never overflow */ if (!first) big[pos++] = ','; first = 0; memcpy(big + pos, entry, elen); pos += elen; } if (pos + 2 < cap) big[pos++] = ']'; big[pos] = '\0'; rc = respond(conn, MHD_HTTP_OK, big); free(big); goto done; } /* GET /slots/:id */ if (strcmp(method, "GET") == 0 && strncmp(url, "/slots/", 7) == 0 && strlen(url) > 7) { const char *id = url + 7; struct fc_slot *s = registry_find(id); if (!s) { rc = respond(conn, MHD_HTTP_NOT_FOUND, "{\"error\":\"slot not found\"}"); goto done; } slot_to_json(s, resp, sizeof resp); rc = respond(conn, MHD_HTTP_OK, resp); goto done; } /* POST /slots */ if (strcmp(method, "POST") == 0 && strcmp(url, "/slots") == 0) { if (!rb->buf || rb->len == 0) { rc = respond(conn, MHD_HTTP_BAD_REQUEST, "{\"error\":\"empty body\"}"); goto done; } char slot_id[FC_MAX_SLOT_ID] = {0}; char source_type[32] = "unknown"; uint32_t width = 0, height = 0, fps_num = 0, fps_den = 0; json_get_str(rb->buf, "slot_id", slot_id, sizeof slot_id); json_get_str(rb->buf, "source_type", source_type, sizeof source_type); json_get_uint(rb->buf, "width", &width); json_get_uint(rb->buf, "height", &height); json_get_uint(rb->buf, "fps_num", &fps_num); json_get_uint(rb->buf, "fps_den", &fps_den); if (!slot_id[0] || !width || !height || !fps_num || !fps_den) { rc = respond(conn, MHD_HTTP_BAD_REQUEST, "{\"error\":\"missing required fields: " "slot_id, width, height, fps_num, fps_den\"}"); goto done; } if (registry_find(slot_id)) { rc = respond(conn, MHD_HTTP_CONFLICT, "{\"error\":\"slot already exists\"}"); goto done; } struct fc_slot *s = fc_slot_create(slot_id, width, height, fps_num, fps_den, FC_PIX_UYVY422, source_type); if (!s) { rc = respond(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "{\"error\":\"failed to create slot\"}"); goto done; } registry_add(s); snprintf(resp, sizeof resp, "{\"slot_id\":\"%s\"," "\"shm_path\":\"%s\"," "\"sem_name\":\"%s\"}", fc_slot_id(s), fc_slot_shm_path(s), fc_slot_sem_name(s)); rc = respond(conn, MHD_HTTP_CREATED, resp); goto done; } /* DELETE /slots/:id */ if (strcmp(method, "DELETE") == 0 && strncmp(url, "/slots/", 7) == 0 && strlen(url) > 7) { const char *id = url + 7; struct fc_slot *s = registry_find(id); if (!s) { rc = respond(conn, MHD_HTTP_NOT_FOUND, "{\"error\":\"slot not found\"}"); goto done; } registry_remove(id); fc_slot_destroy(s); rc = respond(conn, MHD_HTTP_NO_CONTENT, ""); goto done; } rc = respond(conn, MHD_HTTP_NOT_FOUND, "{\"error\":\"not found\"}"); done: req_body_free(rb); free(rb); *con_cls = NULL; return rc; } static void request_completed(void *cls, struct MHD_Connection *conn, void **con_cls, enum MHD_RequestTerminationCode toe) { (void)cls; (void)conn; (void)toe; if (*con_cls) { req_body_free((req_body_t *)*con_cls); free(*con_cls); *con_cls = NULL; } } /* ── main ──────────────────────────────────────────────────────────── */ static volatile int g_running = 1; static volatile int g_received_signal = 0; static void on_signal(int sig) { g_received_signal = sig; g_running = 0; } int main(void) { signal(SIGINT, on_signal); signal(SIGTERM, on_signal); signal(SIGPIPE, SIG_IGN); /* Ensure /dev/shm/framecache exists */ mkdir("/dev/shm/framecache", 0755); /* Write empty registry */ registry_write_json(); const char *port_str = getenv("FC_PORT"); uint16_t port = port_str ? (uint16_t)atoi(port_str) : FC_PORT_DEFAULT; struct MHD_Daemon *daemon = MHD_start_daemon( MHD_USE_SELECT_INTERNALLY, port, NULL, NULL, handle_request, NULL, MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL, MHD_OPTION_END); if (!daemon) { fprintf(stderr, "[framecache] failed to start HTTP server on port %u\n", port); return 1; } fprintf(stderr, "[framecache] listening on port %u\n", port); while (g_running) { struct timespec ts = { .tv_sec = 0, .tv_nsec = 100000000 }; /* 100ms */ nanosleep(&ts, NULL); } fprintf(stderr, "[framecache] shutting down (signal %d)\n", g_received_signal); /* Destroy all active slots */ for (int i = 0; i < FC_MAX_SLOTS; i++) { if (g_registry[i].active) { registry_remove(g_registry[i].slot_id); fc_slot_destroy(g_registry[i].slot); } } MHD_stop_daemon(daemon); return 0; }