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
+98
View File
@@ -0,0 +1,98 @@
// veloxd — the Velox download-manager daemon.
//
// This drop wires up the Unix-socket RPC transport and a dispatcher skeleton so the CLI
// and GUI have a real server to speak to (AGENT-DAEMON.md build order, step 1). The
// WebSocket transport, the SQLite store and the scheduler land next.
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include "rpc/dispatcher.hpp"
#include "rpc/event_loop.hpp"
#include "rpc/runtime_dir.hpp"
#include "rpc/uds_server.hpp"
#include "version.hpp"
namespace {
velox::daemon::rpc::EventLoop* g_loop = nullptr;
void on_signal(int) {
if (g_loop != nullptr) g_loop->stop(); // stop() is async-signal-safe (writes an eventfd)
}
// Single-instance guard: bind an abstract-namespace Unix socket whose name is unique to
// this user. A second daemon gets EADDRINUSE and exits. The kernel reclaims an
// abstract-namespace address when the holding process dies, so a crash never wedges it
// (docs/01 §2). Returns the held fd (kept open for the process lifetime) or -1.
int acquire_single_instance_lock() {
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) return -1;
const std::string name = std::string("velox-daemon-") + std::to_string(::geteuid());
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
// Leading NUL selects the abstract namespace; the name follows, not NUL-terminated.
addr.sun_path[0] = '\0';
std::memcpy(addr.sun_path + 1, name.c_str(), name.size());
const socklen_t len =
static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + 1 + name.size());
if (::bind(fd, reinterpret_cast<sockaddr*>(&addr), len) != 0) {
::close(fd);
return -1;
}
return fd;
}
} // namespace
int main() {
std::cout << "veloxd " << velox::daemon::kDaemonVersion << " (protocol "
<< velox::proto::kProtocolVersion << ")\n";
const int lock_fd = acquire_single_instance_lock();
if (lock_fd < 0) {
std::cerr << "veloxd: another instance is already running for this user\n";
return 1;
}
velox::daemon::rpc::RuntimeDir rt;
if (const auto ec = velox::daemon::rpc::resolve_runtime_dir(rt)) {
std::cerr << "veloxd: cannot prepare runtime directory: " << ec.message() << "\n";
return 1;
}
velox::daemon::rpc::EventLoop loop;
g_loop = &loop;
struct sigaction sa{};
sa.sa_handler = on_signal;
::sigemptyset(&sa.sa_mask);
::sigaction(SIGINT, &sa, nullptr);
::sigaction(SIGTERM, &sa, nullptr);
::signal(SIGPIPE, SIG_IGN); // a client vanishing mid-write is EPIPE, never a signal
velox::daemon::rpc::VeloxDispatcher dispatcher;
velox::daemon::rpc::UdsServer uds(loop, dispatcher, rt.socket_path());
if (const auto ec = uds.start()) {
std::cerr << "veloxd: cannot listen on " << rt.socket_path() << ": " << ec.message()
<< "\n";
return 1;
}
std::cout << "veloxd: listening on " << uds.socket_path() << "\n";
loop.run();
std::cout << "veloxd: shutting down\n";
g_loop = nullptr;
::close(lock_fd);
return 0;
}