#ifndef LOGOS_API_CLIENT_H #define LOGOS_API_CLIENT_H #include #include #include #include #include #include #include #include #include #include "logos_call_error.h" #include "logos_mode.h" #include "logos_shared_api.h" #include "logos_subscription_state.h" #include "logos_transport_config.h" #include class LogosAPI; class LogosAPIConsumer; class LogosObject; class TokenManager; /** * @brief LogosAPIClient provides a high-level interface for remote method calls * * This class serves as a facade over LogosAPIConsumer, providing a clean interface * for applications that need to call remote methods and handle events. * * LOGOS_SHARED_API for the same reason as TokenManager, plus one of its own: * this is the object that reaches the shared TokenManager on the call path, and * archives are pulled a whole OBJECT FILE at a time. If the consumer imports * TokenManager but not LogosAPIClient, ld pulls logos_api_client.cpp.obj out of * liblogos_protocol.a for the client, that object drags token_manager.cpp.obj * back in with it, and the duplicate static reappears. See logos_shared_api.h. */ class LOGOS_SHARED_API LogosAPIClient : public QObject { Q_OBJECT public: /** * @brief Construct a client with explicit transports for both the * target module *and* `capability_module`. * * Two transports because the SDK's auto-`requestModule` flow inside * invokeRemoteMethod{,Async} dials `capability_module` to fetch a * per-target token. When the daemon advertises capability_module on * a different transport from the target (e.g. CLI on host → * core_service over TCP, but capability_module also over TCP on a * sibling port), the auto-dial must use the right one. Pre-building * the consumer once in the constructor (see m_capability_consumer) * keeps the hot path free of per-call lookups. */ LogosAPIClient(const QString& module_to_talk_to, const QString& origin_module, TokenManager* token_manager, const LogosTransportConfig& target_transport, const LogosTransportConfig& capability_transport, QObject *parent = nullptr); /** * @brief No-transport constructor — both target and * capability_module use the process-global default * (LocalSocket) via LogosTransportConfigGlobal::getDefault(). */ explicit LogosAPIClient(const QString& module_to_talk_to, const QString& origin_module, TokenManager* token_manager, QObject *parent = nullptr); ~LogosAPIClient(); /** * @brief Request a LogosObject handle by name * @return LogosObject* handle, or nullptr if failed */ LogosObject* requestObject(const QString& objectName, Timeout timeout = Timeout()); bool isConnected() const; QString registryUrl() const; bool reconnect(); QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName, const QVariantList& args = QVariantList(), Timeout timeout = Timeout()); /** * @brief invokeRemoteMethod with an explicit error out-channel. * * Fills *err with the canonical {code, message, origin} call error when * the failure is detectable (today: "object_unavailable" when the target * object cannot be acquired); cleared on success. Generated typed client * wrappers call this overload and throw logos::LogosCallError so callers * can distinguish a failed call from a legitimately default-valued * result. */ QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName, const QVariantList& args, Timeout timeout, logos::CallError* err); QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName, const QVariant& arg, Timeout timeout = Timeout()); QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName, const QVariant& arg1, const QVariant& arg2, Timeout timeout = Timeout()); QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName, const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, Timeout timeout = Timeout()); QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName, const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, const QVariant& arg4, Timeout timeout = Timeout()); QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName, const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, const QVariant& arg4, const QVariant& arg5, Timeout timeout = Timeout()); // const char* overloads — resolve ambiguity for string literals QVariant invokeRemoteMethod(const char* objectName, const char* methodName, const QVariantList& args = QVariantList(), Timeout timeout = Timeout()) { return invokeRemoteMethod(QString(objectName), QString(methodName), args, timeout); } QVariant invokeRemoteMethod(const char* objectName, const char* methodName, const QVariant& arg, Timeout timeout = Timeout()) { return invokeRemoteMethod(QString(objectName), QString(methodName), arg, timeout); } QVariant invokeRemoteMethod(const char* objectName, const char* methodName, const QVariant& arg1, const QVariant& arg2, Timeout timeout = Timeout()) { return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, timeout); } QVariant invokeRemoteMethod(const char* objectName, const char* methodName, const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, Timeout timeout = Timeout()) { return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, arg3, timeout); } QVariant invokeRemoteMethod(const char* objectName, const char* methodName, const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, const QVariant& arg4, Timeout timeout = Timeout()) { return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, arg3, arg4, timeout); } QVariant invokeRemoteMethod(const char* objectName, const char* methodName, const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, const QVariant& arg4, const QVariant& arg5, Timeout timeout = Timeout()) { return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, arg3, arg4, arg5, timeout); } // std::string overloads — thin wrappers that convert internally QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName, const QVariantList& args = QVariantList(), Timeout timeout = Timeout()) { return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), args, timeout); } QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName, const QVariant& arg, Timeout timeout = Timeout()) { return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg, timeout); } QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName, const QVariant& arg1, const QVariant& arg2, Timeout timeout = Timeout()) { return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, timeout); } QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName, const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, Timeout timeout = Timeout()) { return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, arg3, timeout); } QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName, const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, const QVariant& arg4, Timeout timeout = Timeout()) { return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, arg3, arg4, timeout); } QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName, const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, const QVariant& arg4, const QVariant& arg5, Timeout timeout = Timeout()) { return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, arg3, arg4, arg5, timeout); } // const char* onEvent overload void onEvent(LogosObject* originObject, const char* eventName, std::function callback) { onEvent(originObject, QString(eventName), callback); } // std::string onEvent overloads void onEvent(LogosObject* originObject, const std::string& eventName, std::function callback) { onEvent(originObject, QString::fromStdString(eventName), callback); } void onEvent(LogosObject* originObject, const std::string& eventName, std::function callback) { onEvent(originObject, QString::fromStdString(eventName), [cb = std::move(callback)](const QString& name, const QVariantList& args) { cb(name.toStdString(), args); }); } void onEvent(LogosObject* originObject, const char* eventName, std::function callback) { onEvent(originObject, QString(eventName), [cb = std::move(callback)](const QString& name, const QVariantList& args) { cb(name.toStdString(), args); }); } using AsyncResultCallback = std::function; /** * @brief Async callback with an explicit error out-channel. * * Mirrors the sync `invokeRemoteMethod(..., CallError*)` overload. Set to * code="object_unavailable" when the target object cannot be acquired, * cleared on success. Callers that need to distinguish acquire failure * from a legitimately empty QVariant result should use this overload. */ using AsyncResultErrorCallback = std::function; void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName, const QVariantList& args, AsyncResultCallback callback, Timeout timeout = Timeout()); /** * @brief invokeRemoteMethodAsync with an explicit error out-channel. * See AsyncResultErrorCallback docs above. */ void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName, const QVariantList& args, AsyncResultErrorCallback callback, Timeout timeout = Timeout()); void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName, const QVariant& arg, AsyncResultCallback callback, Timeout timeout = Timeout()); void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName, const QVariant& arg1, const QVariant& arg2, AsyncResultCallback callback, Timeout timeout = Timeout()); void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName, const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, AsyncResultCallback callback, Timeout timeout = Timeout()); void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName, const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, const QVariant& arg4, AsyncResultCallback callback, Timeout timeout = Timeout()); void 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 = Timeout()); /** * @brief Register an event listener via LogosObject's callback mechanism * @param originObject The LogosObject that will emit the event * @param eventName The name of the event to listen for * @param callback Function to call when the event is triggered */ void onEvent(LogosObject* originObject, const QString& eventName, std::function callback); /** * @brief Subscribe to an event on a module that may not be reachable YET. * * The safe alternative to requestObject() + onEvent() for any caller that * subscribes during startup — a UI plugin's Component.onCompleted, a * module's initLogos(), a backend's onContextReady(). Never blocks; arms * when the module appears, including a mid-session install; warns once on * deferral and logs when it arms. See LogosAPIConsumer::onEventWhenAvailable * for the full contract, the cost of waiting, and the (deliberate) lack of * de-duplication. * * Adds no member state to this class — see the ABI note below; it forwards * to the consumer that already exists. * * @return A non-zero id for cancelEventSubscription() / * eventSubscriptionState(), or 0 if the arguments were refused. */ quint64 onEventWhenAvailable(const QString& objectName, const QString& eventName, std::function callback, std::function onArmed = {}); quint64 onEventWhenAvailable(const std::string& objectName, const std::string& eventName, std::function callback, std::function onArmed = {}) { return onEventWhenAvailable(QString::fromStdString(objectName), QString::fromStdString(eventName), [cb = std::move(callback)](const QString& name, const QVariantList& args) { cb(name.toStdString(), args); }, std::move(onArmed)); } /** * @brief Call `onReady` once, as soon as the module becomes acquirable. * * The call-path counterpart of onEventWhenAvailable(), for a caller that * must invoke a method on a module which may still be starting: it neither * fails fast (stranding a UI that will never retry) nor calls straight * through into the transport's acquire timeout on the calling thread. Fires * exactly once, is not re-armed on reconnect, and holds no subscription. * See LogosAPIConsumer::whenObjectAvailable for the full contract. * * @return A non-zero id accepted by cancelEventSubscription(), or 0. */ quint64 whenObjectAvailable(const QString& objectName, std::function onReady); /** * @brief Stop tracking the subscription with this id. * * See LogosAPIConsumer::cancelEventSubscription — in particular that it * stops the BOOKKEEPING (the retry timer, the watchdog, the reconnect * re-arm set) and does not detach the callback from the shared handle. */ bool cancelEventSubscription(quint64 subscriptionId); /** * @brief Whether a subscription id is still pending, armed, or forgotten. * For callers that keep their own de-duplication record and need to * check it rather than trust it. */ LogosSubscriptionState eventSubscriptionState(quint64 subscriptionId) const; /** * @brief Diagnostics: "::" for every deferred subscription * that has not armed yet. */ QStringList pendingEventSubscriptions() const; /** * @brief Emit an event on a LogosObject (for plugins that act as event sources) * @param object The LogosObject to emit the event on * @param eventName The name of the event * @param data The event data */ void onEventResponse(LogosObject* object, const QString& eventName, const QVariantList& data); /** * @brief Backward-compatible overload for QObject-based plugins. * * Old-API plugins call onEventResponse(this, ...) where `this` is a QObject*. * This overload invokes the eventResponse signal on the QObject via QMetaObject. */ void onEventResponse(QObject* object, const QString& eventName, const QVariantList& data); bool informModuleToken(const QString& authToken, const QString& moduleName, const QString& token); bool informModuleToken(const char* authToken, const char* moduleName, const char* token) { return informModuleToken(QString(authToken), QString(moduleName), QString(token)); } bool informModuleToken(const std::string& authToken, const std::string& moduleName, const std::string& token); bool informModuleToken_module(const QString& authToken, const QString& originModule, const QString& moduleName, const QString& token, int timeoutMs = 20000); TokenManager* getTokenManager() const; // The OUTBOUND token for `module_name`: what this client presents when it // CALLS `module_name`. Never a token some caller was issued to call US — // that half of the store has no reader here, by construction (see the // DIRECTION note in token_manager.h). QString getToken(const QString& module_name); // nlohmann::json overloads — args is a JSON array, result is a JSON value. // These convert between nlohmann::json and QVariant internally so callers // never need to touch Qt JSON types. nlohmann::json invokeRemoteMethod(const std::string& objectName, const std::string& methodName, const nlohmann::json& args, Timeout timeout = Timeout()); // nlohmann::json event callback overload — data arrives as a json array. void onEvent(LogosObject* originObject, const std::string& eventName, std::function callback); private: // requestModule handshake against capability_module + cache the minted token // in the shared TokenManager. Returns the token ("" on failure). Factors out // the first-exchange logic so both the initial fetch and the // rejection-driven re-exchange share one path. (Private method — no effect on // the ABI-sensitive data layout below.) // `timeout` is the CALLER's budget, and it bounds the handshake too. It has // to: the exchange runs before the call on an un-tokened target, so a bound // that skipped it would describe only the second half of the operation. QString mintAndCacheToken(const QString& objectName, Timeout timeout); // Async invoke with a bounded retry budget backing the public // invokeRemoteMethodAsync overloads. On a provider rejection sentinel it // drops the stale token and re-enters itself with retriesLeft-1, so the retry // coalesces through the same m_pendingHandshakes machinery. void invokeRemoteMethodAsyncImpl(const QString& objectName, const QString& methodName, const QVariantList& args, AsyncResultErrorCallback callback, Timeout timeout, int retriesLeft); // ABI note: this private layout is consumed by every plugin that // statically links libsdk. Adding a new field in the middle of // this section shifts the offsets of subsequent fields and // SILENTLY breaks any plugin compiled before the change — it // reads m_token_manager at the wrong offset and segfaults on // the first cross-process call. New private members MUST be // appended to the end. (Long-term cure: pimpl this class so // sizeof / offsets become opaque to consumers.) LogosAPIConsumer* m_consumer; QMap m_tokens; TokenManager* m_token_manager; QString m_origin_module; // Pre-built consumer for the auto-`requestModule` token-fetch path // in invokeRemoteMethod{,Async}. Constructed once with the right // transport (see the two-transport ctor) so the hot path doesn't // chase a back-pointer to LogosAPI just to look up the transport // registry. Null only when `m_consumer` itself is for // capability_module (no recursion). In-class default to nullptr // so any old constructor that doesn't list this field still // leaves a defined value. LogosAPIConsumer* m_capability_consumer = nullptr; // Per-target queue of continuations waiting on an in-flight async // requestModule handshake. The FIRST async call to an un-tokened target // starts exactly one handshake; concurrent calls to the same target queue // here and all drain with the single minted token when it resolves. Without // this coalescing a fan-out of N first-calls fires N racing handshakes whose // distinct tokens overwrite each other on the target — so already-dispatched // calls carry a superseded token and get rejected. Touched only on the // owner thread (invokeRemoteMethodAsync marshals there), so it needs no // lock. Appended last per the ABI note above; defaults to empty. QMap>> m_pendingHandshakes; // One-shot latch for the "isolated store with no credential" warning in // mintAndCacheToken(). The condition repeats on every call and the message // is about the HOST's wiring, so it is worth saying once and not N times. // Appended last per the ABI note above; defaults to false. bool m_warnedNoCredential = false; }; #endif // LOGOS_API_CLIENT_H