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
|
add_executable(ollie-gui
|
||||||
gui/main.cpp
|
gui/main.cpp
|
||||||
gui/ollie9pclient.cpp
|
gui/ollie9pclient.cpp
|
||||||
|
gui/lib9pclient.cpp
|
||||||
gui/ninepconnection.cpp
|
gui/ninepconnection.cpp
|
||||||
gui/chatblockmodel.cpp
|
gui/chatblockmodel.cpp
|
||||||
gui/thememanager.cpp
|
gui/thememanager.cpp
|
||||||
gui/streamfsm.cpp
|
gui/streamfsm.cpp
|
||||||
${GUI_KF5_QRC}
|
${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
|
target_link_libraries(ollie-gui
|
||||||
Qt5::Core
|
Qt5::Core
|
||||||
Qt5::Widgets
|
Qt5::Widgets
|
||||||
|
|
@ -101,6 +105,7 @@ if(OLLIE_KF5)
|
||||||
Qt5::QuickControls2
|
Qt5::QuickControls2
|
||||||
KF5::ConfigCore
|
KF5::ConfigCore
|
||||||
KF5::SyntaxHighlighting
|
KF5::SyntaxHighlighting
|
||||||
|
$ENV{HOME}/.local/lib/libollie9p.so
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Install ---
|
# --- Install ---
|
||||||
|
|
@ -148,6 +153,7 @@ else()
|
||||||
add_executable(ollie-gui
|
add_executable(ollie-gui
|
||||||
gui/main.cpp
|
gui/main.cpp
|
||||||
gui/ollie9pclient.cpp
|
gui/ollie9pclient.cpp
|
||||||
|
gui/lib9pclient.cpp
|
||||||
gui/ninepconnection.cpp
|
gui/ninepconnection.cpp
|
||||||
gui/chatblockmodel.cpp
|
gui/chatblockmodel.cpp
|
||||||
gui/thememanager.cpp
|
gui/thememanager.cpp
|
||||||
|
|
@ -155,6 +161,7 @@ else()
|
||||||
)
|
)
|
||||||
target_include_directories(ollie-gui PRIVATE
|
target_include_directories(ollie-gui PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/gui
|
${CMAKE_SOURCE_DIR}/gui
|
||||||
|
$ENV{HOME}/.local/include
|
||||||
)
|
)
|
||||||
target_link_libraries(ollie-gui
|
target_link_libraries(ollie-gui
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
|
|
@ -163,6 +170,7 @@ else()
|
||||||
Qt6::QuickControls2
|
Qt6::QuickControls2
|
||||||
KF6::ConfigCore
|
KF6::ConfigCore
|
||||||
KF6::SyntaxHighlighting
|
KF6::SyntaxHighlighting
|
||||||
|
$ENV{HOME}/.local/lib/libollie9p.so
|
||||||
)
|
)
|
||||||
qt_add_resources(ollie-gui "gui"
|
qt_add_resources(ollie-gui "gui"
|
||||||
PREFIX "/"
|
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);
|
m_daemon = new NinePConnection(this);
|
||||||
connect(m_daemon, &NinePConnection::connected, this, [this]() {
|
connect(m_daemon, &NinePConnection::connected, this, [this]() {
|
||||||
setDaemonConnected(true);
|
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();
|
refreshSessions();
|
||||||
ensureRootDataLoaded();
|
ensureRootDataLoaded();
|
||||||
startAgentConnections();
|
startAgentConnections();
|
||||||
});
|
});
|
||||||
connect(m_daemon, &NinePConnection::disconnected, this, [this]() {
|
connect(m_daemon, &NinePConnection::disconnected, this, [this]() {
|
||||||
setDaemonConnected(false);
|
setDaemonConnected(false);
|
||||||
|
if (m_9p) {
|
||||||
|
m_9p->disconnect();
|
||||||
|
}
|
||||||
stopAgentConnections();
|
stopAgentConnections();
|
||||||
});
|
});
|
||||||
connect(m_daemon, &NinePConnection::readyRead, this, [this](const QByteArray &data) {
|
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()
|
void Ollie9pClient::refreshSessions()
|
||||||
{
|
{
|
||||||
if (!m_daemonConnected) return;
|
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);
|
QString raw = QString::fromUtf8(out);
|
||||||
|
|
||||||
m_sessions.clear();
|
m_sessions.clear();
|
||||||
|
|
@ -615,7 +632,12 @@ void Ollie9pClient::switchAgent(const QString &sessionId, const QString &agentId
|
||||||
void Ollie9pClient::loadRootBackends()
|
void Ollie9pClient::loadRootBackends()
|
||||||
{
|
{
|
||||||
if (m_rootBackendsLoaded) return;
|
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();
|
QString raw = QString::fromUtf8(out).trimmed();
|
||||||
if (!raw.isEmpty()) {
|
if (!raw.isEmpty()) {
|
||||||
m_availableBackends = raw.split('\n', Qt::SkipEmptyParts);
|
m_availableBackends = raw.split('\n', Qt::SkipEmptyParts);
|
||||||
|
|
@ -628,7 +650,12 @@ void Ollie9pClient::loadRootBackends()
|
||||||
void Ollie9pClient::loadRootAgents()
|
void Ollie9pClient::loadRootAgents()
|
||||||
{
|
{
|
||||||
if (m_rootAgentsLoaded) return;
|
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();
|
QString raw = QString::fromUtf8(out).trimmed();
|
||||||
if (!raw.isEmpty()) {
|
if (!raw.isEmpty()) {
|
||||||
m_availableAgents = raw.split('\n', Qt::SkipEmptyParts);
|
m_availableAgents = raw.split('\n', Qt::SkipEmptyParts);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
#include "streamfsm.h"
|
#include "streamfsm.h"
|
||||||
#include "ninepconnection.h"
|
#include "ninepconnection.h"
|
||||||
|
#include "lib9pclient.h"
|
||||||
|
|
||||||
// 9P-based client for ollie. Uses ollie-9p subprocess for transport.
|
// 9P-based client for ollie. Uses ollie-9p subprocess for transport.
|
||||||
// Streaming files (chat, statewait) use StreamFsm instances for lifecycle.
|
// Streaming files (chat, statewait) use StreamFsm instances for lifecycle.
|
||||||
|
|
@ -114,7 +115,7 @@ private:
|
||||||
void stopAgentStreams();
|
void stopAgentStreams();
|
||||||
void stopStreams();
|
void stopStreams();
|
||||||
QString agentPath() const;
|
QString agentPath() const;
|
||||||
QByteArray run9p(const QStringList &args);
|
QByteArray run9p(const QStringList &args); // Fallback for streaming (uses subprocess)
|
||||||
void ensureRootDataLoaded();
|
void ensureRootDataLoaded();
|
||||||
void setDaemonConnected(bool connected);
|
void setDaemonConnected(bool connected);
|
||||||
void startAgentConnections();
|
void startAgentConnections();
|
||||||
|
|
@ -123,6 +124,9 @@ private:
|
||||||
void stopAgentConnections();
|
void stopAgentConnections();
|
||||||
void handleEvent(const QString &eventLine);
|
void handleEvent(const QString &eventLine);
|
||||||
|
|
||||||
|
// Native 9P client (persistent connection, no subprocess overhead)
|
||||||
|
Lib9pClient *m_9p = nullptr;
|
||||||
|
|
||||||
QVariantList m_sessions;
|
QVariantList m_sessions;
|
||||||
QString m_activeSessionId;
|
QString m_activeSessionId;
|
||||||
QString m_activeState = "idle";
|
QString m_activeState = "idle";
|
||||||
|
|
|
||||||
Reference in New Issue