#include "client.hpp" #include #include #include #include #include #include #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 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(&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 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 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(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(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