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:
parent
24921e7a2b
commit
0e9a7016a3
|
|
@ -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 "/"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Reference in New Issue