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
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user