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
110 lines
3.1 KiB
C++
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
|