gui: add heartbeat-based daemon disconnect detection
- Add 2.5 second heartbeat timer using Lib9pClient to read 'help' - Heartbeat detects daemon death reliably (vs unreliable process exit codes) - On disconnect: clear sessions, stop streams, reset active selection - On reconnect: refresh sessions, restart agent streams Replaces flaky NinePConnection::disconnected signal which depended on eventwait process exit behavior.
This commit is contained in:
parent
d01d7c8ad8
commit
2c7b5e6fc5
|
|
@ -47,10 +47,10 @@ Ollie9pClient::Ollie9pClient(QObject *parent)
|
|||
});
|
||||
|
||||
// State stream — guarded, reads agent statewait. Auto-restarts until
|
||||
// session/agent is deselected (guard returns false).
|
||||
// session/agent is deselected or daemon disconnects (guard returns false).
|
||||
m_state = new StreamFsm(StreamFsm::Guarded, this);
|
||||
m_state->setGuard([this]() {
|
||||
return !m_activeSessionId.isEmpty() && !m_agentId.isEmpty();
|
||||
return m_daemonConnected && !m_activeSessionId.isEmpty() && !m_agentId.isEmpty();
|
||||
});
|
||||
connect(m_state, &StreamFsm::readyRead, this, [this](const QByteArray &data) {
|
||||
QString state = QString::fromUtf8(data).trimmed();
|
||||
|
|
@ -60,12 +60,10 @@ Ollie9pClient::Ollie9pClient(QObject *parent)
|
|||
}
|
||||
});
|
||||
|
||||
// Event stream — connection-aware daemon sentinel. The operation reports
|
||||
// connected only after the 9P open has completed, then retries slowly
|
||||
// after the daemon closes or rejects the operation.
|
||||
// Event stream — reads eventwait for server events.
|
||||
// Note: NinePConnection handles retry/reconnect but we use heartbeat for disconnect detection.
|
||||
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);
|
||||
|
|
@ -73,23 +71,19 @@ Ollie9pClient::Ollie9pClient(QObject *parent)
|
|||
if (!m_9p->isConnected()) {
|
||||
m_9p->connectDefault();
|
||||
}
|
||||
refreshSessions();
|
||||
ensureRootDataLoaded();
|
||||
// Restart streams for the active agent if we have one
|
||||
if (!m_activeSessionId.isEmpty() && !m_agentId.isEmpty()) {
|
||||
startActiveAgentStreams();
|
||||
}
|
||||
});
|
||||
connect(m_daemon, &NinePConnection::disconnected, this, [this]() {
|
||||
setDaemonConnected(false);
|
||||
if (m_9p) {
|
||||
m_9p->disconnect();
|
||||
}
|
||||
// Heartbeat will set daemonConnected on first successful ping
|
||||
heartbeat();
|
||||
});
|
||||
connect(m_daemon, &NinePConnection::readyRead, this, [this](const QByteArray &data) {
|
||||
handleEvent(QString::fromUtf8(data).trimmed());
|
||||
});
|
||||
|
||||
// Heartbeat timer — detect daemon death by periodic reads
|
||||
m_heartbeatTimer = new QTimer(this);
|
||||
m_heartbeatTimer->setInterval(2500); // 2.5 seconds
|
||||
connect(m_heartbeatTimer, &QTimer::timeout, this, &Ollie9pClient::heartbeat);
|
||||
m_heartbeatTimer->start();
|
||||
|
||||
refreshSessions();
|
||||
ensureRootDataLoaded();
|
||||
m_daemon->start(ollie9pBin(), {"-a", serverAddr(), "read", "--open-marker", "eventwait"});
|
||||
|
|
@ -97,6 +91,7 @@ Ollie9pClient::Ollie9pClient(QObject *parent)
|
|||
|
||||
Ollie9pClient::~Ollie9pClient()
|
||||
{
|
||||
if (m_heartbeatTimer) m_heartbeatTimer->stop();
|
||||
stopAgentStreams();
|
||||
if (m_daemon) m_daemon->stop();
|
||||
}
|
||||
|
|
@ -136,6 +131,45 @@ void Ollie9pClient::setDaemonConnected(bool connected)
|
|||
emit sessionsChanged();
|
||||
}
|
||||
|
||||
void Ollie9pClient::heartbeat()
|
||||
{
|
||||
// Try to read a cheap file to verify daemon is alive
|
||||
if (!m_9p) {
|
||||
m_9p = new Lib9pClient(this);
|
||||
}
|
||||
if (!m_9p->isConnected()) {
|
||||
m_9p->connectDefault();
|
||||
}
|
||||
|
||||
QByteArray result = m_9p->read(QStringLiteral("help"));
|
||||
bool alive = !result.isEmpty();
|
||||
|
||||
if (alive && !m_daemonConnected) {
|
||||
// Daemon came up — refresh state
|
||||
setDaemonConnected(true);
|
||||
refreshSessions();
|
||||
ensureRootDataLoaded();
|
||||
if (!m_activeSessionId.isEmpty() && !m_agentId.isEmpty()) {
|
||||
startActiveAgentStreams();
|
||||
}
|
||||
} else if (!alive && m_daemonConnected) {
|
||||
// Daemon died — clear state
|
||||
setDaemonConnected(false);
|
||||
stopAgentStreams();
|
||||
m_sessions.clear();
|
||||
m_activeSessionId.clear();
|
||||
m_agentId.clear();
|
||||
m_activeState = "idle";
|
||||
emit sessionsChanged();
|
||||
emit activeSessionIdChanged();
|
||||
emit activeAgentIdChanged();
|
||||
emit activeStateChanged();
|
||||
if (m_9p) {
|
||||
m_9p->disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString Ollie9pClient::sessionConnectionColor(const QString &sessionId) const
|
||||
{
|
||||
for (const QVariant &v : m_sessions) {
|
||||
|
|
@ -755,6 +789,7 @@ void Ollie9pClient::ensureRootDataLoaded()
|
|||
|
||||
void Ollie9pClient::startActiveAgentStreams()
|
||||
{
|
||||
if (!m_daemonConnected) return;
|
||||
if (m_activeSessionId.isEmpty() || m_agentId.isEmpty()) return;
|
||||
|
||||
const QString srv = serverAddr();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include <QVariantList>
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
#if QT_VERSION_MAJOR >= 6
|
||||
#include <QQmlEngine>
|
||||
#endif
|
||||
|
|
@ -110,10 +111,14 @@ private:
|
|||
void setDaemonConnected(bool connected);
|
||||
void startActiveAgentStreams();
|
||||
void handleEvent(const QString &eventLine);
|
||||
void heartbeat();
|
||||
|
||||
// Native 9P client (persistent connection, no subprocess overhead)
|
||||
Lib9pClient *m_9p = nullptr;
|
||||
|
||||
// Heartbeat timer — detects daemon death
|
||||
QTimer *m_heartbeatTimer = nullptr;
|
||||
|
||||
QVariantList m_sessions;
|
||||
QString m_activeSessionId;
|
||||
QString m_activeState = "idle";
|
||||
|
|
|
|||
Reference in New Issue