native 9P client via libollie9p

Replace subprocess spawning (run9p) with native 9P connection for hot paths:
- refreshSessions() - called on every sessionsChanged event
- loadRootBackends() - called on daemon connect
- loadRootAgents() - called on daemon connect

Lib9pClient wraps the Go shared library with a Qt-friendly C++ API.
Connection persists across operations, eliminating ~2-5ms per-call
process spawn overhead.

Requires libollie9p.so from parent repo's lib9p/ package.
This commit is contained in:
Levi Neely 2026-08-03 20:04:17 +02:00
parent 24921e7a2b
commit 0e9a7016a3
5 changed files with 347 additions and 5 deletions

View File

@ -87,13 +87,17 @@ if(OLLIE_KF5)
add_executable(ollie-gui
gui/main.cpp
gui/ollie9pclient.cpp
gui/lib9pclient.cpp
gui/ninepconnection.cpp
gui/chatblockmodel.cpp
gui/thememanager.cpp
gui/streamfsm.cpp
${GUI_KF5_QRC}
)
target_include_directories(ollie-gui PRIVATE ${CMAKE_SOURCE_DIR}/gui)
target_include_directories(ollie-gui PRIVATE
${CMAKE_SOURCE_DIR}/gui
$ENV{HOME}/.local/include
)
target_link_libraries(ollie-gui
Qt5::Core
Qt5::Widgets
@ -101,6 +105,7 @@ if(OLLIE_KF5)
Qt5::QuickControls2
KF5::ConfigCore
KF5::SyntaxHighlighting
$ENV{HOME}/.local/lib/libollie9p.so
)
# --- Install ---
@ -148,6 +153,7 @@ else()
add_executable(ollie-gui
gui/main.cpp
gui/ollie9pclient.cpp
gui/lib9pclient.cpp
gui/ninepconnection.cpp
gui/chatblockmodel.cpp
gui/thememanager.cpp
@ -155,6 +161,7 @@ else()
)
target_include_directories(ollie-gui PRIVATE
${CMAKE_SOURCE_DIR}/gui
$ENV{HOME}/.local/include
)
target_link_libraries(ollie-gui
Qt6::Core
@ -163,6 +170,7 @@ else()
Qt6::QuickControls2
KF6::ConfigCore
KF6::SyntaxHighlighting
$ENV{HOME}/.local/lib/libollie9p.so
)
qt_add_resources(ollie-gui "gui"
PREFIX "/"

245
gui/lib9pclient.cpp Normal file
View File

@ -0,0 +1,245 @@
#include "lib9pclient.h"
Lib9pClient::Lib9pClient(QObject *parent)
: QObject(parent)
{
}
Lib9pClient::~Lib9pClient()
{
disconnect();
}
void Lib9pClient::updateLastError()
{
char *err = ollie9p_last_error();
if (err) {
m_lastError = QString::fromUtf8(err);
ollie9p_free(err);
} else {
m_lastError.clear();
}
}
bool Lib9pClient::connect(const QString &addr, const QString &user, const QString &aname)
{
if (m_handle > 0) {
disconnect();
}
QByteArray addrUtf8 = addr.toUtf8();
QByteArray userUtf8 = user.toUtf8();
QByteArray anameUtf8 = aname.toUtf8();
m_handle = ollie9p_connect(addrUtf8.data(), userUtf8.data(), anameUtf8.data());
if (m_handle < 0) {
updateLastError();
return false;
}
m_lastError.clear();
return true;
}
bool Lib9pClient::connectDefault()
{
if (m_handle > 0) {
disconnect();
}
m_handle = ollie9p_connect_default();
if (m_handle < 0) {
updateLastError();
return false;
}
m_lastError.clear();
return true;
}
void Lib9pClient::disconnect()
{
if (m_handle > 0) {
ollie9p_close(m_handle);
m_handle = -1;
}
}
QByteArray Lib9pClient::read(const QString &path)
{
if (m_handle <= 0) {
m_lastError = QStringLiteral("not connected");
return {};
}
QByteArray pathUtf8 = path.toUtf8();
int len = 0;
char *data = ollie9p_read(m_handle, pathUtf8.data(), &len);
if (!data) {
updateLastError();
return {};
}
QByteArray result(data, len);
ollie9p_free(data);
m_lastError.clear();
return result;
}
bool Lib9pClient::write(const QString &path, const QByteArray &data)
{
if (m_handle <= 0) {
m_lastError = QStringLiteral("not connected");
return false;
}
QByteArray pathUtf8 = path.toUtf8();
int written = ollie9p_write(m_handle, pathUtf8.data(),
const_cast<char*>(data.data()), data.size());
if (written < 0) {
updateLastError();
return false;
}
m_lastError.clear();
return true;
}
QByteArray Lib9pClient::rdwr(const QString &path, const QByteArray &data)
{
if (m_handle <= 0) {
m_lastError = QStringLiteral("not connected");
return {};
}
QByteArray pathUtf8 = path.toUtf8();
int outLen = 0;
char *resp = ollie9p_rdwr(m_handle, pathUtf8.data(),
const_cast<char*>(data.data()), data.size(), &outLen);
if (!resp) {
updateLastError();
return {};
}
QByteArray result(resp, outLen);
ollie9p_free(resp);
m_lastError.clear();
return result;
}
QStringList Lib9pClient::ls(const QString &path)
{
if (m_handle <= 0) {
m_lastError = QStringLiteral("not connected");
return {};
}
QByteArray pathUtf8 = path.toUtf8();
char *names = ollie9p_ls(m_handle, pathUtf8.data());
if (!names) {
updateLastError();
return {};
}
QString str = QString::fromUtf8(names);
ollie9p_free(names);
m_lastError.clear();
if (str.isEmpty()) {
return {};
}
return str.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
}
Lib9pClient::StatResult Lib9pClient::stat(const QString &path)
{
StatResult result;
if (m_handle <= 0) {
m_lastError = QStringLiteral("not connected");
return result;
}
QByteArray pathUtf8 = path.toUtf8();
char *data = ollie9p_stat(m_handle, pathUtf8.data());
if (!data) {
updateLastError();
return result;
}
// Parse "mode\tsize\tmtime\tname"
QString str = QString::fromUtf8(data);
ollie9p_free(data);
QStringList parts = str.split(QLatin1Char('\t'));
if (parts.size() >= 4) {
result.mode = parts[0];
result.size = parts[1].toLongLong();
result.mtime = parts[2].toLongLong();
result.name = parts[3];
result.valid = true;
}
m_lastError.clear();
return result;
}
bool Lib9pClient::remove(const QString &path)
{
if (m_handle <= 0) {
m_lastError = QStringLiteral("not connected");
return false;
}
QByteArray pathUtf8 = path.toUtf8();
if (ollie9p_remove(m_handle, pathUtf8.data()) < 0) {
updateLastError();
return false;
}
m_lastError.clear();
return true;
}
bool Lib9pClient::create(const QString &path, uint perm)
{
if (m_handle <= 0) {
m_lastError = QStringLiteral("not connected");
return false;
}
QByteArray pathUtf8 = path.toUtf8();
if (ollie9p_create(m_handle, pathUtf8.data(), perm) < 0) {
updateLastError();
return false;
}
m_lastError.clear();
return true;
}
bool Lib9pClient::mkdir(const QString &path)
{
if (m_handle <= 0) {
m_lastError = QStringLiteral("not connected");
return false;
}
QByteArray pathUtf8 = path.toUtf8();
if (ollie9p_mkdir(m_handle, pathUtf8.data()) < 0) {
updateLastError();
return false;
}
m_lastError.clear();
return true;
}
bool Lib9pClient::rename(const QString &oldPath, const QString &newName)
{
if (m_handle <= 0) {
m_lastError = QStringLiteral("not connected");
return false;
}
QByteArray oldPathUtf8 = oldPath.toUtf8();
QByteArray newNameUtf8 = newName.toUtf8();
if (ollie9p_rename(m_handle, oldPathUtf8.data(), newNameUtf8.data()) < 0) {
updateLastError();
return false;
}
m_lastError.clear();
return true;
}

58
gui/lib9pclient.h Normal file
View File

@ -0,0 +1,58 @@
#ifndef LIB9PCLIENT_H
#define LIB9PCLIENT_H
#include <QObject>
#include <QString>
#include <QByteArray>
#include <libollie9p.h>
// Qt wrapper for libollie9p - provides a clean C++ interface to the Go 9P library.
// Single persistent connection, thread-safe (Go handles internal locking).
class Lib9pClient : public QObject
{
Q_OBJECT
public:
explicit Lib9pClient(QObject *parent = nullptr);
~Lib9pClient() override;
// Connection management
bool connect(const QString &addr, const QString &user, const QString &aname);
bool connectDefault(); // Uses NAMESPACE and USER env vars
void disconnect();
bool isConnected() const { return m_handle > 0; }
// File operations - all return empty/false on error, check lastError()
QByteArray read(const QString &path);
bool write(const QString &path, const QByteArray &data);
QByteArray rdwr(const QString &path, const QByteArray &data); // Write then read
// Directory operations
QStringList ls(const QString &path);
// Metadata
struct StatResult {
QString mode;
qint64 size;
qint64 mtime;
QString name;
bool valid = false;
};
StatResult stat(const QString &path);
// File management
bool remove(const QString &path);
bool create(const QString &path, uint perm = 0666);
bool mkdir(const QString &path);
bool rename(const QString &oldPath, const QString &newName);
// Error handling
QString lastError() const { return m_lastError; }
private:
void updateLastError();
int m_handle = -1;
QString m_lastError;
};
#endif // LIB9PCLIENT_H

View File

@ -81,12 +81,22 @@ Ollie9pClient::Ollie9pClient(QObject *parent)
m_daemon = new NinePConnection(this);
connect(m_daemon, &NinePConnection::connected, this, [this]() {
setDaemonConnected(true);
// Initialize native 9P client for fast operations
if (!m_9p) {
m_9p = new Lib9pClient(this);
}
if (!m_9p->isConnected()) {
m_9p->connectDefault();
}
refreshSessions();
ensureRootDataLoaded();
startAgentConnections();
});
connect(m_daemon, &NinePConnection::disconnected, this, [this]() {
setDaemonConnected(false);
if (m_9p) {
m_9p->disconnect();
}
stopAgentConnections();
});
connect(m_daemon, &NinePConnection::readyRead, this, [this](const QByteArray &data) {
@ -342,7 +352,14 @@ QString Ollie9pClient::agentState(const QString &sessionId, const QString &agent
void Ollie9pClient::refreshSessions()
{
if (!m_daemonConnected) return;
QByteArray out = run9p({"read", "session/idx"});
// Use native 9P client if available (fast path)
QByteArray out;
if (m_9p && m_9p->isConnected()) {
out = m_9p->read(QStringLiteral("session/idx"));
} else {
out = run9p({"read", "session/idx"});
}
QString raw = QString::fromUtf8(out);
m_sessions.clear();
@ -615,7 +632,12 @@ void Ollie9pClient::switchAgent(const QString &sessionId, const QString &agentId
void Ollie9pClient::loadRootBackends()
{
if (m_rootBackendsLoaded) return;
QByteArray out = run9p({"read", "backends"});
QByteArray out;
if (m_9p && m_9p->isConnected()) {
out = m_9p->read(QStringLiteral("backends"));
} else {
out = run9p({"read", "backends"});
}
QString raw = QString::fromUtf8(out).trimmed();
if (!raw.isEmpty()) {
m_availableBackends = raw.split('\n', Qt::SkipEmptyParts);
@ -628,7 +650,12 @@ void Ollie9pClient::loadRootBackends()
void Ollie9pClient::loadRootAgents()
{
if (m_rootAgentsLoaded) return;
QByteArray out = run9p({"read", "agents"});
QByteArray out;
if (m_9p && m_9p->isConnected()) {
out = m_9p->read(QStringLiteral("agents"));
} else {
out = run9p({"read", "agents"});
}
QString raw = QString::fromUtf8(out).trimmed();
if (!raw.isEmpty()) {
m_availableAgents = raw.split('\n', Qt::SkipEmptyParts);

View File

@ -11,6 +11,7 @@
#include "streamfsm.h"
#include "ninepconnection.h"
#include "lib9pclient.h"
// 9P-based client for ollie. Uses ollie-9p subprocess for transport.
// Streaming files (chat, statewait) use StreamFsm instances for lifecycle.
@ -114,7 +115,7 @@ private:
void stopAgentStreams();
void stopStreams();
QString agentPath() const;
QByteArray run9p(const QStringList &args);
QByteArray run9p(const QStringList &args); // Fallback for streaming (uses subprocess)
void ensureRootDataLoaded();
void setDaemonConnected(bool connected);
void startAgentConnections();
@ -123,6 +124,9 @@ private:
void stopAgentConnections();
void handleEvent(const QString &eventLine);
// Native 9P client (persistent connection, no subprocess overhead)
Lib9pClient *m_9p = nullptr;
QVariantList m_sessions;
QString m_activeSessionId;
QString m_activeState = "idle";