proper cleanup in RemoteLogosObject

This commit is contained in:
Dario Gabriel Lipicar
2026-04-21 12:17:12 -03:00
parent f7c855b110
commit 0ee7bdb55c
5 changed files with 534 additions and 195 deletions
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include <QCoreApplication>
#include <QEvent>
#include <QList>
#include <QObject>
#include <QPointer>
// RAII scope guard that swallows QEvent::DeferredDelete events application-wide
// while active, then re-posts them when it goes out of scope.
//
// Intended use: wrap any code path that spins a nested Qt event loop from
// inside a QML signal handler (typically a synchronous RPC via
// QConnectedReplicaImplementation::waitForSource). Without the guard, Qt's
// event dispatcher processes posted DeferredDelete events during the nested
// loop, and if any of those events destroys an ancestor of the QML item
// whose signal handler is on the call stack, QQmlData::destroyed aborts
// with:
//
// QQmlData::destroyed called qFatal — "Object destroyed while one of
// its QML signal handlers is in progress"
//
// The guard doesn't lose events — they're held in a QPointer<QObject> list
// and re-posted via deleteLater() when the guard is destroyed, so they
// land on the next event-loop iteration, after the outer signal handler
// has unwound.
//
// Scope: installed on QCoreApplication::instance(), so it sees posted
// events destined for main-thread objects only. That matches where this
// crash happens (QML is main-thread). Cost is one virtual eventFilter call
// per event during the guard's active window (milliseconds to seconds);
// negligible in practice.
//
// QPointer guards against the held target being deleted through another
// path while the guard is active — re-posting deleteLater on a null
// QPointer is a no-op.
class DeferredDeleteGuard : public QObject {
public:
DeferredDeleteGuard() {
if (auto* app = QCoreApplication::instance()) {
app->installEventFilter(this);
}
}
~DeferredDeleteGuard() override {
if (auto* app = QCoreApplication::instance()) {
app->removeEventFilter(this);
}
// Re-arm held deletions. Each call posts a fresh DeferredDelete
// that will be delivered on the next event-loop iteration, which
// — by the time we're being destructed — is after the caller's
// signal handler has returned.
for (const QPointer<QObject>& target : std::as_const(m_held)) {
if (target) target->deleteLater();
}
}
DeferredDeleteGuard(const DeferredDeleteGuard&) = delete;
DeferredDeleteGuard& operator=(const DeferredDeleteGuard&) = delete;
// For tests: number of DeferredDelete events held so far.
int heldCount() const { return static_cast<int>(m_held.size()); }
protected:
bool eventFilter(QObject* watched, QEvent* event) override {
if (event && event->type() == QEvent::DeferredDelete) {
m_held.append(QPointer<QObject>(watched));
return true; // swallow — will be re-posted on dtor
}
return false;
}
private:
QList<QPointer<QObject>> m_held;
};
@@ -0,0 +1,45 @@
#pragma once
#include "logos_object.h"
#include <QObject>
// Concrete LogosObject that communicates via Qt Remote Objects.
//
// The constructor takes the replica as QObject* so that this header does not
// pull in QRemoteObjects headers into every translation unit that instantiates
// the class. At runtime the pointer always points to a QRemoteObjectReplica.
//
// m_helper is also stored as QObject* for the same reason; the implementation
// in remote_transport.cpp casts it to RemoteEventHelper* where needed.
class RemoteLogosObject : public LogosObject {
public:
explicit RemoteLogosObject(QObject* replica);
~RemoteLogosObject() override;
QVariant callMethod(const QString& authToken,
const QString& methodName,
const QVariantList& args,
int timeoutMs) override;
void callMethodAsync(const QString& authToken,
const QString& methodName,
const QVariantList& args,
int timeoutMs,
AsyncResultCallback callback) override;
bool informModuleToken(const QString& authToken,
const QString& moduleName,
const QString& token,
int timeoutMs) override;
void onEvent(const QString& eventName, EventCallback callback) override;
void disconnectEvents() override;
void emitEvent(const QString& eventName, const QVariantList& data) override;
QJsonArray getMethods() override;
void release() override;
quintptr id() const override;
private:
QObject* m_replica;
QObject* m_helper;
};
+218 -194
View File
@@ -1,4 +1,6 @@
#include "remote_transport.h"
#include "remote_logos_object.h"
#include "../../deferred_delete_guard.h"
#include <QRemoteObjectRegistryHost>
#include <QRemoteObjectNode>
#include <QRemoteObjectReplica>
@@ -41,205 +43,215 @@ private:
} // anonymous namespace
class RemoteLogosObject : public LogosObject {
public:
explicit RemoteLogosObject(QObject* replica)
: m_replica(replica), m_helper(nullptr)
{
qDebug() << "[LogosObject] Created RemoteLogosObject wrapping QRemoteObjectReplica" << reinterpret_cast<quintptr>(replica);
}
// ── RemoteLogosObject method definitions ─────────────────────────────────────
~RemoteLogosObject() override {
qDebug() << "[LogosObject] Destroying RemoteLogosObject" << reinterpret_cast<quintptr>(m_replica);
delete m_helper;
}
RemoteLogosObject::RemoteLogosObject(QObject* replica)
: m_replica(replica), m_helper(nullptr)
{
qDebug() << "[LogosObject] Created RemoteLogosObject wrapping QRemoteObjectReplica" << reinterpret_cast<quintptr>(replica);
}
QVariant callMethod(const QString& authToken,
const QString& methodName,
const QVariantList& args,
int timeoutMs) override
{
if (!m_replica) {
qWarning() << "RemoteLogosObject: Cannot call method on null replica";
return QVariant();
}
qDebug() << "[LogosObject] RemoteLogosObject::callMethod" << methodName << "args:" << args.size();
QRemoteObjectPendingCall pendingCall;
bool success = QMetaObject::invokeMethod(
m_replica,
"callRemoteMethod",
Qt::DirectConnection,
Q_RETURN_ARG(QRemoteObjectPendingCall, pendingCall),
Q_ARG(QString, authToken),
Q_ARG(QString, methodName),
Q_ARG(QVariantList, args)
);
if (!success) {
qWarning() << "RemoteLogosObject: Failed to invoke callRemoteMethod on replica";
return QVariant();
}
pendingCall.waitForFinished(timeoutMs);
if (!pendingCall.isFinished() || pendingCall.error() != QRemoteObjectPendingCall::NoError) {
qWarning() << "RemoteLogosObject: callRemoteMethod failed or timed out:" << pendingCall.error();
return QVariant();
}
return pendingCall.returnValue();
}
void callMethodAsync(const QString& authToken,
const QString& methodName,
const QVariantList& args,
int timeoutMs,
AsyncResultCallback callback) override
{
if (!callback) return;
if (!m_replica) {
QTimer::singleShot(0, [callback]() { callback(QVariant()); });
return;
}
qDebug() << "[LogosObject] RemoteLogosObject::callMethodAsync" << methodName << "args:" << args.size();
QRemoteObjectPendingCall pendingCall;
bool success = QMetaObject::invokeMethod(
m_replica,
"callRemoteMethod",
Qt::DirectConnection,
Q_RETURN_ARG(QRemoteObjectPendingCall, pendingCall),
Q_ARG(QString, authToken),
Q_ARG(QString, methodName),
Q_ARG(QVariantList, args)
);
if (!success) {
qWarning() << "RemoteLogosObject: Failed to invoke callRemoteMethod on replica (async)";
QTimer::singleShot(0, [callback]() { callback(QVariant()); });
return;
}
auto* watcher = new QRemoteObjectPendingCallWatcher(pendingCall);
// Timeout timer -- parented to the watcher so it is auto-deleted
// when the watcher is destroyed, preventing late timeout callbacks.
auto* timer = new QTimer(watcher);
timer->setSingleShot(true);
// Success handler -- delivers result on the consumer's thread
QObject::connect(watcher, &QRemoteObjectPendingCallWatcher::finished,
watcher, [callback, timer](QRemoteObjectPendingCallWatcher* w) {
timer->stop(); // cancel timeout
QVariant result;
if (w->error() == QRemoteObjectPendingCall::NoError) {
result = w->returnValue();
} else {
qWarning() << "RemoteLogosObject: async callMethod error:" << w->error();
}
callback(result);
w->deleteLater();
}, Qt::QueuedConnection);
// Timeout handler -- stops the watcher and delivers empty result
QObject::connect(timer, &QTimer::timeout, watcher, [watcher, callback]() {
qWarning() << "RemoteLogosObject: async callMethod timed out";
callback(QVariant());
watcher->deleteLater(); // also destroys the timer (child)
});
timer->start(timeoutMs);
}
bool informModuleToken(const QString& authToken,
const QString& moduleName,
const QString& token,
int timeoutMs) override
{
if (!m_replica) {
qWarning() << "RemoteLogosObject: Cannot call informModuleToken on null replica";
return false;
}
QRemoteObjectPendingCall pendingCall;
bool success = QMetaObject::invokeMethod(
m_replica,
"informModuleToken",
Qt::DirectConnection,
Q_RETURN_ARG(QRemoteObjectPendingCall, pendingCall),
Q_ARG(QString, authToken),
Q_ARG(QString, moduleName),
Q_ARG(QString, token)
);
if (!success) {
qWarning() << "RemoteLogosObject: Failed to invoke informModuleToken on replica";
return false;
}
pendingCall.waitForFinished(timeoutMs);
if (!pendingCall.isFinished() || pendingCall.error() != QRemoteObjectPendingCall::NoError) {
qWarning() << "RemoteLogosObject: informModuleToken failed or timed out:" << pendingCall.error();
return false;
}
return pendingCall.returnValue().toBool();
}
void onEvent(const QString& eventName, EventCallback callback) override
{
if (!m_replica) return;
qDebug() << "[LogosObject] RemoteLogosObject::onEvent subscribing to event:" << eventName;
if (!m_helper) {
m_helper = new RemoteEventHelper();
QObject::connect(m_replica, SIGNAL(eventResponse(QString,QVariantList)),
m_helper, SLOT(onEventResponse(QString,QVariantList)));
qDebug() << "[LogosObject] RemoteLogosObject: connected EventHelper to QRemoteObjectReplica signals (IPC)";
}
m_helper->addCallback(eventName, std::move(callback));
}
void disconnectEvents() override
{
delete m_helper;
m_helper = nullptr;
}
void emitEvent(const QString& eventName, const QVariantList& data) override
{
if (!m_replica) return;
qDebug() << "[LogosObject] RemoteLogosObject::emitEvent" << eventName << "data:" << data.size() << "items (via IPC)";
QMetaObject::invokeMethod(m_replica, "eventResponse",
Qt::QueuedConnection,
Q_ARG(QString, eventName),
Q_ARG(QVariantList, data));
}
QJsonArray getMethods() override
{
// Remote introspection not implemented — callers should use
// the local module inspection tools (lm) instead.
return QJsonArray();
}
void release() override
{
disconnectEvents();
delete m_replica;
RemoteLogosObject::~RemoteLogosObject()
{
qDebug() << "[LogosObject] Destroying RemoteLogosObject" << reinterpret_cast<quintptr>(m_replica);
delete m_helper;
// Release the replica via deleteLater so QRemoteObjectNode's
// destroyed-signal wiring gets to deregister us from its internal
// name→replica map. Without this, every invokeRemoteMethod leaks
// a QRemoteObjectDynamicReplica on the node; the accumulated
// entries collide when a later acquireDynamic for the same name
// walks the map, and QRemoteObjectNodePrivate::setReplicaImplementation
// ends up dereferencing a stale vtable pointer.
if (m_replica) {
m_replica->deleteLater();
m_replica = nullptr;
delete this;
}
}
QVariant RemoteLogosObject::callMethod(const QString& authToken,
const QString& methodName,
const QVariantList& args,
int timeoutMs)
{
if (!m_replica) {
qWarning() << "RemoteLogosObject: Cannot call method on null replica";
return QVariant();
}
qDebug() << "[LogosObject] RemoteLogosObject::callMethod" << methodName << "args:" << args.size();
QRemoteObjectPendingCall pendingCall;
bool success = QMetaObject::invokeMethod(
m_replica,
"callRemoteMethod",
Qt::DirectConnection,
Q_RETURN_ARG(QRemoteObjectPendingCall, pendingCall),
Q_ARG(QString, authToken),
Q_ARG(QString, methodName),
Q_ARG(QVariantList, args)
);
if (!success) {
qWarning() << "RemoteLogosObject: Failed to invoke callRemoteMethod on replica";
return QVariant();
}
quintptr id() const override { return reinterpret_cast<quintptr>(m_replica); }
pendingCall.waitForFinished(timeoutMs);
private:
QObject* m_replica;
RemoteEventHelper* m_helper;
};
if (!pendingCall.isFinished() || pendingCall.error() != QRemoteObjectPendingCall::NoError) {
qWarning() << "RemoteLogosObject: callRemoteMethod failed or timed out:" << pendingCall.error();
return QVariant();
}
return pendingCall.returnValue();
}
void RemoteLogosObject::callMethodAsync(const QString& authToken,
const QString& methodName,
const QVariantList& args,
int timeoutMs,
AsyncResultCallback callback)
{
if (!callback) return;
if (!m_replica) {
QTimer::singleShot(0, [callback]() { callback(QVariant()); });
return;
}
qDebug() << "[LogosObject] RemoteLogosObject::callMethodAsync" << methodName << "args:" << args.size();
QRemoteObjectPendingCall pendingCall;
bool success = QMetaObject::invokeMethod(
m_replica,
"callRemoteMethod",
Qt::DirectConnection,
Q_RETURN_ARG(QRemoteObjectPendingCall, pendingCall),
Q_ARG(QString, authToken),
Q_ARG(QString, methodName),
Q_ARG(QVariantList, args)
);
if (!success) {
qWarning() << "RemoteLogosObject: Failed to invoke callRemoteMethod on replica (async)";
QTimer::singleShot(0, [callback]() { callback(QVariant()); });
return;
}
auto* watcher = new QRemoteObjectPendingCallWatcher(pendingCall);
// Timeout timer -- parented to the watcher so it is auto-deleted
// when the watcher is destroyed, preventing late timeout callbacks.
auto* timer = new QTimer(watcher);
timer->setSingleShot(true);
// Success handler -- delivers result on the consumer's thread
QObject::connect(watcher, &QRemoteObjectPendingCallWatcher::finished,
watcher, [callback, timer](QRemoteObjectPendingCallWatcher* w) {
timer->stop(); // cancel timeout
QVariant result;
if (w->error() == QRemoteObjectPendingCall::NoError) {
result = w->returnValue();
} else {
qWarning() << "RemoteLogosObject: async callMethod error:" << w->error();
}
callback(result);
w->deleteLater();
}, Qt::QueuedConnection);
// Timeout handler -- stops the watcher and delivers empty result
QObject::connect(timer, &QTimer::timeout, watcher, [watcher, callback]() {
qWarning() << "RemoteLogosObject: async callMethod timed out";
callback(QVariant());
watcher->deleteLater(); // also destroys the timer (child)
});
timer->start(timeoutMs);
}
bool RemoteLogosObject::informModuleToken(const QString& authToken,
const QString& moduleName,
const QString& token,
int timeoutMs)
{
if (!m_replica) {
qWarning() << "RemoteLogosObject: Cannot call informModuleToken on null replica";
return false;
}
QRemoteObjectPendingCall pendingCall;
bool success = QMetaObject::invokeMethod(
m_replica,
"informModuleToken",
Qt::DirectConnection,
Q_RETURN_ARG(QRemoteObjectPendingCall, pendingCall),
Q_ARG(QString, authToken),
Q_ARG(QString, moduleName),
Q_ARG(QString, token)
);
if (!success) {
qWarning() << "RemoteLogosObject: Failed to invoke informModuleToken on replica";
return false;
}
pendingCall.waitForFinished(timeoutMs);
if (!pendingCall.isFinished() || pendingCall.error() != QRemoteObjectPendingCall::NoError) {
qWarning() << "RemoteLogosObject: informModuleToken failed or timed out:" << pendingCall.error();
return false;
}
return pendingCall.returnValue().toBool();
}
void RemoteLogosObject::onEvent(const QString& eventName, EventCallback callback)
{
if (!m_replica) return;
qDebug() << "[LogosObject] RemoteLogosObject::onEvent subscribing to event:" << eventName;
if (!m_helper) {
m_helper = new RemoteEventHelper();
QObject::connect(m_replica, SIGNAL(eventResponse(QString,QVariantList)),
m_helper, SLOT(onEventResponse(QString,QVariantList)));
qDebug() << "[LogosObject] RemoteLogosObject: connected EventHelper to QRemoteObjectReplica signals (IPC)";
}
static_cast<RemoteEventHelper*>(m_helper)->addCallback(eventName, std::move(callback));
}
void RemoteLogosObject::disconnectEvents()
{
delete m_helper;
m_helper = nullptr;
}
void RemoteLogosObject::emitEvent(const QString& eventName, const QVariantList& data)
{
if (!m_replica) return;
qDebug() << "[LogosObject] RemoteLogosObject::emitEvent" << eventName << "data:" << data.size() << "items (via IPC)";
QMetaObject::invokeMethod(m_replica, "eventResponse",
Qt::QueuedConnection,
Q_ARG(QString, eventName),
Q_ARG(QVariantList, data));
}
QJsonArray RemoteLogosObject::getMethods()
{
// Remote introspection not implemented — callers should use
// the local module inspection tools (lm) instead.
return QJsonArray();
}
void RemoteLogosObject::release()
{
disconnectEvents();
delete m_replica;
m_replica = nullptr;
delete this;
}
quintptr RemoteLogosObject::id() const
{
return reinterpret_cast<quintptr>(m_replica);
}
// ── RemoteTransportHost ──────────────────────────────────────────────────────
@@ -356,6 +368,14 @@ LogosObject* RemoteTransportConnection::requestObject(const QString& objectName,
qDebug() << "RemoteTransportConnection: Requesting object:" << objectName
<< "at" << QTime::currentTime().toString("hh:mm:ss.zzz");
// The DeferredDeleteGuard covers both acquireDynamic() and
// waitForSource() — either one can spin a nested event loop, and we
// must hold DeferredDelete events for the full duration so a QML
// delegate rebuild during the sync wait can't destroy the Button
// whose onClicked is currently on the call stack. See the guard
// definition above for the full rationale.
DeferredDeleteGuard guard;
QRemoteObjectReplica* replica = m_node->acquireDynamic(objectName);
if (!replica) {
qWarning() << "RemoteTransportConnection: Failed to acquire replica for:" << objectName;
@@ -364,7 +384,11 @@ LogosObject* RemoteTransportConnection::requestObject(const QString& objectName,
if (!replica->waitForSource(timeoutMs)) {
qWarning() << "RemoteTransportConnection: Timeout waiting for replica:" << objectName;
delete replica;
// deleteLater, not delete — raw-deleting a replica that the
// QRemoteObjectNode still tracks leaves a dangling entry in the
// node's name→replica map and corrupts future acquireDynamic calls
// for the same name. See RemoteLogosObject dtor.
replica->deleteLater();
return nullptr;
}