mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 09:41:06 +00:00
proper cleanup in RemoteLogosObject
This commit is contained in:
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../cpp ${CMAKE_CURRENT_BINARY_DIR}/logos_sdk)
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../cpp-generator ${CMAKE_CURRENT_BINARY_DIR}/logos_generator)
|
||||
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test Qml)
|
||||
|
||||
# --- Generate dispatch code for the sample provider fixture ---
|
||||
set(SAMPLE_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/sample_provider.h)
|
||||
@@ -40,6 +40,7 @@ add_executable(sdk_tests
|
||||
test_async_calls.cpp
|
||||
test_provider_dispatch.cpp
|
||||
test_std_string_overloads.cpp
|
||||
test_qml_sync_rpc_crash.cpp
|
||||
fixtures/sample_provider.cpp
|
||||
${GENERATED_DISPATCH}
|
||||
)
|
||||
@@ -47,6 +48,7 @@ add_executable(sdk_tests
|
||||
target_include_directories(sdk_tests PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/fixtures
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../cpp
|
||||
${GENERATED_DIR}
|
||||
)
|
||||
|
||||
@@ -54,8 +56,60 @@ target_link_libraries(sdk_tests PRIVATE
|
||||
logos_sdk
|
||||
GTest::gtest
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::Qml
|
||||
Qt${QT_VERSION_MAJOR}::RemoteObjects
|
||||
Qt${QT_VERSION_MAJOR}::Test
|
||||
)
|
||||
|
||||
# The QML integration tests (QmlSyncRpcCrashTest) need to locate Qt's
|
||||
# bundled QML modules (QtQml.Base) at runtime. Under nix the default
|
||||
# QmlImportsPath isn't always propagated into unwrapped test binaries,
|
||||
# so we discover the directory at configure time from the qtdeclarative
|
||||
# install root (derivable from Qt6Qml_DIR) and pass it to the test as a
|
||||
# compile definition. The test then calls engine.addImportPath() with it.
|
||||
#
|
||||
# Qt6Qml_DIR typically points at
|
||||
# <qtdeclarative-root>/lib/cmake/Qt6Qml
|
||||
# On nix/Linux the QML imports are at
|
||||
# <qtdeclarative-root>/lib/qt-6/qml
|
||||
# On macOS (framework builds) the same layout is used — qtdeclarative
|
||||
# also ships lib/qt-6/qml alongside the frameworks.
|
||||
set(_qt_qml_import_path "")
|
||||
if(DEFINED Qt${QT_VERSION_MAJOR}Qml_DIR)
|
||||
get_filename_component(_qtdecl_root
|
||||
"${Qt${QT_VERSION_MAJOR}Qml_DIR}/../../.." ABSOLUTE)
|
||||
foreach(_candidate
|
||||
"${_qtdecl_root}/lib/qt-6/qml"
|
||||
"${_qtdecl_root}/qml"
|
||||
"${_qtdecl_root}/lib/qml")
|
||||
if(EXISTS "${_candidate}/QtQml")
|
||||
set(_qt_qml_import_path "${_candidate}")
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# Fallback: derive from the Qt6::Qml library path by walking up.
|
||||
if(NOT _qt_qml_import_path)
|
||||
get_target_property(_qt_qml_lib Qt${QT_VERSION_MAJOR}::Qml LOCATION)
|
||||
set(_probe "${_qt_qml_lib}")
|
||||
foreach(_i RANGE 10)
|
||||
get_filename_component(_probe "${_probe}" DIRECTORY)
|
||||
if(EXISTS "${_probe}/lib/qt-6/qml/QtQml")
|
||||
set(_qt_qml_import_path "${_probe}/lib/qt-6/qml")
|
||||
break()
|
||||
endif()
|
||||
if(EXISTS "${_probe}/qml/QtQml")
|
||||
set(_qt_qml_import_path "${_probe}/qml")
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
message(STATUS "sdk_tests QML import path: ${_qt_qml_import_path}")
|
||||
|
||||
target_compile_definitions(sdk_tests PRIVATE
|
||||
QT_QML_TEST_IMPORT_PATH="${_qt_qml_import_path}"
|
||||
)
|
||||
|
||||
gtest_discover_tests(sdk_tests)
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Integration test for the DeferredDeleteGuard fix in RemoteLogosObject.
|
||||
//
|
||||
// Root cause being guarded against:
|
||||
// A QML signal handler calls a C++ slot that transitively spins a nested
|
||||
// QEventLoop (via QRemoteObjectReplica::waitForSource). If a deleteLater
|
||||
// was posted on an ancestor of the handler's QML object before the nested
|
||||
// loop started, Qt delivers the DeferredDelete inside the nested loop and
|
||||
// destroys the ancestor while the signal handler is still on the call stack.
|
||||
// QQmlData::destroyed detects this and aborts with:
|
||||
//
|
||||
// "Object destroyed while one of its QML signal handlers is in progress"
|
||||
//
|
||||
// The fix: wrap acquireDynamic + waitForSource in a DeferredDeleteGuard.
|
||||
// The guard intercepts all DeferredDelete events for the duration of the
|
||||
// nested loop and re-posts them after it exits, so they fire only once the
|
||||
// outer signal handler has fully returned.
|
||||
//
|
||||
// What this test proves: a QML signal handler can post deleteLater on its
|
||||
// own root and then call into code that spins a nested event loop (exactly
|
||||
// as RemoteTransportConnection::requestObject does in production) without
|
||||
// triggering the qFatal. Removing the DeferredDeleteGuard line from Bridge
|
||||
// below reproduces the original crash.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QLibraryInfo>
|
||||
#include <QObject>
|
||||
#include <QQmlComponent>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
#include <QRemoteObjectDynamicReplica>
|
||||
#include <QRemoteObjectHost>
|
||||
#include <QRemoteObjectNode>
|
||||
#include <QString>
|
||||
#include <QTest>
|
||||
#include <QUrl>
|
||||
|
||||
#include "deferred_delete_guard.h"
|
||||
#include "implementations/qt_remote/remote_logos_object.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Q_OBJECT helpers — must live at namespace scope for moc.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Minimal source exposed over QRemoteObjects. The method body is irrelevant;
|
||||
// we only need the acquireDynamic + waitForSource handshake to spin a nested
|
||||
// event loop.
|
||||
class MinimalSource : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
Q_INVOKABLE QString ping() { return QStringLiteral("pong"); }
|
||||
};
|
||||
|
||||
// Bridge invoked from inside a QML signal handler.
|
||||
//
|
||||
// Simulates the production crash path: the handler posts deleteLater on its
|
||||
// own root object (ancestor), then acquires a RemoteLogosObject — which
|
||||
// internally calls waitForSource and spins a nested event loop.
|
||||
//
|
||||
// DeferredDeleteGuard is the fix. Without it, the DeferredDelete for ancestor
|
||||
// fires inside the nested loop, destroying a QML-bound object while its
|
||||
// signal handler is still on the call stack and causing a qFatal. With the
|
||||
// guard the delete is swallowed for the duration of the nested loop and
|
||||
// re-posted immediately after, so it fires safely outside the handler.
|
||||
class Bridge : public QObject {
|
||||
Q_OBJECT
|
||||
QRemoteObjectNode* m_node;
|
||||
public:
|
||||
explicit Bridge(QRemoteObjectNode* node, QObject* parent = nullptr)
|
||||
: QObject(parent), m_node(node) {}
|
||||
|
||||
Q_INVOKABLE void triggerCrashScenario(QObject* ancestor) {
|
||||
if (ancestor) ancestor->deleteLater();
|
||||
|
||||
DeferredDeleteGuard guard; // ← remove this to reproduce the crash
|
||||
auto* replica = m_node->acquireDynamic(QStringLiteral("test_source"));
|
||||
if (replica) {
|
||||
replica->waitForSource(1000); // spins nested event loop
|
||||
RemoteLogosObject wrapped(replica);
|
||||
// wrapped.~RemoteLogosObject() calls replica->deleteLater(),
|
||||
// also intercepted by the guard still in scope above.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(QmlSyncRpcCrashTest, GuardPreventsQFatalDuringQmlSignalHandler) {
|
||||
const QString url = QStringLiteral("local:ddg_test");
|
||||
QRemoteObjectHost host(QUrl{url});
|
||||
MinimalSource source;
|
||||
host.enableRemoting(&source, QStringLiteral("test_source"));
|
||||
|
||||
QRemoteObjectNode node;
|
||||
node.connectToNode(QUrl{url});
|
||||
QTest::qWait(100); // let socket negotiation settle
|
||||
|
||||
Bridge bridge(&node);
|
||||
QQmlEngine engine;
|
||||
|
||||
// Ensure Qt's bundled QML modules are findable. Under nix the default
|
||||
// QmlImportsPath isn't always propagated into unwrapped test binaries;
|
||||
// we inject the path discovered at configure time via CMake.
|
||||
#ifdef QT_QML_TEST_IMPORT_PATH
|
||||
engine.addImportPath(QStringLiteral(QT_QML_TEST_IMPORT_PATH));
|
||||
#endif
|
||||
engine.addImportPath(QLibraryInfo::path(QLibraryInfo::QmlImportsPath));
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("bridge"), &bridge);
|
||||
|
||||
QQmlComponent component(&engine);
|
||||
component.setData(R"QML(
|
||||
import QtQml 2.0
|
||||
QtObject {
|
||||
id: root
|
||||
signal kick()
|
||||
onKick: { bridge.triggerCrashScenario(root) }
|
||||
}
|
||||
)QML", QUrl());
|
||||
ASSERT_TRUE(component.isReady()) << component.errorString().toStdString();
|
||||
|
||||
QObject* root = component.create();
|
||||
ASSERT_NE(root, nullptr) << component.errorString().toStdString();
|
||||
|
||||
// Emitting kick() marks root as having an active QML signal handler.
|
||||
// The handler calls Bridge::triggerCrashScenario, which posts
|
||||
// deleteLater on root and then spins a nested event loop. Without
|
||||
// DeferredDeleteGuard the process would qFatal here; with it we reach
|
||||
// the line below without crashing.
|
||||
QMetaObject::invokeMethod(root, "kick", Qt::DirectConnection);
|
||||
|
||||
SUCCEED(); // reaching this line is the positive result
|
||||
|
||||
// Drain re-posted deferred deletes so teardown is clean.
|
||||
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
||||
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
||||
}
|
||||
|
||||
#include "test_qml_sync_rpc_crash.moc"
|
||||
Reference in New Issue
Block a user