Files
vdm/daemon/src/rpc/event_loop.cpp
T
samiandClaude Sonnet 5 e60d6669d8 daemon: rpc/ — Unix-socket transport + generated dispatch wiring (build step 1)
First real code in daemon/. veloxd now listens on
$XDG_RUNTIME_DIR/velox/velox.sock (0600, SO_PEERCRED same-UID check),
frames NDJSON, and routes every method through the generated
velox::proto::dispatch(). The CLI and GUI have a server to talk to.

Modules:
- rpc/ndjson.hpp    — newline-delimited framing, 8 MiB frame cap, CRLF-
                       tolerant, partial-tail buffering. Header-only, tested.
- rpc/event_loop    — single-threaded poll(2) reactor; never blocks the
                       loop. stop()/wake() are async-signal-safe (eventfd).
- rpc/runtime_dir   — $XDG_RUNTIME_DIR/velox resolution, 0700, owner-checked;
                       refuses an insecure fallback rather than using /tmp.
- rpc/uds_server    — listener + non-blocking per-conn read/write with
                       backpressure; handles session.hello (protocol-major
                       check -> -32001, sessionId, transport=uds) and
                       session.subscribe in the server layer; routes the
                       rest through dispatch().
- rpc/dispatcher    — VeloxDispatcher : proto::Dispatcher, all 39 methods.
                       download.list answers an empty table; the rest return
                       "not implemented" (-> -32603) until the store lands.
- main.cpp          — abstract-namespace single-instance lock, signal ->
                       clean shutdown, socket unlinked on exit.

Tests (ASan+UBSan and TSan clean):
- veloxd.ndjson         — framing edge cases
- veloxd.uds_roundtrip  — real socket: hello ok / version mismatch / empty
                          list / -32601 / -32700 / pipelined requests, and a
                          guard on the -32603 collapse documented in P1.

Known gap, filed not worked around: daemon/docs/proto-requests-m1.md P1 —
the generated Dispatcher has no error channel below -32603, so handlers
cannot yet return -32010/-32011/-32013 with their data payloads. The
server layer handles -32001/-32002/-32003 around dispatch(); genuine
in-handler errors collapse to -32603 until PROTO gives handlers a real
error return. Three error fixtures are non-conformant until then.

Not in this drop: rpc/ws_server (next; needs the store for hashed pairing
tokens), store/, sched/, cli/. WS reuses this event loop.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
2026-09-10 15:27:20 +04:00

110 lines
3.1 KiB
C++

#include "rpc/event_loop.hpp"
#include <poll.h>
#include <sys/eventfd.h>
#include <unistd.h>
#include <cerrno>
#include <cstdint>
#include <stdexcept>
#include <vector>
namespace velox::daemon::rpc {
EventLoop::EventLoop() {
wake_fd_ = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (wake_fd_ < 0) throw std::runtime_error("eventfd() failed");
fds_.emplace(wake_fd_, Entry{kRead, [this](int, unsigned) { drain_wakeup(); }});
}
EventLoop::~EventLoop() {
if (wake_fd_ >= 0) ::close(wake_fd_);
}
void EventLoop::add_fd(int fd, unsigned interest, Callback cb) {
fds_[fd] = Entry{interest, std::move(cb)};
}
void EventLoop::mod_fd(int fd, unsigned interest) {
if (auto it = fds_.find(fd); it != fds_.end()) it->second.interest = interest;
}
void EventLoop::del_fd(int fd) {
if (fd == wake_fd_) return; // internal, never removed
fds_.erase(fd);
}
void EventLoop::wake() noexcept {
const std::uint64_t one = 1;
// Best-effort: an EAGAIN here means a wakeup is already pending, which is fine.
[[maybe_unused]] ssize_t n = ::write(wake_fd_, &one, sizeof(one));
}
void EventLoop::stop() noexcept {
stop_requested_ = true;
wake();
}
void EventLoop::drain_wakeup() noexcept {
std::uint64_t sink = 0;
while (::read(wake_fd_, &sink, sizeof(sink)) > 0) {
}
}
void EventLoop::run() {
if (running_) throw std::logic_error("EventLoop::run() is not re-entrant");
running_ = true;
stop_requested_ = false;
std::vector<pollfd> pfds;
std::vector<int> fired;
while (!stop_requested_) {
pfds.clear();
pfds.reserve(fds_.size());
for (const auto& [fd, e] : fds_) {
short ev = 0;
if (e.interest & kRead) ev |= POLLIN;
if (e.interest & kWrite) ev |= POLLOUT;
if (ev == 0 && fd != wake_fd_) continue;
pollfd p{};
p.fd = fd;
p.events = ev;
pfds.push_back(p);
}
const int rc = ::poll(pfds.data(), pfds.size(), -1);
if (rc < 0) {
if (errno == EINTR) continue;
throw std::runtime_error("poll() failed");
}
if (rc == 0) continue;
// Snapshot the fds that fired before invoking any callback: a callback may erase
// entries from fds_, which would invalidate iteration over pfds' referents.
fired.clear();
for (const auto& p : pfds) {
if (p.revents != 0) fired.push_back(p.fd);
}
for (const int fd : fired) {
const auto it = fds_.find(fd);
if (it == fds_.end()) continue; // removed by an earlier callback this pass
// Recompute revents for this fd from the snapshot.
unsigned events = 0;
for (const auto& p : pfds) {
if (p.fd != fd) continue;
if (p.revents & (POLLIN | POLLHUP | POLLERR)) events |= kRead;
if (p.revents & POLLOUT) events |= kWrite;
break;
}
if (events != 0) it->second.cb(fd, events);
}
}
running_ = false;
}
} // namespace velox::daemon::rpc