/* SPDX-License-Identifier: BSD-2-Clause */ /* * chardev_client.c — kernel-bridge client for the daedalus-v4l2 daemon. */ #include "chardev_client.h" #include "decoder.h" #include "ffmpeg_loader.h" #include "log.h" #include #include #include #include #include #include #define CHARDEV_PATH "/dev/daedalus-v4l2" #define CHARDEV_READ_BUFSZ (sizeof(struct daedalus_msg_hdr) + \ DAEDALUS_PROTO_MAX_PAYLOAD) int chardev_client_open(struct chardev_client *cli, struct ffmpeg_loader *loader, volatile sig_atomic_t *stop_flag) { int fd, rc; memset(cli, 0, sizeof(*cli)); cli->fd = -1; cli->loader = loader; cli->stop_flag = stop_flag; fd = open(CHARDEV_PATH, O_RDWR | O_CLOEXEC); if (fd < 0) { rc = -errno; log_err("open(%s): %s", CHARDEV_PATH, strerror(errno)); return rc; } cli->decoder = calloc(1, sizeof(*cli->decoder)); if (!cli->decoder) { close(fd); return -ENOMEM; } rc = daedalus_decoder_init(cli->decoder, loader); if (rc < 0) { free(cli->decoder); cli->decoder = NULL; close(fd); log_err("daedalus_decoder_init: %d", rc); return rc; } cli->fd = fd; log_info("chardev: opened %s (fd %d)", CHARDEV_PATH, fd); return 0; } void chardev_client_close(struct chardev_client *cli) { if (!cli) return; if (cli->decoder) { daedalus_decoder_cleanup(cli->decoder); free(cli->decoder); cli->decoder = NULL; } if (cli->fd >= 0) { close(cli->fd); cli->fd = -1; } } static int send_response(struct chardev_client *cli, uint32_t type, uint32_t cookie, const void *payload, size_t payload_len) { struct daedalus_msg_hdr hdr; int rc; if (payload_len > DAEDALUS_PROTO_MAX_PAYLOAD) return -EMSGSIZE; hdr.magic = DAEDALUS_PROTO_MAGIC; hdr.version = DAEDALUS_PROTO_VERSION; hdr.type = type; hdr.cookie = cookie; hdr.payload_len = (uint32_t) payload_len; hdr.reserved = 0; /* * The kernel's write() path validates count == sizeof(hdr) * + hdr.payload_len in a single call, and only implements * .write (not .write_iter), so a writev() lands as -EINVAL. * Marshal the message into a single buffer and write() it. * * Response payloads are small (struct daedalus_resp_frame = * 36 bytes; PONG echoes <= 64 KiB). A short-lived heap * allocation per response is fine; per-loop reuse can come * later if profiling demands it. */ { size_t total = sizeof(hdr) + payload_len; uint8_t *out = malloc(total); ssize_t n; if (!out) return -ENOMEM; memcpy(out, &hdr, sizeof(hdr)); if (payload_len) memcpy(out + sizeof(hdr), payload, payload_len); for (;;) { n = write(cli->fd, out, total); if (n >= 0) { if ((size_t) n != total) { log_err("chardev: short write %zd != %zu", n, total); rc = -EIO; } else { rc = 0; } break; } if (errno == EINTR) continue; rc = -errno; log_err("chardev: write: %s", strerror(errno)); break; } free(out); } return rc; } static int handle_req_decode(struct chardev_client *cli, const struct daedalus_msg_hdr *hdr, const uint8_t *payload) { struct daedalus_req_decode req; struct daedalus_resp_frame resp; int rc; if (hdr->payload_len < sizeof(req)) { log_err("REQ_DECODE cookie=%u: payload too short %u < %zu", hdr->cookie, hdr->payload_len, sizeof(req)); memset(&resp, 0, sizeof(resp)); resp.status = DAEDALUS_DECODE_ERR_RECV; return send_response(cli, DAEDALUS_MSG_RESP_FRAME, hdr->cookie, &resp, sizeof(resp)); } memcpy(&req, payload, sizeof(req)); if ((size_t) req.bitstream_len + sizeof(req) != hdr->payload_len) { log_err("REQ_DECODE cookie=%u: bitstream_len %u inconsistent with payload_len %u", hdr->cookie, req.bitstream_len, hdr->payload_len); memset(&resp, 0, sizeof(resp)); resp.status = DAEDALUS_DECODE_ERR_RECV; return send_response(cli, DAEDALUS_MSG_RESP_FRAME, hdr->cookie, &resp, sizeof(resp)); } log_info("REQ_DECODE cookie=%u codec=%u bitstream=%u bytes", hdr->cookie, req.codec_id, req.bitstream_len); rc = daedalus_decoder_run_request(cli->decoder, &req, payload + sizeof(req), &resp); if (rc < 0) return rc; return send_response(cli, DAEDALUS_MSG_RESP_FRAME, hdr->cookie, &resp, sizeof(resp)); } static int handle_ping(struct chardev_client *cli, const struct daedalus_msg_hdr *hdr, const uint8_t *payload) { log_info("PING cookie=%u plen=%u — echoing PONG", hdr->cookie, hdr->payload_len); return send_response(cli, DAEDALUS_MSG_PONG, hdr->cookie, payload, hdr->payload_len); } static int handle_one_message(struct chardev_client *cli, uint8_t *buf) { struct daedalus_msg_hdr hdr; ssize_t n; /* * The kernel chardev delivers exactly one message per * read(). Pass a buffer that can hold any legal message. */ for (;;) { n = read(cli->fd, buf, CHARDEV_READ_BUFSZ); if (n >= 0) break; if (errno == EINTR) { if (*cli->stop_flag) return 0; continue; } log_err("chardev: read: %s", strerror(errno)); return -errno; } if (n == 0) return -EIO; /* EOF / device unplugged */ if ((size_t) n < sizeof(hdr)) { log_err("chardev: short read %zd < hdr", n); return -EBADMSG; } memcpy(&hdr, buf, sizeof(hdr)); if (hdr.magic != DAEDALUS_PROTO_MAGIC) { log_err("chardev: bad magic 0x%08x", hdr.magic); return -EBADMSG; } if (hdr.version != DAEDALUS_PROTO_VERSION) { log_err("chardev: unsupported version %u", hdr.version); return -EPROTO; } if ((size_t) n != sizeof(hdr) + hdr.payload_len) { log_err("chardev: framing mismatch n=%zd expected %zu", n, sizeof(hdr) + hdr.payload_len); return -EBADMSG; } switch (hdr.type) { case DAEDALUS_MSG_PING: return handle_ping(cli, &hdr, buf + sizeof(hdr)); case DAEDALUS_MSG_REQ_DECODE: return handle_req_decode(cli, &hdr, buf + sizeof(hdr)); default: log_warn("chardev: unknown request type 0x%08x cookie=%u", hdr.type, hdr.cookie); return 0; /* skip, don't bail the loop */ } } int chardev_client_run(struct chardev_client *cli) { uint8_t *buf; int rc = 0; buf = malloc(CHARDEV_READ_BUFSZ); if (!buf) return -ENOMEM; log_info("daemon loop started; waiting for kernel requests"); while (!*cli->stop_flag) { rc = handle_one_message(cli, buf); if (rc < 0) { if (rc == -EINTR) continue; log_err("chardev: handle_one_message: %d", rc); break; } } log_info("daemon loop exiting (stop=%d rc=%d)", *cli->stop_flag, rc); free(buf); return rc; }