fix: make isConnected() mean connected, and stop the log claiming it

QRemoteObjectNode::connectToNode() returns false only when the URL
SCHEME is unregistered -- it never contacts the peer. Our registry URLs
are COMPUTED rather than discovered (logos_instance.h:
local:logos_<module>_<instanceId>), so they are identical whether or not
the module exists. Latching m_connected from that return therefore made
isConnected() answer "yes" for modules that were never loaded, which made
every `if (!client->isConnected()) return;` guard in the codebase DEAD
CODE.

Callers then paid a 20 s waitForSource per call, twice over, because the
token handshake tries capability_module first. Measured in Basecamp with
package_manager absent: ~417 s of blocked GUI thread on macOS and 361 s
on Linux before the window appeared, and over 900 s under load. Not a
Windows bug -- the Windows port merely exposed it.

isConnected() now also requires a listener at the endpoint. For `local:`
that is a direct socket / named-pipe probe, which costs microseconds
precisely in the case that used to cost 20 seconds; any other scheme
keeps its previous behaviour.

Two logging changes, because the diagnostics cost more than the defect:
"Successfully connected to registry" asserted a connection that often did
not exist and sent three separate investigations to the wrong place -- it
now says a connect attempt started and makes no claim about the peer.
And requestObject warns BEFORE a doomed wait instead of going silent for
20 s and then reporting failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dario Gabriel Lipicar
2026-08-07 00:11:59 -03:00
co-authored by Claude Opus 5
parent b928af0a70
commit 123831653a
2 changed files with 64 additions and 5 deletions
@@ -11,6 +11,7 @@
#include <QEventLoop>
#include <QDebug>
#include <QUrl>
#include <QLocalSocket>
#include <QMetaObject>
#include <QTime>
#include <QJsonArray>
@@ -571,9 +572,44 @@ bool RemoteTransportConnection::connectToHost()
return connectToRegistry();
}
bool RemoteTransportConnection::endpointHasListener() const
{
const QUrl url(m_registryUrl);
// Only `local:` can be probed cheaply and without side effects worth
// worrying about. Anything else keeps the previous semantics.
if (url.scheme() != QLatin1String("local"))
return true;
const QString serverName = url.path().isEmpty() ? url.host() : url.path();
if (serverName.isEmpty())
return true;
// A client connect is exactly what QtRO itself does, so a transient one is
// benign against a live registry, and against a dead endpoint it fails
// immediately -- ERROR_FILE_NOT_FOUND on Windows, ENOENT/ECONNREFUSED on
// Unix. That is the whole point: cost is microseconds when the answer is
// "no", which is the case that currently costs 20 seconds.
QLocalSocket probe;
probe.connectToServer(serverName, QIODevice::ReadOnly);
const bool alive = probe.waitForConnected(250);
probe.abort();
return alive;
}
bool RemoteTransportConnection::isConnected() const
{
return m_connected;
// m_connected alone is NOT an answer. QRemoteObjectNode::connectToNode()
// returns false only when the URL scheme is unregistered -- it never
// contacts the peer -- and our registry URLs are COMPUTED rather than
// discovered (logos_instance.h: local:logos_<module>_<instanceId>), so they
// are identical whether or not the module exists. Returning the raw latch
// made every `if (!client->isConnected()) return;` guard in the codebase
// dead code, and callers then paid a 20 s waitForSource per call against
// modules that were never loaded -- measured at ~417 s of blocked GUI
// thread in Basecamp on macOS, 361 s on Linux, before its window appeared.
if (!m_connected)
return false;
return endpointHasListener();
}
bool RemoteTransportConnection::reconnect()
@@ -609,13 +645,17 @@ bool RemoteTransportConnection::connectToRegistry()
if (success) {
m_connected = true;
qDebug() << "RemoteTransportConnection: Successfully connected to registry:" << m_registryUrl;
// Deliberately NOT "Successfully connected". connectToNode() only
// accepted the URL scheme; it never contacted a peer, and this line
// previously asserted a connection that frequently did not exist. That
// false claim sent three separate investigations to the wrong place --
// it cost considerably more than the bug it hid.
qDebug() << "RemoteTransportConnection: Registry connect attempt started (no peer contact yet):"
<< m_registryUrl;
} else {
m_connected = false;
qWarning() << "RemoteTransportConnection: Failed to connect to registry:" << m_registryUrl;
qWarning() << "RemoteTransportConnection: Registry URL scheme rejected:" << m_registryUrl;
}
qDebug() << "RemoteTransportConnection: Connected to registry at"
<< QTime::currentTime().toString("hh:mm:ss.zzz");
return m_connected;
}
@@ -627,6 +667,15 @@ LogosObject* RemoteTransportConnection::requestObject(const QString& objectName,
return nullptr;
}
// Warn BEFORE a doomed wait rather than after it. Without this the log went
// silent for the full timeout and then reported failure, which reads as a
// hang with no cause attached.
if (!endpointHasListener()) {
qWarning() << "RemoteTransportConnection: no listener at" << m_registryUrl
<< "-- request for" << objectName << "will block up to" << timeoutMs
<< "ms and then fail. Is the module loaded?";
}
qDebug() << "RemoteTransportConnection: Requesting object:" << objectName
<< "at" << QTime::currentTime().toString("hh:mm:ss.zzz");
@@ -41,6 +41,16 @@ private:
bool connectToRegistry();
QRemoteObjectNode* m_node;
// Does the registry endpoint currently have a listener?
//
// Separate from m_connected because connectToNode() cannot answer it: it
// returns false only for an unregistered URL SCHEME and never touches the
// peer, so m_connected records "we attempted a connection", nothing more.
// For `local:` URLs this probes the socket / named pipe directly; for any
// other scheme it returns true, leaving those transports' behaviour
// unchanged.
bool endpointHasListener() const;
QString m_registryUrl;
bool m_connected;
};