mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 20:11:07 +00:00
* fix(startup): publish a token-only handshake surface before a module initializes A module's initializer is synchronous and routinely calls out — a Qt module's initLogos, a cdylib's context-ready hook — including capability_module's requestModule, which capability answers by pushing a token back to that same module. The module's business object is published only once the initializer returns, so that push had nothing to reach: capability waited for a source that could not appear until the initializer returned, and the initializer could not return until capability answered. On Linux this wedged UI startup until the standalone app's 10s ui-host deadline expired and the view never rendered. Adds a second, deliberately tiny surface — ModuleHandshakeProxy, published under logos::handshakeObjectName(name) — carrying informModuleToken and nothing else. It forwards to the ModuleProxy that owns the token store, so a grant delivered early is the one the business object honours later, with the same authorization. The business object's publish timing is UNCHANGED, which is the point: a caller of a real method still blocks at acquire until the module is genuinely ready, exactly as it always has. An earlier attempt published the business object early and refused calls during init; that quietly turned a call that used to wait and succeed into one that returned empty, which old consumers cannot even detect. informModuleToken_module now tries the handshake surface first (short probe) and falls back to the business object, so modules built before this surface existed are reached exactly as they are today. It also reuses the cached handle instead of acquiring a fresh replica per grant, and takes a timeout (default unchanged). No wire change, no ABI change, no reply-shape change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(startup): do not treat a handshake refusal as the final answer The handshake surface is published before the target's initializer runs, so a target whose token store is seeded BY that initializer refuses a push that arrives first. Returning that refusal to the caller handed it an empty grant it could not distinguish from a real denial: measured on Linux, the first requestModule for wallet_backend_module came back empty in 29 of 34 runs, and never once in the pre-surface baseline. Fall through to the business object instead, which is what the caller got before this surface existed. The business object is published only once the initializer has returned, by which point the store is populated. The wait is bounded by the caller's own budget -- capability_module passes 3000 ms, not the 20 s default that made the original deadlock fatal -- so this cannot reintroduce the wedge. The companion change in logos-qt-sdk seeds the trust anchor before publishing, which removes the refusal at its source; this is the safety net for hosts and modules that do not. Also adds the regression test that would have caught this: the existing case seeds "core" before pushing, which is exactly the state that does NOT hold in the window the surface covers, so it asserted the surface works under a precondition production never met. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(startup): marshal the token push, and stop re-probing a missing handshake Two review findings from Copilot, both verified against the code before acting. 1. Thread affinity. informModuleToken_module was one of only two entry points in LogosAPIClient that did not wrap in logos::runOnOwnerThread -- requestObject, both invokeRemoteMethod forms and onEvent all do. The missing marshal is inherited, but THIS change is what made it reachable: the method used to take an uncached requestObject() + release() and touch no shared state, and routing it through acquireCachedObject put it on m_objectCache, which is declared single-threaded and holds thread-affine QtRO handles. Now marshalled, matching its four siblings. The 3-arg informModuleToken has the same gap but still uses an uncached handle and predates this work, so it is deliberately left alone rather than widened into this fix; noted at the call site. 2. No negative cache on the handshake probe. acquireCachedObject caches successes only, so a module built before the handshake surface existed failed the probe on EVERY grant -- and on QtRO that failure is a blocking waitForSource, i.e. 250 ms of dead time per token, forever. Remember the absence and go straight to the business object; cleared by clearObjectCache() so a reconnect, or a module reloaded from a build that has the surface, is re-probed rather than written off permanently. (The review attributed this cost to the Local/Plain adapters rejecting a non-ModuleProxy object. Checked per transport: plain is unaffected -- its token push is nameless fire-and-forget and it never had the acquire deadlock -- and on qt_local requestObject ignores timeoutMs entirely, so the cost there is a spurious warning, not 250 ms. The real cost is the missing negative cache, on QtRO.) The same review's ABI-break and name-collision findings were measured and do not apply: logos_protocol is a static archive with zero undefined imports of these symbols anywhere in the built stack, and object names are scoped to a per-module socket rather than a global registry. Both answered in-thread. 290/290 protocol tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(startup): exercise the handshake surface over a real transport The existing handshake cases call ModuleHandshakeProxy directly, with no transport underneath. That is what let a whole class of defect through: the surface is only useful if a transport will PUBLISH a token-only QObject and a consumer can ACQUIRE it by the derived name, and a direct-call test can see neither half. The adapter survey prompted by review found qt_local silently rejects a non-ModuleProxy on acquire while still reporting a successful publish -- invisible to every test in the suite. These run on the transport the production stack actually uses (QtRO, the LogosTransportConfig default), and model the startup window honestly: the handshake object is published and the business object deliberately is NOT, because it does not exist until the initializer returns. That window is the entire reason the surface exists and is the one state the direct-call tests could never represent. TokenReachesAModuleWhoseBusinessObjectIsNotPublishedYet the pre-init window end to end: publish -> probe by derived name -> acquire -> push lands on the provider. AnUnseededAnchorRefusesEvenThoughTheSurfaceIsReachable the transport-level twin of the gate test: proves the refusal measured in production (29 of 34 app runs) is the gate rejecting the push, not the transport failing to deliver it -- the provider is never reached. ALegacyModuleFallsBackAndIsNotReProbed a module with no handshake surface still gets its token, and the missing surface is probed ONCE. Timed rather than functional, so it was falsified before being trusted: with the negative cache removed the suite fails on exactly this case and no other. 293/293 protocol tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
486 lines
23 KiB
C++
486 lines
23 KiB
C++
#include "logos_api_client.h"
|
|
#include "logos_api_consumer.h"
|
|
#include "logos_object.h"
|
|
#include "logos_types.h"
|
|
#include "logos_json_convert.h"
|
|
#include "logos_thread_marshal.h"
|
|
#include "logos_rpc_status.h"
|
|
#include "token_manager.h"
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QJsonValue>
|
|
#include <QMetaObject>
|
|
#include <QMetaType>
|
|
#include <QPointer>
|
|
#include <string>
|
|
|
|
using logos::qvariantToNlohmann;
|
|
using logos::nlohmannArgsToQVariantList;
|
|
|
|
LogosAPIClient::LogosAPIClient(const QString& module_to_talk_to,
|
|
const QString& origin_module,
|
|
TokenManager* token_manager,
|
|
const LogosTransportConfig& target_transport,
|
|
const LogosTransportConfig& capability_transport,
|
|
QObject *parent)
|
|
: QObject(parent)
|
|
, m_consumer(new LogosAPIConsumer(module_to_talk_to, origin_module,
|
|
token_manager, target_transport, this))
|
|
, m_token_manager(token_manager)
|
|
, m_origin_module(origin_module)
|
|
// Pre-build the capability_module consumer once. We skip it for
|
|
// the capability_module client itself — the auto-`requestModule`
|
|
// path is gated by `objectName != "capability_module"` so we'd
|
|
// never use it, and constructing one would be a redundant
|
|
// self-connection. Init-list order matches the declaration order
|
|
// in the header — `m_capability_consumer` is appended at the end
|
|
// for ABI stability (see header comment).
|
|
, m_capability_consumer(module_to_talk_to == QStringLiteral("capability_module")
|
|
? nullptr
|
|
: new LogosAPIConsumer(QStringLiteral("capability_module"),
|
|
origin_module, token_manager,
|
|
capability_transport, this))
|
|
{
|
|
}
|
|
|
|
LogosAPIClient::LogosAPIClient(const QString& module_to_talk_to,
|
|
const QString& origin_module,
|
|
TokenManager* token_manager,
|
|
QObject *parent)
|
|
: LogosAPIClient(module_to_talk_to, origin_module, token_manager,
|
|
LogosTransportConfigGlobal::getDefault(),
|
|
LogosTransportConfigGlobal::getDefault(), parent)
|
|
{
|
|
}
|
|
|
|
LogosAPIClient::~LogosAPIClient()
|
|
{
|
|
}
|
|
|
|
LogosObject* LogosAPIClient::requestObject(const QString& objectName, Timeout timeout)
|
|
{
|
|
// Marshal to the owner thread: the replica is acquired and lives there.
|
|
return logos::runOnOwnerThread(this, [&]() -> LogosObject* {
|
|
return m_consumer->requestObject(objectName, timeout);
|
|
});
|
|
}
|
|
|
|
bool LogosAPIClient::isConnected() const
|
|
{
|
|
return m_consumer->isConnected();
|
|
}
|
|
|
|
QString LogosAPIClient::registryUrl() const
|
|
{
|
|
return m_consumer->registryUrl();
|
|
}
|
|
|
|
bool LogosAPIClient::reconnect()
|
|
{
|
|
return m_consumer->reconnect();
|
|
}
|
|
|
|
QVariant LogosAPIClient::invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariantList& args, Timeout timeout)
|
|
{
|
|
return invokeRemoteMethod(objectName, methodName, args, timeout, nullptr);
|
|
}
|
|
|
|
QVariant LogosAPIClient::invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariantList& args, Timeout timeout, logos::CallError* err)
|
|
{
|
|
if (err) err->clear();
|
|
// Marshal the whole operation (capability/token fetch + the call) onto the
|
|
// owner thread so a worker thread (e.g. an HTTP handler) can call other
|
|
// modules. Same-thread callers run directly. See logos_thread_marshal.h.
|
|
return logos::runOnOwnerThread(this, [&]() -> QVariant {
|
|
qDebug() << "LogosAPIClient: invoking remote method" << objectName << methodName << "args_count:" << args.size();
|
|
|
|
const bool eligible = objectName != QStringLiteral("capability_module") && m_capability_consumer;
|
|
|
|
QString token = getToken(objectName);
|
|
if (token.isEmpty() && eligible)
|
|
token = mintAndCacheToken(objectName); // first exchange (cached for later calls)
|
|
|
|
QVariant result = m_consumer->invokeRemoteMethod(token, objectName, methodName, args, timeout, err);
|
|
|
|
// Re-exchange on rejection, once. The provider rejected our (stale) token —
|
|
// drop it, mint a fresh one via capability_module, and retry the call. Gated
|
|
// on the explicit provider sentinel (never a plain empty result), so it can't
|
|
// loop, can't misfire on a legitimately-empty return, and never fires against
|
|
// an old provider (which returns a bare QVariant() we don't match). This also
|
|
// lazily recovers the common provider-reload case. See logos_rpc_status.h.
|
|
if (eligible && logos::isUnauthorizedSentinel(result)) {
|
|
qWarning() << "LogosAPIClient: token for" << objectName
|
|
<< "rejected by provider; re-exchanging and retrying once";
|
|
m_token_manager->removeToken(objectName);
|
|
const QString fresh = mintAndCacheToken(objectName);
|
|
if (!fresh.isEmpty())
|
|
result = m_consumer->invokeRemoteMethod(fresh, objectName, methodName, args, timeout, err);
|
|
}
|
|
|
|
// Never surface the sentinel to the typed wrapper. If we still hold it the
|
|
// retry failed (capability down / provider truly gone): collapse to today's
|
|
// empty result, and for NEW callers set a distinguishable CallError.
|
|
if (logos::isUnauthorizedSentinel(result)) {
|
|
if (err) {
|
|
err->code = "unauthorized";
|
|
err->message = "call to '" + objectName.toStdString()
|
|
+ "' rejected: token not recognized (re-exchange failed)";
|
|
err->origin = objectName.toStdString();
|
|
}
|
|
return QVariant();
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
|
|
QString LogosAPIClient::mintAndCacheToken(const QString& objectName)
|
|
{
|
|
qDebug() << "LogosAPIClient: calling requestModule for" << objectName;
|
|
const QString capabilityToken = getToken(QStringLiteral("capability_module"));
|
|
const QString token = QString::fromStdString(
|
|
m_capability_consumer->requestModule(capabilityToken.toStdString(),
|
|
m_origin_module.toStdString(),
|
|
objectName.toStdString()));
|
|
qDebug() << "LogosAPIClient: requestModule result for" << objectName << ":" << token;
|
|
// Cache the minted token so subsequent calls skip the handshake — closes the
|
|
// token-rotation race where overlapping requestModule calls mint fresh tokens
|
|
// that overwrite each other at the target (e.g. QtRO's sync wait reentering
|
|
// via a nested event loop).
|
|
if (!token.isEmpty())
|
|
m_token_manager->saveToken(objectName, token);
|
|
return token;
|
|
}
|
|
|
|
QVariant LogosAPIClient::invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg, Timeout timeout)
|
|
{
|
|
return invokeRemoteMethod(objectName, methodName, QVariantList() << arg, timeout);
|
|
}
|
|
|
|
QVariant LogosAPIClient::invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, Timeout timeout)
|
|
{
|
|
return invokeRemoteMethod(objectName, methodName, QVariantList() << arg1 << arg2, timeout);
|
|
}
|
|
|
|
QVariant LogosAPIClient::invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, Timeout timeout)
|
|
{
|
|
return invokeRemoteMethod(objectName, methodName, QVariantList() << arg1 << arg2 << arg3, timeout);
|
|
}
|
|
|
|
QVariant LogosAPIClient::invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, Timeout timeout)
|
|
{
|
|
return invokeRemoteMethod(objectName, methodName, QVariantList() << arg1 << arg2 << arg3 << arg4, timeout);
|
|
}
|
|
|
|
QVariant LogosAPIClient::invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, const QVariant& arg5, Timeout timeout)
|
|
{
|
|
return invokeRemoteMethod(objectName, methodName, QVariantList() << arg1 << arg2 << arg3 << arg4 << arg5, timeout);
|
|
}
|
|
|
|
void LogosAPIClient::invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariantList& args, AsyncResultCallback callback,
|
|
Timeout timeout)
|
|
{
|
|
// Delegate to the CallError-aware overload; legacy callers just drop the
|
|
// error field. Keeps the handshake-coalescing logic single-sourced.
|
|
invokeRemoteMethodAsync(objectName, methodName, args,
|
|
[cb = std::move(callback)](QVariant r, const logos::CallError&) mutable {
|
|
if (cb) cb(std::move(r));
|
|
},
|
|
timeout);
|
|
}
|
|
|
|
void LogosAPIClient::invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariantList& args, AsyncResultErrorCallback callback,
|
|
Timeout timeout)
|
|
{
|
|
// Public entry: grant one retry for the rejection-driven re-exchange.
|
|
invokeRemoteMethodAsyncImpl(objectName, methodName, args, std::move(callback), timeout, /*retriesLeft=*/1);
|
|
}
|
|
|
|
void LogosAPIClient::invokeRemoteMethodAsyncImpl(const QString& objectName, const QString& methodName,
|
|
const QVariantList& args, AsyncResultErrorCallback callback,
|
|
Timeout timeout, int retriesLeft)
|
|
{
|
|
if (!callback) return;
|
|
|
|
// The async path acquires a replica too, so it must also run on the owner
|
|
// thread. Unlike the sync path we post non-blocking (QueuedConnection): the
|
|
// worker caller returns immediately and the result callback fires on the
|
|
// owner thread when the reply arrives. Preserve retriesLeft across the hop.
|
|
if (QThread::currentThread() != this->thread()) {
|
|
QMetaObject::invokeMethod(this,
|
|
[this, objectName, methodName, args,
|
|
callback = std::move(callback), timeout, retriesLeft]() mutable {
|
|
invokeRemoteMethodAsyncImpl(objectName, methodName, args,
|
|
std::move(callback), timeout, retriesLeft);
|
|
},
|
|
Qt::QueuedConnection);
|
|
return;
|
|
}
|
|
|
|
const bool eligible = objectName != QStringLiteral("capability_module") && m_capability_consumer;
|
|
|
|
// Wrap the user callback so a provider rejection sentinel triggers one
|
|
// re-exchange + retry, and the sentinel is never surfaced to the caller.
|
|
// Mirrors the sync path's retry in logos_api_client.cpp's invokeRemoteMethod.
|
|
QPointer<LogosAPIClient> selfGuard(this);
|
|
AsyncResultErrorCallback onResult =
|
|
[this, selfGuard, objectName, methodName, args, timeout, retriesLeft, cb = std::move(callback)]
|
|
(QVariant result, const logos::CallError& err) mutable {
|
|
if (!selfGuard) return; // client destroyed mid-flight: drop
|
|
if (retriesLeft > 0 && objectName != QStringLiteral("capability_module")
|
|
&& m_capability_consumer && logos::isUnauthorizedSentinel(result)) {
|
|
qWarning() << "LogosAPIClient: token for" << objectName
|
|
<< "rejected by provider (async); re-exchanging and retrying once";
|
|
m_token_manager->removeToken(objectName);
|
|
// Token is empty now → the re-entry coalesces the retry through the
|
|
// same m_pendingHandshakes machinery, so a burst of concurrent
|
|
// rejections doesn't restorm capability_module with N handshakes.
|
|
invokeRemoteMethodAsyncImpl(objectName, methodName, args,
|
|
std::move(cb), timeout, retriesLeft - 1);
|
|
return;
|
|
}
|
|
if (logos::isUnauthorizedSentinel(result)) {
|
|
logos::CallError e;
|
|
e.code = "unauthorized";
|
|
e.message = "call to '" + objectName.toStdString()
|
|
+ "' rejected: token not recognized (re-exchange failed)";
|
|
e.origin = objectName.toStdString();
|
|
cb(QVariant(), e);
|
|
return;
|
|
}
|
|
cb(std::move(result), err);
|
|
};
|
|
|
|
QString token = getToken(objectName);
|
|
|
|
if (token.isEmpty() && eligible) {
|
|
// Async-chain: dispatch the requestModule call asynchronously, and only
|
|
// fire the real method's invokeRemoteMethodAsync from its callback. The
|
|
// previous version called `requestModule` synchronously here, which made
|
|
// the "async" entry point block its caller for the full round-trip.
|
|
//
|
|
// COALESCE concurrent first-calls behind ONE handshake. A driver that
|
|
// fans out N async calls to an un-tokened target before any completes
|
|
// would otherwise fire N separate requestModule handshakes; each mints a
|
|
// distinct token and informs the target, and the later inform OVERWRITES
|
|
// the earlier token there (the target stores one token per caller). The
|
|
// already-dispatched calls then carry a superseded token and the target
|
|
// rejects them as unauthorized. So only the first caller starts the
|
|
// handshake; the rest queue and all drain with the single minted token.
|
|
// (The sync path can't hit this — it blocks per call, so handshakes
|
|
// never overlap.) m_pendingHandshakes is touched only on the owner
|
|
// thread, reached above, so no lock is needed.
|
|
m_pendingHandshakes[objectName].push_back(
|
|
[this, objectName, methodName, args, timeout, cb = std::move(onResult)]
|
|
(const QString& tok) mutable {
|
|
m_consumer->invokeRemoteMethodAsync(tok, objectName, methodName, args,
|
|
std::move(cb), timeout);
|
|
});
|
|
if (m_pendingHandshakes[objectName].size() > 1)
|
|
return; // a handshake for this target is already in flight
|
|
|
|
const QString capabilityToken = getToken("capability_module");
|
|
const QString origin = m_origin_module;
|
|
// Lifetime: capture the client through a QPointer guard. If it (and its
|
|
// QObject-parented consumers + the pending queue) is destroyed while the
|
|
// requestModule round-trip is in flight, the guard goes null and we drop
|
|
// the queued continuations instead of dereferencing dangling memory.
|
|
QPointer<LogosAPIClient> self(this);
|
|
m_capability_consumer->invokeRemoteMethodAsync(
|
|
capabilityToken,
|
|
QStringLiteral("capability_module"),
|
|
QStringLiteral("requestModule"),
|
|
QVariantList() << origin << objectName,
|
|
[self, objectName](const QVariant& tokenResult) mutable {
|
|
if (!self) return; // client destroyed mid-flight
|
|
const QString tok = tokenResult.toString();
|
|
// Cache the minted token before draining so future calls skip the handshake — m_pendingHandshakes only coalesces the first burst, the cache stops a second burst from racing the same rotation.
|
|
if (!tok.isEmpty()) self->m_token_manager->saveToken(objectName, tok);
|
|
// Drain every continuation queued for this target with the one
|
|
// minted token — the target was informed of exactly this token.
|
|
// An empty tok (handshake failed) still flows through: the
|
|
// consumer call is then rejected and each callback fires with an
|
|
// invalid QVariant, so callers never hang.
|
|
auto it = self->m_pendingHandshakes.find(objectName);
|
|
if (it == self->m_pendingHandshakes.end()) return;
|
|
std::vector<std::function<void(const QString&)>> calls = std::move(it.value());
|
|
self->m_pendingHandshakes.erase(it);
|
|
for (auto& c : calls) c(tok);
|
|
},
|
|
timeout);
|
|
return;
|
|
}
|
|
|
|
m_consumer->invokeRemoteMethodAsync(token, objectName, methodName, args, std::move(onResult), timeout);
|
|
}
|
|
|
|
void LogosAPIClient::invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg, AsyncResultCallback callback,
|
|
Timeout timeout)
|
|
{
|
|
invokeRemoteMethodAsync(objectName, methodName, QVariantList() << arg, std::move(callback), timeout);
|
|
}
|
|
|
|
void LogosAPIClient::invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2,
|
|
AsyncResultCallback callback, Timeout timeout)
|
|
{
|
|
invokeRemoteMethodAsync(objectName, methodName, QVariantList() << arg1 << arg2, std::move(callback), timeout);
|
|
}
|
|
|
|
void LogosAPIClient::invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
AsyncResultCallback callback, Timeout timeout)
|
|
{
|
|
invokeRemoteMethodAsync(objectName, methodName, QVariantList() << arg1 << arg2 << arg3, std::move(callback), timeout);
|
|
}
|
|
|
|
void LogosAPIClient::invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, AsyncResultCallback callback,
|
|
Timeout timeout)
|
|
{
|
|
invokeRemoteMethodAsync(objectName, methodName, QVariantList() << arg1 << arg2 << arg3 << arg4, std::move(callback), timeout);
|
|
}
|
|
|
|
void LogosAPIClient::invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, const QVariant& arg5,
|
|
AsyncResultCallback callback, Timeout timeout)
|
|
{
|
|
invokeRemoteMethodAsync(objectName, methodName, QVariantList() << arg1 << arg2 << arg3 << arg4 << arg5, std::move(callback), timeout);
|
|
}
|
|
|
|
void LogosAPIClient::onEvent(LogosObject* originObject, const QString& eventName, std::function<void(const QString&, const QVariantList&)> callback)
|
|
{
|
|
// Marshal to the owner thread: event registration touches the replica.
|
|
logos::runOnOwnerThread(this, [&]() {
|
|
m_consumer->onEvent(originObject, eventName, std::move(callback));
|
|
});
|
|
}
|
|
|
|
void LogosAPIClient::onEventResponse(LogosObject* object, const QString& eventName, const QVariantList& data)
|
|
{
|
|
qDebug() << "[LogosObject] LogosAPIClient::onEventResponse" << eventName << "-> LogosObject::emitEvent";
|
|
|
|
if (eventName.isEmpty()) {
|
|
qWarning() << "LogosAPIClient: Event name cannot be empty";
|
|
return;
|
|
}
|
|
|
|
if (!object) {
|
|
qWarning() << "LogosAPIClient: Cannot emit event on null object";
|
|
return;
|
|
}
|
|
|
|
object->emitEvent(eventName, data);
|
|
}
|
|
|
|
void LogosAPIClient::onEventResponse(QObject* object, const QString& eventName, const QVariantList& data)
|
|
{
|
|
qDebug() << "[LogosObject] LogosAPIClient::onEventResponse (QObject* compat)" << eventName;
|
|
|
|
if (eventName.isEmpty()) {
|
|
qWarning() << "LogosAPIClient: Event name cannot be empty";
|
|
return;
|
|
}
|
|
|
|
if (!object) {
|
|
qWarning() << "LogosAPIClient: Cannot emit event on null QObject";
|
|
return;
|
|
}
|
|
|
|
QMetaObject::invokeMethod(object, "eventResponse",
|
|
Qt::DirectConnection,
|
|
Q_ARG(QString, eventName),
|
|
Q_ARG(QVariantList, data));
|
|
}
|
|
|
|
bool LogosAPIClient::informModuleToken(const QString& authToken, const QString& moduleName, const QString& token)
|
|
{
|
|
return m_consumer->informModuleToken(authToken, moduleName, token);
|
|
}
|
|
|
|
bool LogosAPIClient::informModuleToken(const std::string& authToken, const std::string& moduleName, const std::string& token)
|
|
{
|
|
return informModuleToken(QString::fromStdString(authToken),
|
|
QString::fromStdString(moduleName),
|
|
QString::fromStdString(token));
|
|
}
|
|
|
|
bool LogosAPIClient::informModuleToken_module(const QString& authToken, const QString& originModule, const QString& moduleName, const QString& token, int timeoutMs)
|
|
{
|
|
// Marshal to the owner thread, exactly as requestObject/invokeRemoteMethod do.
|
|
// This path now goes through acquireCachedObject, so it reads and mutates
|
|
// m_objectCache — declared single-threaded, and holding thread-affine QtRO
|
|
// handles. Before the handshake surface existed this method used an
|
|
// uncached requestObject + release(), so it touched no shared state; routing
|
|
// it onto the cache is what made the missing marshal reachable.
|
|
//
|
|
// (LogosAPIClient::informModuleToken — the 3-arg form above — has the same
|
|
// missing marshal, but it still uses an uncached handle and predates this
|
|
// change, so it is left alone rather than widened into this fix.)
|
|
return logos::runOnOwnerThread(this, [&]() -> bool {
|
|
return m_consumer->informModuleToken_module(authToken, originModule, moduleName, token, timeoutMs);
|
|
});
|
|
}
|
|
|
|
TokenManager* LogosAPIClient::getTokenManager() const
|
|
{
|
|
return m_token_manager;
|
|
}
|
|
|
|
QString LogosAPIClient::getToken(const QString& module_name)
|
|
{
|
|
qDebug() << "LogosAPIClient: getToken for module:" << module_name;
|
|
|
|
QString token = m_token_manager->getToken(module_name);
|
|
if (!token.isEmpty()) {
|
|
qDebug() << "LogosAPIClient: Found token for module:" << module_name;
|
|
return token;
|
|
}
|
|
|
|
qDebug() << "LogosAPIClient: No token found for module:" << module_name;
|
|
return "";
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// nlohmann::json overloads
|
|
// ---------------------------------------------------------------------------
|
|
|
|
nlohmann::json LogosAPIClient::invokeRemoteMethod(const std::string& objectName,
|
|
const std::string& methodName,
|
|
const nlohmann::json& args,
|
|
Timeout timeout)
|
|
{
|
|
QVariantList qArgs = nlohmannArgsToQVariantList(args);
|
|
QVariant result = invokeRemoteMethod(
|
|
QString::fromStdString(objectName),
|
|
QString::fromStdString(methodName),
|
|
qArgs, timeout);
|
|
return qvariantToNlohmann(result);
|
|
}
|
|
|
|
void LogosAPIClient::onEvent(LogosObject* originObject, const std::string& eventName,
|
|
std::function<void(const std::string&, const nlohmann::json&)> callback)
|
|
{
|
|
onEvent(originObject, QString::fromStdString(eventName),
|
|
[cb = std::move(callback)](const QString& name, const QVariantList& data) {
|
|
nlohmann::json jData = nlohmann::json::array();
|
|
for (const QVariant& v : data)
|
|
jData.push_back(qvariantToNlohmann(v));
|
|
cb(name.toStdString(), jData);
|
|
});
|
|
}
|