gui: RPC client, download table model, and a live main window
First vertical slice of velox-gui, built entirely against tools/mockd (no daemon dependency): - rpc/: RpcConnection runs a QLocalSocket on a worker thread with newline-delimited JSON-RPC framing, drives the session.hello / session.subscribe handshake, and reconnects with exponential backoff (250 ms -> 8 s). RpcClient is the main-thread face: marshals calls onto the worker, delivers replies as main-thread callbacks, re-emits server notifications as typed Qt signals, and issues the one-shot download.list on reaching Connected. - models/DownloadTableModel: QAbstractTableModel over TaskSummary. A progress batch is a row patch with a narrow dataChanged over the value columns only; beginResetModel() is reserved for the initial load and a reconnect resync. - widgets/ProgressDelegate: in-cell progress bar for the Status column. - mainwindow/MainWindow: the table, a status-bar connection dot, an offline banner instead of a modal, dialog-free pause/resume/stop actions, and QSettings column/geometry persistence. - gui/CMakeLists.txt links velox::proto (never velox::core, ADR 0009) and self-guards on the veloxproto target so main keeps configuring if it is ever absent again. - i18n from the first commit: every string via tr(), plus an Arabic .ts stub for the RTL check. - tests/: headless QTest for the model — proves the progress patch is a narrow dataChanged and never resets the model. Verified end-to-end against `mockd --tasks 300`: handshake, initial list, live progress batches applied to the model, and a clean Reconnecting -> Connected recovery when mockd is bounced mid-run. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
#include "rpc/RpcClient.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <QLoggingCategory>
|
||||
|
||||
#include "rpc/RpcConnection.hpp"
|
||||
|
||||
namespace velox::gui::rpc {
|
||||
|
||||
// Off by default; enable with QT_LOGGING_RULES="velox.rpc.info=true".
|
||||
Q_LOGGING_CATEGORY(lcRpc, "velox.rpc")
|
||||
|
||||
RpcClient::RpcClient(QString socketPath, QObject *parent) : QObject(parent) {
|
||||
conn_ = new RpcConnection(std::move(socketPath));
|
||||
conn_->moveToThread(&thread_);
|
||||
// QThread flushes DeferredDelete for its objects after the loop exits, so this is the
|
||||
// safe way to destroy an object that lives on the worker thread.
|
||||
connect(&thread_, &QThread::finished, conn_, &QObject::deleteLater);
|
||||
|
||||
connect(conn_, &RpcConnection::stateChanged, this, &RpcClient::onConnectionState);
|
||||
connect(conn_, &RpcConnection::notificationReceived, this, &RpcClient::onNotification);
|
||||
connect(conn_, &RpcConnection::callCompleted, this, &RpcClient::onCallCompleted);
|
||||
}
|
||||
|
||||
RpcClient::~RpcClient() {
|
||||
if (thread_.isRunning()) {
|
||||
stop(); // joins thread_; the finished -> deleteLater above disposes of conn_
|
||||
} else {
|
||||
delete conn_; // start() was never called: no loop will service deleteLater
|
||||
}
|
||||
conn_ = nullptr;
|
||||
// Nothing will ever complete the in-flight callbacks now.
|
||||
const auto pending = std::move(callbacks_);
|
||||
callbacks_.clear();
|
||||
for (const auto &cb : pending) {
|
||||
if (cb) {
|
||||
RpcReply reply;
|
||||
reply.error = {-1, QStringLiteral("client shutting down"), {}};
|
||||
cb(reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RpcClient::start() {
|
||||
if (!thread_.isRunning()) {
|
||||
thread_.start();
|
||||
}
|
||||
QMetaObject::invokeMethod(conn_, "start", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void RpcClient::stop() {
|
||||
if (!thread_.isRunning()) {
|
||||
return;
|
||||
}
|
||||
QMetaObject::invokeMethod(conn_, "stop", Qt::QueuedConnection);
|
||||
thread_.quit();
|
||||
thread_.wait();
|
||||
}
|
||||
|
||||
void RpcClient::call(const QString &methodName, const QJsonObject ¶ms,
|
||||
std::function<void(const RpcReply &)> cb) {
|
||||
const quint64 tag = nextTag_++;
|
||||
callbacks_.insert(tag, std::move(cb));
|
||||
QMetaObject::invokeMethod(conn_, "sendCall", Qt::QueuedConnection, Q_ARG(quint64, tag),
|
||||
Q_ARG(QString, methodName), Q_ARG(QJsonObject, params));
|
||||
}
|
||||
|
||||
void RpcClient::onConnectionState(int state) {
|
||||
state_ = static_cast<ConnectionState>(state);
|
||||
qCInfo(lcRpc, "connection state: %s", toString(state_));
|
||||
emit stateChanged(state_);
|
||||
if (state_ == ConnectionState::Connected) {
|
||||
requestInitialList();
|
||||
}
|
||||
}
|
||||
|
||||
void RpcClient::requestInitialList() {
|
||||
call(QString::fromLatin1(method::kDownloadList), QJsonObject{{"limit", 1000}},
|
||||
[this](const RpcReply &reply) {
|
||||
if (!reply.ok()) {
|
||||
qCWarning(lcRpc, "download.list failed: %d %s", reply.error.code,
|
||||
qUtf8Printable(reply.error.message));
|
||||
return;
|
||||
}
|
||||
const QJsonArray items = reply.result.toObject().value("items").toArray();
|
||||
qCInfo(lcRpc, "initial download.list: %lld row(s)",
|
||||
static_cast<long long>(items.size()));
|
||||
emit taskListReset(items);
|
||||
});
|
||||
}
|
||||
|
||||
void RpcClient::onNotification(const QString &methodName, const QJsonObject ¶ms) {
|
||||
qCDebug(lcRpc, "notification: %s", qUtf8Printable(methodName));
|
||||
if (methodName == QLatin1String(event::kTaskProgress)) {
|
||||
const QJsonArray tasks = params.value("tasks").toArray();
|
||||
qCDebug(lcRpc, "progress batch: %lld task(s)", static_cast<long long>(tasks.size()));
|
||||
emit taskProgress(tasks);
|
||||
} else if (methodName == QLatin1String(event::kTaskAdded)) {
|
||||
emit taskAdded(params.value("summary").toObject());
|
||||
} else if (methodName == QLatin1String(event::kTaskState)) {
|
||||
emit taskStateChanged(params);
|
||||
} else if (methodName == QLatin1String(event::kTaskRemoved)) {
|
||||
emit taskRemoved(params.value("taskId").toString());
|
||||
} else if (methodName == QLatin1String(event::kSpeedGlobal)) {
|
||||
emit speedGlobal(params);
|
||||
} else if (methodName == QLatin1String(event::kNotify)) {
|
||||
emit notify(params);
|
||||
}
|
||||
}
|
||||
|
||||
void RpcClient::onCallCompleted(quint64 tag, const QJsonObject &envelope) {
|
||||
const auto cb = callbacks_.take(tag);
|
||||
if (!cb) {
|
||||
return;
|
||||
}
|
||||
RpcReply reply;
|
||||
if (envelope.contains("error")) {
|
||||
const QJsonObject e = envelope.value("error").toObject();
|
||||
reply.error = {e.value("code").toInt(), e.value("message").toString(), e.value("data")};
|
||||
} else {
|
||||
reply.result = envelope.value("result");
|
||||
}
|
||||
cb(reply);
|
||||
}
|
||||
|
||||
} // namespace velox::gui::rpc
|
||||
Reference in New Issue
Block a user