Files
vdm/cli/src/client.cpp
T
samiandClaude Sonnet 5 0c7ce1437c cli: velox — add / ls / pause / resume / rm, with --json (build step 8, pulled forward)
The scriptable client, built now rather than last: it is how the daemon
gets exercised before the GUI is pointed at it (AGENT-DAEMON.md).

- src/client — synchronous blocking RPC over the Unix socket: resolve
  $XDG_RUNTIME_DIR/velox/velox.sock, connect, session.hello, one framed
  request/reply per call. Distinguishes transport failure (exit 3) from
  a daemon-returned error (exit 1).
- src/main — subcommands add/ls/pause/resume/rm; --json prints the raw
  JSON-RPC result or error; --dir/--out/--segments on add;
  --delete-file on rm. Usage errors exit 2.

ls works end to end against veloxd today (empty table). add and the
bulk verbs reach the daemon and surface its "not implemented" (-32603)
cleanly until the store lands — the plumbing is done, the commands
light up as handlers do.

Test velox.client: the real Client against an in-process UdsServer —
no-daemon path, session.hello, download.list, and a not-implemented
method surfacing as an RPC error rather than a transport error.
ASan+UBSan clean; full tree (21 tests, incl. conformance) green.

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

131 lines
4.4 KiB
C++

#include "client.hpp"
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include "velox_proto.hpp"
namespace velox::cli {
namespace {
std::string read_error_message(int e) { return std::strerror(e); }
} // namespace
std::string default_socket_path() {
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());
}
if (!base.empty() && base.back() == '/') base.pop_back();
return base + "/velox/velox.sock";
}
Client::~Client() {
if (fd_ >= 0) ::close(fd_);
}
std::optional<CallError> Client::connect() {
socket_path_ = default_socket_path();
if (socket_path_.size() + 1 > sizeof(sockaddr_un::sun_path)) {
return CallError{CallError::kConnect, "socket path too long: " + socket_path_, {}};
}
fd_ = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd_ < 0) return CallError{CallError::kConnect, read_error_message(errno), {}};
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
std::memcpy(addr.sun_path, socket_path_.c_str(), socket_path_.size());
if (::connect(fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
const int e = errno;
::close(fd_);
fd_ = -1;
return CallError{CallError::kConnect,
"cannot reach veloxd at " + socket_path_ + ": " + read_error_message(e),
{}};
}
nlohmann::json hello = {
{"clientType", "cli"},
{"clientName", std::string("velox ") + std::string(velox::proto::kProtocolVersion)},
{"protocolVersion", std::string(velox::proto::kProtocolVersion)},
};
auto r = call("session.hello", hello);
if (!r) return r.error();
hello_result_ = *r;
return std::nullopt;
}
std::expected<nlohmann::json, CallError> Client::call(const std::string& method,
const nlohmann::json& params) {
const nlohmann::json request = {
{"jsonrpc", "2.0"},
{"id", next_id_++},
{"method", method},
{"params", params},
};
return round_trip(request);
}
std::expected<nlohmann::json, CallError> Client::round_trip(const nlohmann::json& request) {
if (fd_ < 0) return std::unexpected(CallError{CallError::kConnect, "not connected", {}});
std::string out = request.dump();
out.push_back('\n');
std::size_t off = 0;
while (off < out.size()) {
const ssize_t n = ::write(fd_, out.data() + off, out.size() - off);
if (n > 0) {
off += static_cast<std::size_t>(n);
continue;
}
if (n < 0 && errno == EINTR) continue;
return std::unexpected(CallError{CallError::kConnect,
"write to daemon failed: " + read_error_message(errno), {}});
}
// Read until a newline completes a frame.
for (;;) {
if (const auto nl = inbuf_.find('\n'); nl != std::string::npos) {
const std::string line = inbuf_.substr(0, nl);
inbuf_.erase(0, nl + 1);
nlohmann::json reply = nlohmann::json::parse(line, nullptr, false);
if (reply.is_discarded()) {
return std::unexpected(
CallError{CallError::kProtocol, "daemon sent a malformed reply", {}});
}
if (reply.contains("error")) {
const auto& e = reply.at("error");
return std::unexpected(CallError{e.value("code", 0), e.value("message", ""),
e.contains("data") ? e.at("data") : nlohmann::json()});
}
return reply.contains("result") ? reply.at("result") : nlohmann::json(nullptr);
}
char chunk[8192];
const ssize_t n = ::read(fd_, chunk, sizeof(chunk));
if (n > 0) {
inbuf_.append(chunk, static_cast<std::size_t>(n));
continue;
}
if (n == 0) {
return std::unexpected(CallError{CallError::kConnect,
"daemon closed the connection", {}});
}
if (errno == EINTR) continue;
return std::unexpected(CallError{CallError::kConnect,
"read from daemon failed: " + read_error_message(errno), {}});
}
}
} // namespace velox::cli