#include "rpc/RpcConnection.hpp" #include #include #include #include #include "rpc/Protocol.hpp" #include "velox_proto.hpp" namespace velox::gui::rpc { namespace { constexpr int kStateDisconnected = int(ConnectionState::Disconnected); constexpr int kStateConnecting = int(ConnectionState::Connecting); constexpr int kStateHandshaking = int(ConnectionState::Handshaking); constexpr int kStateConnected = int(ConnectionState::Connected); constexpr int kStateReconnecting = int(ConnectionState::Reconnecting); QJsonObject makeErrorEnvelope(int code, const QString &message) { QJsonObject err{{"code", code}, {"message", message}}; return QJsonObject{{"jsonrpc", "2.0"}, {"error", err}}; } QString contractProtocolVersion() { const auto v = velox::proto::kProtocolVersion; return QString::fromUtf8(v.data(), static_cast(v.size())); } } // namespace RpcConnection::RpcConnection(QString socketPath, QObject *parent) : QObject(parent), socketPath_(std::move(socketPath)) { socket_ = new QLocalSocket(this); connect(socket_, &QLocalSocket::connected, this, &RpcConnection::onConnected); connect(socket_, &QLocalSocket::disconnected, this, &RpcConnection::onDisconnected); connect(socket_, &QLocalSocket::errorOccurred, this, &RpcConnection::onErrorOccurred); connect(socket_, &QLocalSocket::readyRead, this, &RpcConnection::onReadyRead); reconnectTimer_ = new QTimer(this); reconnectTimer_->setSingleShot(true); connect(reconnectTimer_, &QTimer::timeout, this, &RpcConnection::attemptReconnect); } RpcConnection::~RpcConnection() = default; void RpcConnection::start() { if (wantConnected_) { return; } wantConnected_ = true; backoffMs_ = kMinBackoffMs; setState(kStateConnecting); socket_->connectToServer(socketPath_); } void RpcConnection::stop() { wantConnected_ = false; reconnectTimer_->stop(); socket_->abort(); rxBuffer_.clear(); // Fail any in-flight callbacks so RpcClient does not leak them. const auto tags = pendingTags_.values(); pendingTags_.clear(); for (const quint64 tag : tags) { emit callCompleted(tag, makeErrorEnvelope(-1, QStringLiteral("connection stopped"))); } setState(kStateDisconnected); } void RpcConnection::sendCall(quint64 tag, const QString &methodName, const QJsonObject ¶ms) { if (state_ != kStateConnected) { emit callCompleted(tag, makeErrorEnvelope(-1, QStringLiteral("not connected"))); return; } const qint64 id = nextRpcId_++; pendingTags_.insert(id, tag); sendRaw(id, methodName, params); } void RpcConnection::onConnected() { rxBuffer_.clear(); beginHandshake(); } void RpcConnection::beginHandshake() { setState(kStateHandshaking); sendRaw(kHelloId, QString::fromLatin1(method::kSessionHello), QJsonObject{{"clientType", "gui"}, {"clientName", QStringLiteral("velox-gui 0.1.0")}, {"protocolVersion", contractProtocolVersion()}}); } // A dropped connection surfaces as disconnected(); a *failed* connect attempt surfaces as // errorOccurred() with no disconnected() to follow. Both funnel here, and the isActive() // guard means the two firing together for one failure only arms one timer. void RpcConnection::scheduleReconnect() { if (!wantConnected_) { setState(kStateDisconnected); return; } if (reconnectTimer_->isActive()) { return; } setState(kStateReconnecting); reconnectTimer_->start(backoffMs_); backoffMs_ = qMin(backoffMs_ * 2, kMaxBackoffMs); } void RpcConnection::onDisconnected() { scheduleReconnect(); } void RpcConnection::onErrorOccurred() { scheduleReconnect(); } void RpcConnection::attemptReconnect() { if (!wantConnected_) { return; } setState(kStateConnecting); socket_->abort(); socket_->connectToServer(socketPath_); } void RpcConnection::onReadyRead() { rxBuffer_ += socket_->readAll(); int nl = rxBuffer_.indexOf('\n'); while (nl != -1) { const QByteArray line = rxBuffer_.left(nl).trimmed(); rxBuffer_.remove(0, nl + 1); nl = rxBuffer_.indexOf('\n'); if (line.isEmpty()) { continue; } QJsonParseError perr{}; const QJsonDocument doc = QJsonDocument::fromJson(line, &perr); if (perr.error != QJsonParseError::NoError || !doc.isObject()) { continue; // a malformed frame is dropped, never thrown } dispatchFrame(doc.object()); } } void RpcConnection::dispatchFrame(const QJsonObject &frame) { const QJsonValue idVal = frame.value("id"); const bool isResponse = frame.contains("result") || frame.contains("error"); if (idVal.isDouble() && isResponse) { const qint64 id = static_cast(idVal.toDouble()); if (id == kHelloId) { if (frame.contains("error")) { socket_->abort(); // version mismatch or refused — bounce and retry return; } sendRaw( kSubscribeId, QString::fromLatin1(method::kSessionSubscribe), QJsonObject{ {"events", QJsonArray{event::kTaskAdded, event::kTaskRemoved, event::kTaskState, event::kTaskProgress, event::kSpeedGlobal, event::kNotify, event::kSettingsChanged, event::kGrabberProgress}}}); return; } if (id == kSubscribeId) { if (frame.contains("error")) { socket_->abort(); return; } backoffMs_ = kMinBackoffMs; setState(kStateConnected); return; } const auto it = pendingTags_.constFind(id); if (it != pendingTags_.cend()) { const quint64 tag = it.value(); pendingTags_.erase(it); emit callCompleted(tag, frame); } return; } const QString methodName = frame.value("method").toString(); if (!methodName.isEmpty()) { emit notificationReceived(methodName, frame.value("params").toObject()); } } void RpcConnection::sendRaw(qint64 rpcId, const QString &methodName, const QJsonObject ¶ms) { const QJsonObject req{{"jsonrpc", "2.0"}, {"id", static_cast(rpcId)}, {"method", methodName}, {"params", params}}; QByteArray line = QJsonDocument(req).toJson(QJsonDocument::Compact); line += '\n'; socket_->write(line); } void RpcConnection::setState(int state) { if (state_ == state) { return; } state_ = state; emit stateChanged(state); } } // namespace velox::gui::rpc