mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-31 05:51:08 +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>
393 lines
19 KiB
C++
393 lines
19 KiB
C++
#include "logos_api_consumer.h"
|
||
#include "logos_object.h"
|
||
#include "module_proxy.h"
|
||
#include "token_manager.h"
|
||
#include "logos_mode.h"
|
||
#include "logos_instance.h"
|
||
#include "logos_transport.h"
|
||
#include "logos_transport_factory.h"
|
||
#include <chrono>
|
||
#include <thread>
|
||
#include <QDebug>
|
||
#include <QUrl>
|
||
#include <QMetaObject>
|
||
#include <QTimer>
|
||
#include <QTime>
|
||
#include <QPointer>
|
||
|
||
LogosAPIConsumer::LogosAPIConsumer(const QString& module_to_talk_to,
|
||
const QString& origin_module,
|
||
TokenManager* token_manager,
|
||
const LogosTransportConfig& transport,
|
||
QObject *parent)
|
||
: QObject(parent)
|
||
, m_registryUrl(LogosInstance::id(module_to_talk_to))
|
||
, m_token_manager(token_manager)
|
||
{
|
||
// Single transport-resolution path: the factory combines LogosMode
|
||
// + LogosTransportConfig (mode wins for Mock/Local; transport
|
||
// chooses the wire protocol in Remote mode). The choice scopes to
|
||
// this consumer only — any LogosAPIProvider in the same LogosAPI
|
||
// still constructs its host from the global default.
|
||
m_transport = LogosTransportFactory::createConnection(transport, m_registryUrl);
|
||
|
||
// Initial connect with deadline-driven retry. The target module's
|
||
// listener may not be ready yet — particularly for TCP/TLS, where
|
||
// the child subprocess's QTcpServer::listen() lags the runtime
|
||
// returning from its load callback. QLocalSocket internally
|
||
// tolerates this (it retries connect until a deadline), but
|
||
// boost::asio::connect on TCP fails fast with "connection refused"
|
||
// and we'd surface a warning + return nullptr for any subsequent
|
||
// requestObject before the listener even came up.
|
||
//
|
||
// 50ms × up-to-100 attempts ≈ 5s budget — same shape as
|
||
// logos-liblogos's sendTokenToProcess loop and generous enough to
|
||
// cover cold-start child Qt initialisation under load.
|
||
using clock = std::chrono::steady_clock;
|
||
const auto deadline = clock::now() + std::chrono::milliseconds(5000);
|
||
while (true) {
|
||
if (m_transport->connectToHost()) break;
|
||
if (clock::now() >= deadline) break;
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||
}
|
||
}
|
||
|
||
LogosAPIConsumer::LogosAPIConsumer(const QString& module_to_talk_to,
|
||
const QString& origin_module,
|
||
TokenManager* token_manager,
|
||
QObject *parent)
|
||
: LogosAPIConsumer(module_to_talk_to, origin_module, token_manager,
|
||
LogosTransportConfigGlobal::getDefault(), parent)
|
||
{
|
||
}
|
||
|
||
LogosAPIConsumer::~LogosAPIConsumer()
|
||
{
|
||
// Release cached handles while m_transport is still alive (the destructor
|
||
// body runs before member destruction).
|
||
clearObjectCache();
|
||
}
|
||
|
||
LogosObject* LogosAPIConsumer::requestObject(const QString& objectName, Timeout timeout)
|
||
{
|
||
qDebug() << "LogosAPIConsumer: Requesting object:" << objectName << "at" << QTime::currentTime().toString("hh:mm:ss.zzz");
|
||
|
||
if (objectName.isEmpty()) {
|
||
qWarning() << "LogosAPIConsumer: Object name cannot be empty";
|
||
return nullptr;
|
||
}
|
||
|
||
if (!m_transport->isConnected()) {
|
||
qWarning() << "LogosAPIConsumer: Not connected to registry. Cannot request object:" << objectName;
|
||
return nullptr;
|
||
}
|
||
|
||
LogosObject* object = m_transport->requestObject(objectName, timeout.ms);
|
||
if (object) {
|
||
qDebug() << "[LogosObject] LogosAPIConsumer: acquired LogosObject for:" << objectName << "(id:" << object->id() << ")";
|
||
}
|
||
return object;
|
||
}
|
||
|
||
bool LogosAPIConsumer::isConnected() const
|
||
{
|
||
return m_transport->isConnected();
|
||
}
|
||
|
||
QString LogosAPIConsumer::registryUrl() const
|
||
{
|
||
return m_registryUrl;
|
||
}
|
||
|
||
bool LogosAPIConsumer::reconnect()
|
||
{
|
||
qDebug() << "LogosAPIConsumer: Attempting to reconnect to registry:" << m_registryUrl;
|
||
// Handles from the old connection point at replicas that are now dead; drop
|
||
// them so the next call re-acquires against the fresh connection.
|
||
clearObjectCache();
|
||
return m_transport->reconnect();
|
||
}
|
||
|
||
QVariant LogosAPIConsumer::invokeRemoteMethod(const QString& authToken, const QString& objectName, const QString& methodName,
|
||
const QVariantList& args, Timeout timeout)
|
||
{
|
||
return invokeRemoteMethod(authToken, objectName, methodName, args, timeout, nullptr);
|
||
}
|
||
|
||
QVariant LogosAPIConsumer::invokeRemoteMethod(const QString& authToken, const QString& objectName, const QString& methodName,
|
||
const QVariantList& args, Timeout timeout, logos::CallError* err)
|
||
{
|
||
if (err) err->clear();
|
||
qDebug() << "LogosAPIConsumer: Calling invokeRemoteMethod:" << objectName << methodName << "args_count:" << args.size() << "timeout:" << timeout.ms;
|
||
|
||
// Reuse a cached handle across calls. Acquiring a QtRO replica per call
|
||
// (acquireDynamic + waitForSource) is expensive — under a tight loop (e.g. a
|
||
// proxy forwarding every method to its target) it dominates and can starve
|
||
// the nested synchronous calls. The handle is kept alive in m_objectCache
|
||
// and re-acquired only when it goes stale.
|
||
LogosObject* plugin = acquireCachedObject(objectName, timeout.ms);
|
||
if (!plugin) {
|
||
qWarning() << "LogosAPIConsumer: Failed to acquire plugin/replica for object:" << objectName;
|
||
if (err) {
|
||
err->code = "object_unavailable";
|
||
err->message = "failed to acquire remote object '"
|
||
+ objectName.toStdString()
|
||
+ "' (module not loaded, not published, or transport failure)";
|
||
err->origin = objectName.toStdString();
|
||
}
|
||
return QVariant();
|
||
}
|
||
|
||
qDebug() << "[LogosObject] LogosAPIConsumer: calling via LogosObject::callMethod" << methodName;
|
||
// No release() here: the handle stays cached for the next call. Released in
|
||
// clearObjectCache() (destructor / reconnect) or evicted when stale.
|
||
//
|
||
// Prefer the error channel when the transport implements it (see
|
||
// LogosObjectErrorChannel in logos_object.h). Without it, `err` could only
|
||
// ever describe an ACQUIRE failure — everything that went wrong after the
|
||
// handle existed (the deadline elapsing, the connection dropping, the peer
|
||
// answering "not published") came back as a bare QVariant() with a clean
|
||
// err, i.e. reported as a method that returned null.
|
||
if (auto* channel = dynamic_cast<LogosObjectErrorChannel*>(plugin))
|
||
return channel->callMethodWithError(authToken, methodName, args,
|
||
timeout.ms, err);
|
||
return plugin->callMethod(authToken, methodName, args, timeout.ms);
|
||
}
|
||
|
||
// Get-or-acquire a remote-object handle, transparently refreshing a stale one.
|
||
LogosObject* LogosAPIConsumer::acquireCachedObject(const QString& objectName, int timeoutMs)
|
||
{
|
||
if (LogosObject* cached = m_objectCache.value(objectName, nullptr)) {
|
||
if (cached->isValid())
|
||
return cached;
|
||
// The source went away (module unloaded / transport dropped) — discard
|
||
// the dead handle and acquire a fresh one below.
|
||
qDebug() << "LogosAPIConsumer: cached handle for" << objectName << "went stale; re-acquiring";
|
||
cached->release();
|
||
m_objectCache.remove(objectName);
|
||
}
|
||
LogosObject* obj = m_transport->requestObject(objectName, timeoutMs);
|
||
if (obj)
|
||
m_objectCache.insert(objectName, obj);
|
||
return obj;
|
||
}
|
||
|
||
void LogosAPIConsumer::clearObjectCache()
|
||
{
|
||
for (LogosObject* obj : m_objectCache)
|
||
if (obj) obj->release();
|
||
m_objectCache.clear();
|
||
// Drop the remembered absences too: after a reconnect, or once a module is
|
||
// reloaded from a build that has the surface, it deserves a fresh probe.
|
||
m_noHandshakeSurface.clear();
|
||
}
|
||
|
||
void LogosAPIConsumer::invokeRemoteMethodAsync(const QString& authToken, const QString& objectName, const QString& methodName,
|
||
const QVariantList& args,
|
||
AsyncResultCallback callback,
|
||
Timeout timeout)
|
||
{
|
||
// Delegate to the CallError-aware overload so there is one acquire/dispatch
|
||
// path — the legacy callback simply drops the error field.
|
||
invokeRemoteMethodAsync(authToken, objectName, methodName, args,
|
||
[cb = std::move(callback)](QVariant r, const logos::CallError&) mutable {
|
||
if (cb) cb(std::move(r));
|
||
},
|
||
timeout);
|
||
}
|
||
|
||
void LogosAPIConsumer::invokeRemoteMethodAsync(const QString& authToken, const QString& objectName, const QString& methodName,
|
||
const QVariantList& args,
|
||
AsyncResultErrorCallback callback,
|
||
Timeout timeout)
|
||
{
|
||
if (!callback) {
|
||
qWarning() << "LogosAPIConsumer: invokeRemoteMethodAsync called with null callback";
|
||
return;
|
||
}
|
||
|
||
// Reuse the cached handle, same as the sync path — repeated async calls to
|
||
// one object (e.g. a proxy forwarding asynchronously) no longer re-acquire a
|
||
// replica per call. The handle stays owned by m_objectCache; the callback
|
||
// must NOT release it (it is shared across in-flight calls and freed only on
|
||
// eviction/teardown, via release()'s deferred deleteLater).
|
||
LogosObject* plugin = acquireCachedObject(objectName, timeout.ms);
|
||
if (!plugin) {
|
||
qWarning() << "LogosAPIConsumer: Failed to acquire plugin/replica for object:" << objectName;
|
||
logos::CallError err;
|
||
err.code = "object_unavailable";
|
||
err.message = "failed to acquire remote object '"
|
||
+ objectName.toStdString()
|
||
+ "' (module not loaded, not published, or transport failure)";
|
||
err.origin = objectName.toStdString();
|
||
QTimer::singleShot(0, this, [callback, err]() { callback(QVariant(), err); });
|
||
return;
|
||
}
|
||
|
||
qDebug() << "[LogosObject] LogosAPIConsumer: async calling via LogosObject::callMethodAsync" << methodName;
|
||
// QPointer guards against use-after-free: if the consumer is destroyed
|
||
// before the transport callback fires, the callback is silently dropped and
|
||
// the handle is released by the destructor's clearObjectCache(), not here.
|
||
QPointer<LogosAPIConsumer> self(this);
|
||
|
||
// Prefer the error channel when the transport implements it. The lambda
|
||
// below used to take only `QVariant result` and hand the caller a
|
||
// hard-coded empty logos::CallError — so once acquire had succeeded, every
|
||
// async outcome was reported as a success, whatever actually happened.
|
||
if (auto* channel = dynamic_cast<LogosObjectErrorChannel*>(plugin)) {
|
||
channel->callMethodAsyncWithError(authToken, methodName, args, timeout.ms,
|
||
[callback, self](QVariant result, const logos::CallError& err) {
|
||
if (!self)
|
||
return;
|
||
callback(std::move(result), err);
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Transport without an error channel (the mock): unchanged behaviour —
|
||
// the value, and no diagnosis to give.
|
||
plugin->callMethodAsync(authToken, methodName, args, timeout.ms,
|
||
[callback, self](QVariant result) {
|
||
if (!self)
|
||
return;
|
||
callback(result, logos::CallError{});
|
||
});
|
||
}
|
||
|
||
void LogosAPIConsumer::onEvent(LogosObject* originObject, const QString& eventName, std::function<void(const QString&, const QVariantList&)> callback)
|
||
{
|
||
qDebug() << "[LogosObject] LogosAPIConsumer::onEvent registering for:" << eventName << "on LogosObject id:" << originObject;
|
||
|
||
if (!originObject) {
|
||
qWarning() << "LogosAPIConsumer: Cannot register event on null object";
|
||
return;
|
||
}
|
||
|
||
originObject->onEvent(eventName, std::move(callback));
|
||
|
||
qDebug() << "[LogosObject] LogosAPIConsumer: event callback registered for:" << eventName;
|
||
}
|
||
|
||
bool LogosAPIConsumer::informModuleToken(const QString& authToken, const QString& moduleName, const QString& token)
|
||
{
|
||
qDebug() << "LogosAPIConsumer: Informing module token for module:" << moduleName << "with token:" << redactToken(token);
|
||
|
||
LogosObject* plugin = m_transport->requestObject("capability_module", 20000);
|
||
if (!plugin) {
|
||
qWarning() << "LogosAPIConsumer: Failed to acquire plugin/replica for object: capability_module";
|
||
return false;
|
||
}
|
||
|
||
qDebug() << "[LogosObject] LogosAPIConsumer: calling LogosObject::informModuleToken for" << moduleName;
|
||
bool result = plugin->informModuleToken(authToken, moduleName, token, 20000);
|
||
qDebug() << "LogosAPIConsumer: informModuleToken completed with result:" << result;
|
||
plugin->release();
|
||
return result;
|
||
}
|
||
|
||
namespace {
|
||
// How long to wait when probing for a handshake surface before concluding the
|
||
// target predates it. Long enough to cover a live local socket round trip,
|
||
// short enough that the fallback is not perceptibly delayed.
|
||
constexpr int kHandshakeProbeTimeoutMs = 250;
|
||
} // namespace
|
||
|
||
bool LogosAPIConsumer::informModuleToken_module(const QString& authToken, const QString& originModule, const QString& moduleName, const QString& token, int timeoutMs)
|
||
{
|
||
// A non-positive budget would make the wait transport-dependent rather than
|
||
// bounded; fall back to the historical default.
|
||
if (timeoutMs <= 0) {
|
||
timeoutMs = 20000;
|
||
}
|
||
qDebug() << "LogosAPIConsumer: Informing module token for module:" << moduleName << "with token:" << redactToken(token);
|
||
|
||
// Prefer the handshake surface. It is published before the target's
|
||
// initializer runs, so it is reachable even while the target is still
|
||
// starting up — which is the one case the business object cannot cover,
|
||
// because that one is published only once the initializer returns.
|
||
//
|
||
// Short budget on this attempt: a module built before the handshake surface
|
||
// existed simply has no such object, and we must not spend the full timeout
|
||
// discovering that before falling back.
|
||
// acquireCachedObject caches successes only, so without the negative cache
|
||
// below a module built before this surface existed would pay the full probe
|
||
// budget on EVERY grant — on QtRO that is a blocking waitForSource, i.e.
|
||
// kHandshakeProbeTimeoutMs of dead time per token, forever. Remember the
|
||
// absence instead and go straight to the business object. Cleared with the
|
||
// handle cache on reconnect/destroy, so a module that comes back with a
|
||
// handshake surface is re-probed rather than written off permanently.
|
||
const QString handshake = logos::handshakeObjectName(originModule);
|
||
if (m_noHandshakeSurface.contains(handshake)) {
|
||
return informModuleTokenViaBusinessObject(authToken, originModule, moduleName, token, timeoutMs);
|
||
}
|
||
LogosObject* early = acquireCachedObject(handshake, kHandshakeProbeTimeoutMs);
|
||
if (!early) {
|
||
m_noHandshakeSurface.insert(handshake);
|
||
qDebug() << "LogosAPIConsumer:" << originModule << "publishes no handshake surface"
|
||
<< "- not probing again until the handle cache is cleared";
|
||
}
|
||
if (early) {
|
||
qDebug() << "[LogosObject] LogosAPIConsumer: delivering token for" << moduleName
|
||
<< "via the handshake surface of" << originModule;
|
||
if (early->informModuleToken(authToken, moduleName, token, timeoutMs)) {
|
||
qDebug() << "LogosAPIConsumer: informModuleToken completed with result: true";
|
||
return true;
|
||
}
|
||
// A refusal HERE is not authoritative, so do not report it as the answer.
|
||
// The handshake surface goes live before the target's initializer runs,
|
||
// and a target whose token store is only seeded by that initializer will
|
||
// refuse a push that arrives first. Falling through to the business
|
||
// object — which exists only once the initializer has returned, by which
|
||
// point the store is populated — is what the caller got before this
|
||
// surface existed. Returning false here instead would hand the caller an
|
||
// empty grant that it has no way to distinguish from a real denial.
|
||
//
|
||
// This cannot reintroduce the startup wedge: the wait below is bounded by
|
||
// the caller's own budget (capability_module passes 3000 ms), not by the
|
||
// 20 s default that made the original deadlock fatal.
|
||
qWarning() << "LogosAPIConsumer: handshake surface of" << originModule
|
||
<< "refused the token for" << moduleName
|
||
<< "- it is probably still initializing; retrying on the business object";
|
||
}
|
||
|
||
return informModuleTokenViaBusinessObject(authToken, originModule, moduleName, token, timeoutMs);
|
||
}
|
||
|
||
// Fall back to the business object: modules built before the handshake surface
|
||
// existed are reached exactly as they always were. Also the landing place for a
|
||
// handshake surface that refused the push (target still initializing).
|
||
bool LogosAPIConsumer::informModuleTokenViaBusinessObject(const QString& authToken, const QString& originModule, const QString& moduleName, const QString& token, int timeoutMs)
|
||
{
|
||
LogosObject* plugin = acquireCachedObject(originModule, timeoutMs);
|
||
if (!plugin) {
|
||
qWarning() << "LogosAPIConsumer: Failed to acquire plugin/replica for object:" << originModule
|
||
<< "- no handshake surface and no published business object"
|
||
<< "(waited" << timeoutMs << "ms; it may still be initializing)";
|
||
return false;
|
||
}
|
||
|
||
qDebug() << "[LogosObject] LogosAPIConsumer: calling LogosObject::informModuleToken for" << moduleName << "on" << originModule;
|
||
bool result = plugin->informModuleToken(authToken, moduleName, token, timeoutMs);
|
||
qDebug() << "LogosAPIConsumer: informModuleToken completed with result:" << result;
|
||
// The cache owns the handle now, so it is not released here.
|
||
return result;
|
||
}
|
||
|
||
std::string LogosAPIConsumer::requestModule(const std::string& authToken, const std::string& originModule, const std::string& targetModule)
|
||
{
|
||
const QString qOrigin = QString::fromStdString(originModule);
|
||
const QString qTarget = QString::fromStdString(targetModule);
|
||
qDebug() << "LogosAPIConsumer: requestModule for origin:" << qOrigin << "target:" << qTarget;
|
||
|
||
LogosObject* plugin = m_transport->requestObject("capability_module", 20000);
|
||
if (!plugin) {
|
||
qWarning() << "LogosAPIConsumer: Failed to acquire plugin/replica for object: capability_module";
|
||
return {};
|
||
}
|
||
|
||
QVariant result = plugin->callMethod(QString::fromStdString(authToken), QStringLiteral("requestModule"),
|
||
QVariantList() << qOrigin << qTarget, 20000);
|
||
plugin->release();
|
||
return result.toString().toStdString();
|
||
}
|