369 lines
12 KiB
C
369 lines
12 KiB
C
/**
|
||
* 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 <time.h>
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdint.h>
|
||
#include <signal.h>
|
||
#include <errno.h>
|
||
#include <sys/stat.h>
|
||
#include <microhttpd.h>
|
||
|
||
#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;
|
||
}
|