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
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
// A single-threaded poll(2) reactor. Every RPC listener and connection registers its fd
|
||||
// here; the loop never blocks on disk or DNS (AGENT-DAEMON.md build step 1 — "Never block
|
||||
// the RPC loop"). Long work is handed to CORE's pools later; this class only multiplexes
|
||||
// readiness.
|
||||
//
|
||||
// Thread model: run() executes on one thread. add_fd/mod_fd/del_fd are called from
|
||||
// callbacks on that same thread. stop() and wake() are async-signal-safe and safe to call
|
||||
// from any thread or a signal handler — they only write() a byte to an internal eventfd.
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace velox::daemon::rpc {
|
||||
|
||||
enum Interest : unsigned {
|
||||
kNone = 0,
|
||||
kRead = 1u << 0,
|
||||
kWrite = 1u << 1,
|
||||
};
|
||||
|
||||
class EventLoop {
|
||||
public:
|
||||
// Called when the fd is readable and/or writable. `events` is the subset of the fd's
|
||||
// registered Interest that fired. A callback may add/modify/remove any fd, including
|
||||
// its own, and may call stop().
|
||||
using Callback = std::function<void(int fd, unsigned events)>;
|
||||
|
||||
EventLoop();
|
||||
~EventLoop();
|
||||
|
||||
EventLoop(const EventLoop&) = delete;
|
||||
EventLoop& operator=(const EventLoop&) = delete;
|
||||
|
||||
// Register `fd` (must be non-blocking) for `interest`. Replaces any prior registration.
|
||||
void add_fd(int fd, unsigned interest, Callback cb);
|
||||
// Change the interest mask for an already-registered fd.
|
||||
void mod_fd(int fd, unsigned interest);
|
||||
// Stop watching `fd`. Does not close it — ownership stays with the caller.
|
||||
void del_fd(int fd);
|
||||
|
||||
// Run until stop() is called. Re-entrant calls are not supported.
|
||||
void run();
|
||||
|
||||
// Ask run() to return after the current poll wakeup. Async-signal-safe.
|
||||
void stop() noexcept;
|
||||
|
||||
// Force one poll() wakeup without stopping — used when interest changed from outside a
|
||||
// callback. Async-signal-safe.
|
||||
void wake() noexcept;
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
unsigned interest;
|
||||
Callback cb;
|
||||
};
|
||||
|
||||
void drain_wakeup() noexcept;
|
||||
|
||||
int wake_fd_; // eventfd, always registered
|
||||
bool running_ = false;
|
||||
std::atomic<bool> stop_requested_ = false; // set from stop(), read by run()
|
||||
std::unordered_map<int, Entry> fds_;
|
||||
};
|
||||
|
||||
} // namespace velox::daemon::rpc
|
||||
Reference in New Issue
Block a user