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:
2026-09-10 15:27:20 +04:00
co-authored by Claude Sonnet 5
parent 170bcfdb3e
commit e60d6669d8
18 changed files with 1563 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
#include "rpc/runtime_dir.hpp"
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
#include <string>
namespace velox::daemon::rpc {
namespace {
std::error_code errc(int e) { return std::error_code(e, std::generic_category()); }
// Ensure `dir` exists as a directory we own with mode 0700. Creates it if absent.
std::error_code ensure_private_dir(const std::string& dir) {
if (::mkdir(dir.c_str(), 0700) != 0 && errno != EEXIST) return errc(errno);
struct stat st{};
if (::lstat(dir.c_str(), &st) != 0) return errc(errno);
if (!S_ISDIR(st.st_mode)) return errc(ENOTDIR);
if (st.st_uid != ::geteuid()) return errc(EPERM);
// Tighten if a prior run (or umask) left it looser. Group/other bits must be clear:
// the socket is 0600 but a traversable parent still lets another user stat it.
if ((st.st_mode & 077) != 0 && ::chmod(dir.c_str(), 0700) != 0) return errc(errno);
return {};
}
} // namespace
std::error_code resolve_runtime_dir(RuntimeDir& out) {
std::string base;
if (const char* xdg = ::getenv("XDG_RUNTIME_DIR"); xdg != nullptr && xdg[0] != '\0') {
base = xdg;
} else {
base = "/run/user/" + std::to_string(::geteuid());
struct stat st{};
if (::stat(base.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) {
// No XDG_RUNTIME_DIR and no /run/user/<uid>: we refuse rather than pick an
// insecure fallback. The caller surfaces this as "cannot start".
return errc(ENOENT);
}
}
if (!base.empty() && base.back() == '/') base.pop_back();
const std::string dir = base + "/velox";
if (auto ec = ensure_private_dir(dir)) return ec;
out.path = dir;
return {};
}
} // namespace velox::daemon::rpc