Files
logos-protocol/cpp/logos_api_consumer.cpp
T
Dario Lipicar 9de4165ab6 Qt split + module authoring groundwork: LogosProviderPlugin + the common module-impl C ABI (#3)
* Extract the Logos protocol layer from logos-cpp-sdk

Transports (plain TCP/TLS, qt_local, qt_remote/QRO, mock), token manager,
consumer core (LogosAPIClient/LogosAPIConsumer incl. the capability
auto-requestModule flow), ModuleProxy, the abstract LogosProviderObject
interface, and the canonical QVariant<->JSON conversion — now behind the
language-neutral lp_* C ABI (logos_protocol.h) carrying the protocol
semver (LOGOS_PROTOCOL_VERSION_*, lp_protocol_version()).

Bytes crossing the ABI use the lossless {"_bytes": base64url} tagging
(NUL-safe), matching the plain wire encoding.

Provider lp_* surface is compiled groundwork; serving lands with module
authoring.

* Move LogosProviderPlugin into logos_provider_interface.h

Plugin-loading tools (logos-cpp-generator's introspection mode, lm, the
hosts) need only qobject_cast<LogosProviderPlugin*>() + the abstract
LogosProviderObject — both framework-internal. Hosting the detection
interface here keeps those tools off the developer-facing logos-qt-sdk
layer. Same iid (org.logos.LogosProviderPlugin); header-only, ABI-neutral.

* Define the common module-impl C ABI (logos_module_impl.h)

ONE cdylib contract for module implementations in every language:
dispatch / get_methods / set_context / set_emit_callback / accept_token
/ get_protocol_version / string_free. The C++ and Rust SDKs emit these
exports around their respective impls; the uniform generated Qt glue
(and later a no-Qt host) talks to the cdylib only through this ABI.
JSON data model and tagged bytes form match the lp_* consumer ABI; the
protocol-version handshake complements the build-time metadata stamp.

* json convert: integers stay integers across the C ABI

QJsonValue::fromVariant degrades every numeric to double, so Int/UInt/
LongLong/ULongLong QVariants serialized as 5.0 — and a strict consumer on
the other side of the C ABI (a generated dispatch reading an int param)
rejects or zeroes them. Surfaced by the first cdylib-authored module
whose inbound args cross qvariantToNlohmann; the dlopen smoke harness
fed hand-written int JSON and never exercised this edge.

* call-error channel: surface {code,message,origin} for unacquirable targets

invokeRemoteMethod could not distinguish a failed call from a void/null
result — lp_invoke returned LP_OK with a null JSON result even when the
target module was never reached, and generated typed wrappers silently
defaulted (0 / empty string). Additive err-out overloads on
LogosAPIConsumer/LogosAPIClient fill a std-only logos::CallError
(logos_call_error.h, new LogosCallError exception for the generated
wrappers to throw); lp_invoke now honors its documented contract for
this class of failure: LP_ERR_UNAVAILABLE + canonical error JSON.
First detectable code: object_unavailable (requestObject failure) —
the struct is the extension point for transport-level statuses.

* call-error: drop the exception type — the error channel is the out-param

Per review, generated wrappers expose CallError as an optional trailing
out-parameter instead of throwing; the struct is the whole contract.

* ci: build + run the protocol test suite

On every pull request (unfiltered — stacked PRs included), master pushes,
and manual dispatch. The repo shipped without CI; its 111-test suite only
ran locally and through the workspace gate.

* consumer: typed requestModule for the capability flow

Port of logos-cpp-sdk master f5a127dd ('use updated capability module',
cpp-sdk#85, Iuri Matias) — the touched files (logos_api_client.cpp,
logos_api_consumer.{h,cpp}) moved into this repo in the P1 extraction.
The capability auto-requestModule path now calls a typed std::string
helper on the consumer (which acquires the capability object directly)
instead of a stringly invokeRemoteMethod round-trip. 111/111 tests.

* ci: DeterminateSystems nix installer (macOS runners)

cachix/install-nix-action fails on the macOS runners with
eDSRecordAlreadyExists (pre-existing nix build users); the org's
macOS-bearing workflows use the DeterminateSystems installer.
2026-06-12 19:39:57 -03:00

238 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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()
{
}
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;
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;
LogosObject* plugin = m_transport->requestObject(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;
QVariant result = plugin->callMethod(authToken, methodName, args, timeout.ms);
plugin->release();
return result;
}
void LogosAPIConsumer::invokeRemoteMethodAsync(const QString& authToken, const QString& objectName, const QString& methodName,
const QVariantList& args,
AsyncResultCallback callback,
Timeout timeout)
{
if (!callback) {
qWarning() << "LogosAPIConsumer: invokeRemoteMethodAsync called with null callback";
return;
}
LogosObject* plugin = m_transport->requestObject(objectName, timeout.ms);
if (!plugin) {
qWarning() << "LogosAPIConsumer: Failed to acquire plugin/replica for object:" << objectName;
QTimer::singleShot(0, this, [callback]() { callback(QVariant()); });
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.
// No re-queuing needed -- the transport already delivers on a deferred
// event-loop iteration (QTimer / QueuedConnection).
QPointer<LogosAPIConsumer> self(this);
plugin->callMethodAsync(authToken, methodName, args, timeout.ms,
[plugin, callback, self](QVariant result) {
// Deliver the result before release(): destroying the replica can
// invalidate storage that QVariant still references for some return
// types (matches sync invokeRemoteMethod: callMethod then release).
if (!self) {
plugin->release();
return;
}
callback(result);
plugin->release();
});
}
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;
}
bool LogosAPIConsumer::informModuleToken_module(const QString& authToken, const QString& originModule, const QString& moduleName, const QString& token)
{
qDebug() << "LogosAPIConsumer: Informing module token for module:" << moduleName << "with token:" << redactToken(token);
LogosObject* plugin = m_transport->requestObject(originModule, 20000);
if (!plugin) {
qWarning() << "LogosAPIConsumer: Failed to acquire plugin/replica for object:" << originModule;
return false;
}
qDebug() << "[LogosObject] LogosAPIConsumer: calling LogosObject::informModuleToken for" << moduleName << "on" << originModule;
bool result = plugin->informModuleToken(authToken, moduleName, token, 20000);
qDebug() << "LogosAPIConsumer: informModuleToken completed with result:" << result;
plugin->release();
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();
}