mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 20:11:07 +00:00
feat(windows): named pipes, an explicit lp_* ABI, and a cross target (#58)
* feat(windows): port logos_socket_paths and add a cross target logos_socket_paths.cpp is the only POSIX-bound file in logos-protocol. All of it is unix-domain-socket machinery, and on Windows the local transport is named pipes (QLocalServer maps a name to \\.\pipe\<name>), where none of the assumptions hold: a pipe has no inode to lstat/chown/chmod -- access comes from a security descriptor set at CreateNamedPipe time -- and a pipe cannot outlive its last handle, so a hard-killed process leaves nothing behind. isSocketDead and reapStaleSockets are therefore not merely unimplemented on Windows, they are vacuous: the state they detect cannot arise. Both return the fail-closed answer (false / 0), matching the documented contract that an endpoint is never reported dead unless certain. applySocketPerms deliberately does NOT no-op. With no policy requested it returns true, as on POSIX. But when LOGOS_SOCKET_GROUP or LOGOS_SOCKET_MODE *are* set it fails with an explanatory error, because silently returning true would leave the endpoint more permissive than the operator asked for -- the one direction this file is careful never to go (cf. the chgrp-then-chmod ordering in the POSIX branch). Granting a pipe to a group needs a DACL plus a group->SID resolver; until that exists, refuse loudly. Also gates qt6.wrapQtAppsNoGuiHook behind !isWindows and sets dontWrapQtApps. Both halves are required: the hook does not even evaluate for a mingw host, it would be inert anyway (wrap-qt-apps-hook.sh skips anything that is not ELF or Mach-O), and qtbase's setup hook hard-errors in qtPreHook unless dontWrapQtApps is set. Header contract updated per function. POSIX branch unchanged and still compiles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: make the Boost.System component optional, not required find_package(Boost REQUIRED COMPONENTS system) hard-fails on Boost 1.89: Could not find a package configuration file provided by "boost_system" Boost.System has been header-only for years, and 1.89 finally dropped the compiled boost_system library, so no boost_systemConfig.cmake is installed at all. The COMPONENTS request was not gratuitous though -- on 1.87 the Boost::system imported target is only exported when the component is asked for, which is what the previous comment recorded. So ask optionally and fall back to Boost::headers, which supplies the same header-only error_code either way. The choice is by BOOST VERSION, not by platform: this is not a Windows quirk, it simply surfaced first there because the Windows target pins a newer nixpkgs (Boost 1.89) than the native one (Boost 1.87). Verified both ways -- native aarch64-darwin still selects Boost::system: -- Boost.System target: Boost::system (Boost 1.87.0) and the build completes unchanged. Also adds QT_HOST_PATH / QT_ADDITIONAL_HOST_PACKAGES_PREFIX_PATH for the Windows target. Qt6RemoteObjectsDependencies.cmake declares set(__qt_RemoteObjects_tool_deps "Qt6RemoteObjectsTools;6.11.1") and Qt6RemoteObjectsTools holds repc, which must RUN on the build machine -- so under cross it lives in the build-platform Qt, not the mingw one. Without these, find_package reports the thoroughly misleading "Expected Config file at <qtbase>/lib/cmake/Qt6RemoteObjects ... does NOT exist": the TARGET config is found fine; it is the HOST tool package that is missing. Every Qt-consuming repo will need this, so it should be hoisted into logos-nix rather than repeated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: declare the lp_* C ABI explicitly instead of relying on auto-export Adds LP_API (__declspec(dllexport) when building the shared library, default visibility elsewhere) to the 21 lp_* entry points, and defines LOGOS_PROTOCOL_BUILDING_SHARED for the shared target only, so the static archive leaves LP_API empty and its consumers need no import library. This is NOT a bug fix, contrary to what the concern in the Windows plan suggested. Measured on the cross-built DLL, before and after: before: export table 0x2ece (11982 symbols), lp_* present: 21 after: export table 0x15 ( 21 symbols), lp_* present: 21 GNU ld's PE auto-export was already exporting lp_* -- along with roughly twelve thousand other symbols. The worry was that logos_module_impl.h's __declspec(dllexport) would disable auto-export image-wide and silently drop lp_*; it does not, because no translation unit in logos_protocol includes that header (it is listed in PROTOCOL_SOURCES for IDE visibility only). What this does buy is worth having anyway: the exported surface is now the ABI we actually declare rather than whatever happens to have external linkage, it stops being contingent on auto-export staying enabled -- which the very next TU to gain a dllexport would silently end -- and it drops ~12k incidental symbols from the export table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: relax the Boost.System requirement in the EXPORTED cmake config too The previous commit fixed cpp/CMakeLists.txt but left logos-protocolConfig.cmake.in still doing find_dependency(Boost REQUIRED COMPONENTS system) so logos-protocol itself built fine on Boost 1.89 while every CONSUMER of its installed CMake package failed at configure time -- caught by logos-qt-sdk, which is the first downstream repo to be cross-built. Worth noting as a general trap: a package can be internally consistent and still ship a broken contract, because the exported config is a separate artifact from the build. Anything changed in one has to be checked in the other. Verified both directions: the Windows cross builds of logos-cpp-sdk and logos-qt-sdk now succeed, and a native aarch64-darwin logos-qt-sdk build -- which consumes this same config against Boost 1.87 -- still succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(windows): mark the types that must exist once per process PE has no symbol interposition. ELF and Mach-O interpose across the whole image set, so when liblogos_core exports TokenManager::instance() every other image binds to that one definition and the function-local `static TokenManager instance;` is genuinely a singleton. On Windows every image that links liblogos_protocol.a / liblogos_qt_sdk.a statically gets its own copy of the code and therefore its own statics -- measured: NINE images in the Basecamp payload each defined TokenManager::instance()::instance. The host saved a capability token into its copy, the UI plugin read its own empty copy, and every cross-module call was refused (29 "ModuleProxy: rejecting unauthorized call"). LOGOS_SHARED_API marks the affected types. It expands to __declspec(dllimport) only for a consumer that opts in with LOGOS_SHARED_USE_DLL, and to nothing everywhere else -- off Windows, and inside logos-protocol/logos-qt-sdk/liblogos_core themselves, so the static archives compile byte-identically to before. The dllimport is the load-bearing half, not the export: it rewrites the reference to go through __imp_, so the plain symbol is never undefined and GNU ld never pulls the archive member that would redefine it. Without it the link still succeeds, binds to the archive, and gives no diagnostic at all. logos_shared_api.h records both wrong answers -- export everything (collides with the static archive over LogosAPI) and export nothing (today's silent per-image statics) -- so neither gets reinvented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(windows): let checks and devShells take the arg forAllSystems now passes The cross-target commit added `inherit system;` to forAllSystems so the Windows arm could tell which target it was building, but left `checks` and `devShells` on the strict `({ pkgs }: ...)` pattern. A Nix attrset pattern without `...` is exact, so both stopped evaluating: error: function 'anonymous lambda' called with unexpected argument 'system' on EVERY platform, not just Windows -- `nix flake check` and `ws develop logos-protocol` are dead on this branch while they work on master. `packages` was unaffected because it goes through forAllTargets, which is why nothing caught it. Measured, same worktree, before and after: before: checks.aarch64-darwin -> the error above at flake.nix:52 after: checks.aarch64-darwin -> [ "tests" ] devShells.aarch64-darwin.default.name -> "nix-shell" packages -> [ aarch64-darwin aarch64-linux x86_64-darwin x86_64-linux x86_64-windows ] * chore(deps): re-pin logos-nix to the merged Windows overlay The cross overlay landed in logos-nix#2. This branch was locked to a pre-merge rev, which has no `lib.forAllTargets` and no `lib.mkWindowsPkgs`, so it could not evaluate standalone -- only against the unmerged branch. Level 2 of the Windows chain; L1 (logos-nix) is merged. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
07745712e8
commit
03842db5c1
+42
-6
@@ -11,10 +11,23 @@ set(CMAKE_AUTOMOC ON)
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core RemoteObjects)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core RemoteObjects)
|
||||
|
||||
# Plain-C++ transport dependencies (no Qt). The `system` component is
|
||||
# required for boost::system::error_code (Boost.Asio); without it
|
||||
# Boost::system isn't an exported imported target on Boost 1.87+.
|
||||
find_package(Boost REQUIRED COMPONENTS system)
|
||||
# Plain-C++ transport dependencies (no Qt).
|
||||
#
|
||||
# Boost.System is header-only, but Boost 1.87 only EXPORTS the Boost::system
|
||||
# imported target when the component is requested -- hence the explicit
|
||||
# COMPONENTS below. Boost 1.89 then removed the compiled boost_system library
|
||||
# outright, so there is no boost_systemConfig.cmake at all and requesting it as
|
||||
# REQUIRED hard-fails ("Could not find a package configuration file provided by
|
||||
# boost_system"). Ask for it optionally and fall back to Boost::headers, which
|
||||
# carries the same header-only error_code either way.
|
||||
find_package(Boost REQUIRED)
|
||||
find_package(Boost QUIET OPTIONAL_COMPONENTS system)
|
||||
if(TARGET Boost::system)
|
||||
set(LOGOS_BOOST_SYSTEM Boost::system)
|
||||
else()
|
||||
set(LOGOS_BOOST_SYSTEM Boost::headers)
|
||||
endif()
|
||||
message(STATUS "Boost.System target: ${LOGOS_BOOST_SYSTEM} (Boost ${Boost_VERSION})")
|
||||
find_package(OpenSSL REQUIRED)
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
|
||||
@@ -22,6 +35,7 @@ set(PROTOCOL_SOURCES
|
||||
logos_protocol.h
|
||||
logos_module_impl.h
|
||||
logos_call_error.h
|
||||
logos_shared_api.h
|
||||
logos_protocol.cpp
|
||||
logos_types.cpp
|
||||
logos_types.h
|
||||
@@ -101,14 +115,27 @@ add_library(logos_protocol STATIC ${PROTOCOL_SOURCES})
|
||||
# Link Qt + plain-C++ transport deps. Boost.Asio is header-only
|
||||
# *except* for `boost::system::error_code` (and friends), which lives
|
||||
# in libboost_system — see the matching note in logos-cpp-sdk.
|
||||
# Boost.Asio's IOCP backend calls the Winsock EXTENSION functions AcceptEx and
|
||||
# GetAcceptExSockaddrs, which live in mswsock rather than ws2_32. Without this
|
||||
# the plain TCP transport compiles cleanly and then fails at link with
|
||||
# "undefined reference to `AcceptEx'" out of
|
||||
# win_iocp_socket_service_base::start_accept_op. ws2_32 supplies core Winsock
|
||||
# and is listed explicitly rather than relied upon arriving via Qt::Network.
|
||||
if(WIN32)
|
||||
set(LOGOS_PROTOCOL_PLATFORM_LIBS ws2_32 mswsock)
|
||||
else()
|
||||
set(LOGOS_PROTOCOL_PLATFORM_LIBS "")
|
||||
endif()
|
||||
|
||||
target_link_libraries(logos_protocol PUBLIC
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::RemoteObjects
|
||||
Boost::headers
|
||||
Boost::system
|
||||
${LOGOS_BOOST_SYSTEM}
|
||||
OpenSSL::SSL
|
||||
OpenSSL::Crypto
|
||||
nlohmann_json::nlohmann_json
|
||||
${LOGOS_PROTOCOL_PLATFORM_LIBS}
|
||||
)
|
||||
|
||||
target_include_directories(logos_protocol PUBLIC
|
||||
@@ -150,12 +177,17 @@ target_link_libraries(logos_protocol_shared PUBLIC
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::RemoteObjects
|
||||
Boost::headers
|
||||
Boost::system
|
||||
${LOGOS_BOOST_SYSTEM}
|
||||
OpenSSL::SSL
|
||||
OpenSSL::Crypto
|
||||
nlohmann_json::nlohmann_json
|
||||
${LOGOS_PROTOCOL_PLATFORM_LIBS}
|
||||
)
|
||||
|
||||
# Only the SHARED build marks the lp_* C ABI dllexport; the static archive
|
||||
# leaves LP_API empty so consumers linking it need no import library.
|
||||
target_compile_definitions(logos_protocol_shared PRIVATE LOGOS_PROTOCOL_BUILDING_SHARED)
|
||||
|
||||
target_include_directories(logos_protocol_shared PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/qt_local>
|
||||
@@ -210,6 +242,10 @@ install(FILES
|
||||
logos_codec.h
|
||||
logos_call_error.h
|
||||
logos_rpc_status.h
|
||||
# Windows single-provider marker. token_manager.h / logos_api_client.h /
|
||||
# logos_types.h include it, and so does logos-qt-sdk's logos_api.h, so it
|
||||
# has to ship even though nothing in this library's own sources needs it.
|
||||
logos_shared_api.h
|
||||
logos_types.h
|
||||
logos_api_client.h
|
||||
logos_api_consumer.h
|
||||
|
||||
@@ -14,7 +14,15 @@ include(CMakeFindDependencyMacro)
|
||||
find_dependency(Qt6 REQUIRED COMPONENTS Core RemoteObjects)
|
||||
# `system` for boost::system::error_code (Boost.Asio) — see the
|
||||
# matching note in cpp/CMakeLists.txt next to target_link_libraries.
|
||||
find_dependency(Boost REQUIRED COMPONENTS system)
|
||||
# Boost.System is header-only, and Boost 1.89 removed the compiled
|
||||
# boost_system library outright -- so requiring it as a COMPONENT hard-fails
|
||||
# there ("Could not find a package configuration file provided by
|
||||
# boost_system"). Boost 1.87 does still need the explicit request, because it
|
||||
# only exports the Boost::system target when the component is asked for.
|
||||
# Request it optionally; the imported target below already carries whichever
|
||||
# of Boost::system / Boost::headers this build actually linked against.
|
||||
find_dependency(Boost REQUIRED)
|
||||
find_package(Boost QUIET OPTIONAL_COMPONENTS system)
|
||||
find_dependency(OpenSSL REQUIRED)
|
||||
find_dependency(nlohmann_json REQUIRED)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#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 <nlohmann/json.hpp>
|
||||
@@ -27,8 +28,15 @@ class TokenManager;
|
||||
*
|
||||
* 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 LogosAPIClient : public QObject
|
||||
class LOGOS_SHARED_API LogosAPIClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
|
||||
+49
-22
@@ -69,6 +69,33 @@
|
||||
#define LOGOS_PROTOCOL_VERSION_PATCH 0
|
||||
#define LOGOS_PROTOCOL_VERSION_STRING "0.2.0"
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Export marking.
|
||||
*
|
||||
* These lp_* functions are the stable C ABI the JS and Rust SDKs bind to, so
|
||||
* they must appear in the export table of the shared build (liblogos_protocol
|
||||
* .dll / .so). On Windows that is not automatic: CMake builds shared libraries
|
||||
* with symbol export disabled unless symbols are marked explicitly or
|
||||
* WINDOWS_EXPORT_ALL_SYMBOLS is set -- the cross-built DLL was measured with
|
||||
* ZERO exports before this macro existed, so every FFI consumer would have
|
||||
* failed to bind at load time.
|
||||
*
|
||||
* Marked explicitly rather than via WINDOWS_EXPORT_ALL_SYMBOLS so the ABI
|
||||
* surface is the one we declare, not whatever happens to have external
|
||||
* linkage. Mirrors logos_module_impl.h's LOGOS_MODULE_IMPL_EXPORT.
|
||||
* ------------------------------------------------------------------------- */
|
||||
#if defined(_WIN32)
|
||||
#if defined(LOGOS_PROTOCOL_BUILDING_SHARED)
|
||||
#define LP_API __declspec(dllexport)
|
||||
#else
|
||||
/* Consumers get plain declarations: dllimport would force them to link the
|
||||
* import library even when they use the static archive. */
|
||||
#define LP_API
|
||||
#endif
|
||||
#else
|
||||
#define LP_API __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -88,18 +115,18 @@ extern "C" {
|
||||
|
||||
/** Version string "MAJOR.MINOR.PATCH" of the linked logos-protocol.
|
||||
* Returns a static string — do NOT free. */
|
||||
const char* lp_protocol_version(void);
|
||||
LP_API const char* lp_protocol_version(void);
|
||||
|
||||
/** MAJOR component of the linked logos-protocol version. Equal majors are
|
||||
* compatible; unequal majors are not. */
|
||||
int lp_protocol_abi_major(void);
|
||||
LP_API int lp_protocol_abi_major(void);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Memory
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/** Free a string returned by this library. Safe to call with NULL. */
|
||||
void lp_string_free(char* s);
|
||||
LP_API void lp_string_free(char* s);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Process-global mode / transport defaults
|
||||
@@ -108,10 +135,10 @@ void lp_string_free(char* s);
|
||||
/** Set the process-wide communication mode: "remote" (IPC, default),
|
||||
* "local" (in-process registry) or "mock" (in-memory, for tests).
|
||||
* Returns LP_OK or LP_ERR_INVALID_ARG. */
|
||||
int lp_set_mode(const char* mode);
|
||||
LP_API int lp_set_mode(const char* mode);
|
||||
|
||||
/** Current mode as "remote" | "local" | "mock". Static string — do not free. */
|
||||
const char* lp_get_mode(void);
|
||||
LP_API const char* lp_get_mode(void);
|
||||
|
||||
/** Set the process-global default transport from a JSON object, e.g.
|
||||
* {"protocol":"local"}
|
||||
@@ -119,7 +146,7 @@ const char* lp_get_mode(void);
|
||||
* {"protocol":"tcp_ssl","host":"...","port":6443,"codec":"cbor",
|
||||
* "ca_file":"...","cert_file":"...","key_file":"...","verify_peer":true}
|
||||
* Returns LP_OK or LP_ERR_INVALID_ARG on parse failure. */
|
||||
int lp_set_default_transport(const char* transport_json);
|
||||
LP_API int lp_set_default_transport(const char* transport_json);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Consumer: clients, invoke, subscribe
|
||||
@@ -156,14 +183,14 @@ typedef void (*lp_event_cb)(const char* event_name, const char* data_json,
|
||||
*
|
||||
* Returns NULL on invalid arguments.
|
||||
*/
|
||||
lp_client* lp_client_create(const char* target_module,
|
||||
LP_API lp_client* lp_client_create(const char* target_module,
|
||||
const char* origin_module,
|
||||
const char* target_transport_json,
|
||||
const char* capability_transport_json);
|
||||
|
||||
/** Destroy a client. After this returns, no further callbacks fire for the
|
||||
* client or its subscriptions. */
|
||||
void lp_client_destroy(lp_client* client);
|
||||
LP_API void lp_client_destroy(lp_client* client);
|
||||
|
||||
/**
|
||||
* Call `method` on the client's target module, blocking until the result
|
||||
@@ -178,7 +205,7 @@ void lp_client_destroy(lp_client* client);
|
||||
* On failure: *out_error_json (if non-NULL) receives the canonical error
|
||||
* object. Both out-strings are owned by the caller (lp_string_free).
|
||||
*/
|
||||
int lp_invoke(lp_client* client,
|
||||
LP_API int lp_invoke(lp_client* client,
|
||||
const char* method,
|
||||
const char* args_json,
|
||||
int timeout_ms,
|
||||
@@ -213,7 +240,7 @@ int lp_invoke(lp_client* client,
|
||||
* Argument/handle validation still fails synchronously with
|
||||
* LP_ERR_INVALID_ARG and `cb` is NOT called in that case.
|
||||
*/
|
||||
int lp_invoke_async(lp_client* client,
|
||||
LP_API int lp_invoke_async(lp_client* client,
|
||||
const char* method,
|
||||
const char* args_json,
|
||||
int timeout_ms,
|
||||
@@ -240,7 +267,7 @@ int lp_invoke_async(lp_client* client,
|
||||
*
|
||||
* Returns NULL only for a null/empty client, event name or callback.
|
||||
*/
|
||||
lp_subscription* lp_subscribe(lp_client* client,
|
||||
LP_API lp_subscription* lp_subscribe(lp_client* client,
|
||||
const char* event_name,
|
||||
lp_event_cb cb,
|
||||
void* user_data);
|
||||
@@ -262,7 +289,7 @@ lp_subscription* lp_subscribe(lp_client* client,
|
||||
* just-cancelled subscription until the owner thread runs. If the client is
|
||||
* destroyed first the cancellation simply never runs, which is correct — the
|
||||
* registry died with it. */
|
||||
void lp_unsubscribe(lp_subscription* sub);
|
||||
LP_API void lp_unsubscribe(lp_subscription* sub);
|
||||
|
||||
/** Diagnostics: a JSON array of "<module>::<event>" for every subscription on
|
||||
* this client that has been accepted but has not armed yet — i.e. is waiting
|
||||
@@ -272,12 +299,12 @@ void lp_unsubscribe(lp_subscription* sub);
|
||||
* ABI had none, which is precisely why a subscription that silently never
|
||||
* armed was undetectable from Rust, Nim or a universal C++ module. Caller
|
||||
* frees via lp_string_free; NULL only for a null client. */
|
||||
char* lp_pending_subscriptions(lp_client* client);
|
||||
LP_API char* lp_pending_subscriptions(lp_client* client);
|
||||
|
||||
/** Introspect the target module's methods/events as a JSON array (the
|
||||
* same shape `lm` prints). Caller frees via lp_string_free. NULL on
|
||||
* failure. */
|
||||
char* lp_get_methods(lp_client* client);
|
||||
LP_API char* lp_get_methods(lp_client* client);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Tokens
|
||||
@@ -285,14 +312,14 @@ char* lp_get_methods(lp_client* client);
|
||||
|
||||
/** Get the stored token for `module_name`. Returns NULL when absent;
|
||||
* caller frees via lp_string_free. */
|
||||
char* lp_token_get(const char* module_name);
|
||||
LP_API char* lp_token_get(const char* module_name);
|
||||
|
||||
/** Store a token for `module_name`. */
|
||||
int lp_token_save(const char* module_name, const char* token);
|
||||
LP_API int lp_token_save(const char* module_name, const char* token);
|
||||
|
||||
/** Deliver a module token to the client's target (the consumer-side
|
||||
* `informModuleToken`). Returns LP_OK when the target accepted it. */
|
||||
int lp_inform_module_token(lp_client* client,
|
||||
LP_API int lp_inform_module_token(lp_client* client,
|
||||
const char* auth_token,
|
||||
const char* module_name,
|
||||
const char* token);
|
||||
@@ -318,18 +345,18 @@ typedef char* (*lp_getmethods_cb)(void* user_data);
|
||||
typedef int (*lp_token_cb)(const char* module_name, const char* token,
|
||||
void* user_data);
|
||||
|
||||
lp_provider* lp_provider_create(const char* module_name,
|
||||
LP_API lp_provider* lp_provider_create(const char* module_name,
|
||||
const char* transport_set_json);
|
||||
void lp_provider_destroy(lp_provider* provider);
|
||||
int lp_provider_register(lp_provider* provider,
|
||||
LP_API void lp_provider_destroy(lp_provider* provider);
|
||||
LP_API int lp_provider_register(lp_provider* provider,
|
||||
lp_dispatch_cb dispatch,
|
||||
lp_getmethods_cb get_methods,
|
||||
lp_token_cb on_token,
|
||||
void* user_data);
|
||||
int lp_provider_emit_event(lp_provider* provider,
|
||||
LP_API int lp_provider_emit_event(lp_provider* provider,
|
||||
const char* event_name,
|
||||
const char* data_json);
|
||||
int lp_provider_save_token(lp_provider* provider,
|
||||
LP_API int lp_provider_save_token(lp_provider* provider,
|
||||
const char* module_name,
|
||||
const char* token);
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef LOGOS_SHARED_API_H
|
||||
#define LOGOS_SHARED_API_H
|
||||
|
||||
/**
|
||||
* @file logos_shared_api.h
|
||||
* @brief Marks the runtime types that must exist EXACTLY ONCE per process.
|
||||
*
|
||||
* ELF and Mach-O give this for free. Both formats interpose symbols across the
|
||||
* whole process image set, so when liblogos_core.{so,dylib} exports
|
||||
* TokenManager::instance() every other image in the process — the host binary,
|
||||
* the UI plugin — binds to that one definition and the function-local
|
||||
* `static TokenManager instance;` inside it is genuinely a singleton.
|
||||
*
|
||||
* PE has no interposition. A symbol is either in a DLL's export table and
|
||||
* reached through an import thunk, or it is resolved image-locally; there is no
|
||||
* "first definition wins across the process" rule. So on Windows every image
|
||||
* that links liblogos_protocol.a / liblogos_qt_sdk.a statically gets its OWN
|
||||
* copy of the code, and therefore its own copy of every function-local static
|
||||
* inside it. Measured on the Basecamp payload: nine images each define
|
||||
* TokenManager::instance()::instance. The host saves a capability token into
|
||||
* its copy, the UI plugin reads its own empty copy, and every cross-module call
|
||||
* is refused ("ModuleProxy: rejecting unauthorized call") — the package manager
|
||||
* never appears in the sidebar.
|
||||
*
|
||||
* The obvious fixes are both wrong, and the wrongness is not obvious, so:
|
||||
*
|
||||
* - Exporting everything from liblogos_core (-Wl,--export-all-symbols) makes
|
||||
* its import library a second definition of symbols that liblogos_qt_sdk.a
|
||||
* also defines, and the link dies with "multiple definition of
|
||||
* `LogosAPI::LogosAPI'". See the note in logos-liblogos/src/CMakeLists.txt.
|
||||
* - Exporting nothing (today's C-API-only narrowing) links, and silently
|
||||
* gives every image its own statics. That is the bug above.
|
||||
*
|
||||
* The resolution is ONE PROVIDER: liblogos_core.dll exports these types
|
||||
* explicitly (a generated .def, see logos-liblogos/cmake/gen-shared-exports.sh)
|
||||
* and the in-process consumers — LogosBasecamp.exe and main_ui.dll — compile
|
||||
* with LOGOS_SHARED_USE_DLL so their references become `__declspec(dllimport)`.
|
||||
*
|
||||
* The dllimport is the load-bearing half, not the export. It rewrites the
|
||||
* reference to go through `__imp_`, so the plain symbol is never undefined and
|
||||
* GNU ld never pulls the archive member that would have redefined it —
|
||||
* regardless of where the static archive sits on the link line. Without it the
|
||||
* link still succeeds and binds to the archive, with no diagnostic at all.
|
||||
*
|
||||
* Everything here is deliberately a no-op unless a consumer opts in:
|
||||
*
|
||||
* - Off Windows the macro is empty; ELF/Mach-O already do the right thing.
|
||||
* - Inside logos-protocol / logos-qt-sdk / liblogos_core the macro is empty,
|
||||
* so the static archives are compiled byte-identically to before and the
|
||||
* export side is driven purely by the .def at liblogos_core's link.
|
||||
* - logos_host.exe, ui-host.exe and the module plugin DLLs do not define
|
||||
* LOGOS_SHARED_USE_DLL. They are separate processes that do not load
|
||||
* liblogos_core, so they keep their own (correct, per-process) statics.
|
||||
*/
|
||||
#if defined(_WIN32) && defined(LOGOS_SHARED_USE_DLL)
|
||||
# define LOGOS_SHARED_API __declspec(dllimport)
|
||||
#else
|
||||
# define LOGOS_SHARED_API
|
||||
#endif
|
||||
|
||||
#endif // LOGOS_SHARED_API_H
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <grp.h>
|
||||
@@ -15,9 +16,66 @@
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace logos {
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
// On Windows the local transport is backed by NAMED PIPES: QLocalServer maps a
|
||||
// server name to \\.\pipe\<name>. Every assumption this file is built on is a
|
||||
// unix-domain-socket assumption, and none of them survives the move:
|
||||
//
|
||||
// * a pipe has no filesystem inode, so there is nothing to lstat, chown or
|
||||
// chmod — access is governed by a security descriptor supplied at
|
||||
// CreateNamedPipe time, not by mode bits;
|
||||
// * a pipe instance ceases to exist when its last handle closes, so a
|
||||
// hard-killed process cannot leave a stale endpoint behind. There is no
|
||||
// equivalent of the /tmp/logos_<name>_<instance> litter the reaper exists
|
||||
// to clean up.
|
||||
//
|
||||
// So isSocketDead/reapStaleSockets are not merely unimplemented here, they are
|
||||
// vacuous: the condition they detect cannot arise.
|
||||
|
||||
bool applySocketPerms(const std::string& absPath, std::string* errOut)
|
||||
{
|
||||
const char* grpEnv = std::getenv("LOGOS_SOCKET_GROUP");
|
||||
const char* modeEnv = std::getenv("LOGOS_SOCKET_MODE");
|
||||
const bool wantGroup = grpEnv && *grpEnv;
|
||||
const bool wantMode = modeEnv && *modeEnv;
|
||||
|
||||
// Policy unset is the overwhelmingly common case and is a genuine no-op.
|
||||
if (!wantGroup && !wantMode) return true;
|
||||
|
||||
// Policy SET, though, is a request we cannot honour. Returning true here
|
||||
// would silently widen access relative to what the operator asked for --
|
||||
// the one direction this file is careful never to go (see the chgrp-then-
|
||||
// chmod ordering in the POSIX branch). Granting a Windows pipe to a group
|
||||
// means building a DACL and resolving the group to a SID; until that
|
||||
// exists, refuse loudly rather than pretend.
|
||||
if (errOut) {
|
||||
*errOut = "LOGOS_SOCKET_GROUP/LOGOS_SOCKET_MODE are not supported on "
|
||||
"Windows: the local transport uses named pipes, which carry a "
|
||||
"security descriptor rather than owner/group/mode. Unset them, "
|
||||
"or run the node per-user (%LOCALAPPDATA%). Path: " + absPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isSocketDead(const std::string& /*absPath*/)
|
||||
{
|
||||
// Fail closed, matching the documented contract: never report an endpoint
|
||||
// dead unless certain. A named pipe that still exists still has an owner.
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t reapStaleSockets(const std::string& /*dir*/, const std::string& /*prefix*/)
|
||||
{
|
||||
return 0; // named pipes leave nothing behind to reap
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
namespace {
|
||||
|
||||
// Resolve a "group" env value to a gid. Accepts an all-digits string as a
|
||||
@@ -168,4 +226,6 @@ std::size_t reapStaleSockets(const std::string& dir, const std::string& prefix)
|
||||
return removed;
|
||||
}
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
} // namespace logos
|
||||
|
||||
@@ -6,10 +6,16 @@
|
||||
|
||||
// Qt-free helpers for making the unix-domain sockets that back the local
|
||||
// transport shareable across OS users, and for cleaning up the stale socket
|
||||
// files that a hard-killed process leaves behind. Deliberately depends on
|
||||
// nothing but POSIX + the C++ standard library so it can be used from both the
|
||||
// Qt (qt_remote) and the Qt-free (plain) transport code paths, and unit-tested
|
||||
// on its own.
|
||||
// files that a hard-killed process leaves behind. Depends on nothing but POSIX
|
||||
// (or, on Windows, nothing at all) plus the C++ standard library, so it can be
|
||||
// used from both the Qt (qt_remote) and the Qt-free (plain) transport code
|
||||
// paths, and unit-tested on its own.
|
||||
//
|
||||
// ON WINDOWS the local transport is named pipes, not unix sockets, and every
|
||||
// one of these operations changes character — see the block comment at the top
|
||||
// of the .cpp. In short: there is no inode to chmod, and a pipe cannot outlive
|
||||
// its last handle, so there is never anything stale to reap. Each function's
|
||||
// Windows behaviour is called out below.
|
||||
//
|
||||
// The policy is read from the environment so every process in a Logos node's
|
||||
// tree — the daemon, the module subprocesses (logos_host), any child it spawns
|
||||
@@ -35,6 +41,12 @@ namespace logos {
|
||||
// Returns true when every requested change succeeded (or nothing was
|
||||
// requested). On failure returns false and, if `errOut` is non-null, writes a
|
||||
// human-readable reason.
|
||||
//
|
||||
// Windows: returns true when no policy is requested, and FAILS with an
|
||||
// explanatory error when LOGOS_SOCKET_GROUP/LOGOS_SOCKET_MODE are set --
|
||||
// honouring them would need a pipe DACL and a group->SID resolver. Refusing
|
||||
// loudly is deliberate: silently returning true would leave the endpoint more
|
||||
// permissive than the operator asked for.
|
||||
bool applySocketPerms(const std::string& absPath, std::string* errOut = nullptr);
|
||||
|
||||
// True iff `absPath` is a unix socket that WE own and whose listener is
|
||||
@@ -45,12 +57,17 @@ bool applySocketPerms(const std::string& absPath, std::string* errOut = nullptr)
|
||||
// and returns false. The predicate fails closed: it never reports a socket dead
|
||||
// unless it is sure, so a reaper built on it cannot unlink a live endpoint or a
|
||||
// regular file that merely shares the name.
|
||||
//
|
||||
// Windows: always false. A named pipe stops existing when its last handle
|
||||
// closes, so a "dead but present" endpoint is not a state that can occur.
|
||||
bool isSocketDead(const std::string& absPath);
|
||||
|
||||
// Unlink every entry `<dir>/<prefix>...` for which isSocketDead() returns true.
|
||||
// Regular files are never removed (only S_ISSOCK inodes pass the predicate), so
|
||||
// a large build artefact that happens to match the prefix is safe. Returns the
|
||||
// number of socket files actually unlinked. A missing/unreadable `dir` yields 0.
|
||||
//
|
||||
// Windows: always 0 -- nothing is ever left behind to reap.
|
||||
std::size_t reapStaleSockets(const std::string& dir, const std::string& prefix);
|
||||
|
||||
} // namespace logos
|
||||
|
||||
+11
-3
@@ -5,6 +5,8 @@
|
||||
#include <QVariant>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "logos_shared_api.h"
|
||||
|
||||
class LogosResultException : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
@@ -131,8 +133,14 @@ struct LogosResult
|
||||
}
|
||||
};
|
||||
|
||||
// Provide (de)serialisation for being use as Remote Object
|
||||
QDataStream &operator<<(QDataStream &out, const LogosResult &result);
|
||||
QDataStream &operator>>(QDataStream &in, LogosResult &result);
|
||||
// Provide (de)serialisation for being use as Remote Object.
|
||||
//
|
||||
// LOGOS_SHARED_API not because these hold state, but because they are the only
|
||||
// out-of-line symbols in logos_types.cpp.obj: leaving them un-imported lets ld
|
||||
// pull that object into a Windows consumer, and archive pull-in is transitive.
|
||||
// The single-provider rule is per object file, not per class — see
|
||||
// logos_shared_api.h.
|
||||
LOGOS_SHARED_API QDataStream &operator<<(QDataStream &out, const LogosResult &result);
|
||||
LOGOS_SHARED_API QDataStream &operator>>(QDataStream &in, LogosResult &result);
|
||||
|
||||
#endif
|
||||
|
||||
+9
-1
@@ -9,6 +9,8 @@
|
||||
#include <QCryptographicHash>
|
||||
#include <string>
|
||||
|
||||
#include "logos_shared_api.h"
|
||||
|
||||
/**
|
||||
* @brief Render a capability/auth token safe to write to logs.
|
||||
*
|
||||
@@ -38,8 +40,14 @@ inline QString redactToken(const QString& token)
|
||||
*
|
||||
* This class manages a collection of tokens identified by keys, providing thread-safe
|
||||
* access to store, retrieve, and manage tokens throughout the application lifecycle.
|
||||
*
|
||||
* LOGOS_SHARED_API: the singleton below is a function-local static, so it is one
|
||||
* per copy of the code. On PE that means one per IMAGE unless the consumer
|
||||
* imports it from liblogos_core.dll — which is exactly the token-invisibility
|
||||
* bug described in logos_shared_api.h. Off Windows, and inside the provider,
|
||||
* this expands to nothing.
|
||||
*/
|
||||
class TokenManager : public QObject
|
||||
class LOGOS_SHARED_API TokenManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
|
||||
Generated
+21
-4
@@ -2,14 +2,15 @@
|
||||
"nodes": {
|
||||
"logos-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-windows": "nixpkgs-windows"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774455309,
|
||||
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
|
||||
"lastModified": 1786399295,
|
||||
"narHash": "sha256-Bl1A0UgsIXioZw5uEW8Jl5u7FfZcFMnpYlpsdezFwK0=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
|
||||
"rev": "6e0f4a7120fced10829b0b3a698ff619a11d4605",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -34,6 +35,22 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-windows": {
|
||||
"locked": {
|
||||
"lastModified": 1782723713,
|
||||
"narHash": "sha256-oPXCU/SSUokcGaJREHibG1CBX3+s/W7orDWQOZDsEeQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b5aa0fbd538984f6e3d201be0005b4463d8b09f8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b5aa0fbd538984f6e3d201be0005b4463d8b09f8",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix",
|
||||
|
||||
@@ -8,11 +8,17 @@
|
||||
let
|
||||
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
|
||||
inherit system;
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
});
|
||||
|
||||
# Adds the "x86_64-windows" pseudo-system. A cross derivation's `system`
|
||||
# attribute is its BUILD platform, so packages.x86_64-windows.* evaluates
|
||||
# anywhere but realises on x86_64-linux.
|
||||
forAllTargets = logos-nix.lib.forAllTargets;
|
||||
in
|
||||
{
|
||||
packages = forAllSystems ({ pkgs }:
|
||||
packages = forAllTargets ({ pkgs, ... }:
|
||||
let
|
||||
common = import ./nix/default.nix { inherit pkgs; };
|
||||
src = ./.;
|
||||
@@ -43,7 +49,7 @@
|
||||
}
|
||||
);
|
||||
|
||||
checks = forAllSystems ({ pkgs }:
|
||||
checks = forAllSystems ({ pkgs, ... }:
|
||||
let
|
||||
common = import ./nix/default.nix { inherit pkgs; };
|
||||
src = ./.;
|
||||
@@ -54,7 +60,7 @@
|
||||
}
|
||||
);
|
||||
|
||||
devShells = forAllSystems ({ pkgs }: {
|
||||
devShells = forAllSystems ({ pkgs, ... }: {
|
||||
default = pkgs.mkShell {
|
||||
nativeBuildInputs = [
|
||||
pkgs.cmake
|
||||
|
||||
+17
-4
@@ -1,8 +1,16 @@
|
||||
# Common build configuration shared across all packages
|
||||
{ pkgs }:
|
||||
|
||||
let
|
||||
# wrapQtAppsNoGuiHook does not even EVALUATE for a mingw host, and would be
|
||||
# inert anyway: wrap-qt-apps-hook.sh skips anything that is not ELF or
|
||||
# Mach-O, so a PE is never wrapped. qtbase's own setup hook then hard-errors
|
||||
# unless dontWrapQtApps is set -- hence both halves of this, not just one.
|
||||
isWindows = pkgs.stdenv.hostPlatform.isWindows;
|
||||
in
|
||||
{
|
||||
pname = "logos-protocol";
|
||||
inherit isWindows;
|
||||
version = "0.2.0";
|
||||
|
||||
# Common native build inputs
|
||||
@@ -10,8 +18,8 @@
|
||||
pkgs.cmake
|
||||
pkgs.ninja
|
||||
pkgs.pkg-config
|
||||
pkgs.qt6.wrapQtAppsNoGuiHook
|
||||
];
|
||||
]
|
||||
++ pkgs.lib.optional (!isWindows) pkgs.qt6.wrapQtAppsNoGuiHook;
|
||||
|
||||
# Common runtime dependencies. Qt is an implementation detail of the
|
||||
# qt_remote (QRO) / qt_local transports — the public C ABI is Qt-free.
|
||||
@@ -39,11 +47,16 @@
|
||||
];
|
||||
|
||||
# Common CMake flags
|
||||
cmakeFlags = [ "-GNinja" ];
|
||||
cmakeFlags = [ "-GNinja" ]
|
||||
# Qt splits its host TOOLS (repc, moc, qmltyperegistrar) into separate
|
||||
# packages that must RUN on the build machine; logos-nix's Windows overlay
|
||||
# exposes the flags that point Qt at them. Absent -- and so empty -- on a
|
||||
# native build, which is why this needs no isWindows guard.
|
||||
++ (pkgs.logosQtCrossCmakeFlags or [ ]);
|
||||
|
||||
# Metadata
|
||||
meta = with pkgs.lib; {
|
||||
description = "Logos protocol — transports, token exchange and the language-neutral lp_* C ABI";
|
||||
platforms = platforms.unix;
|
||||
platforms = platforms.unix ++ platforms.windows;
|
||||
};
|
||||
}
|
||||
|
||||
+22
-1
@@ -16,13 +16,34 @@ pkgs.stdenv.mkDerivation {
|
||||
propagatedBuildInputs = common.propagatedBuildInputs;
|
||||
|
||||
dontUseCmakeConfigure = true;
|
||||
# Required whenever the Qt wrapper hooks are absent (Windows) -- qtbase's
|
||||
# setup hook errors out in qtPreHook otherwise.
|
||||
dontWrapQtApps = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
mkdir -p build-protocol
|
||||
cd build-protocol
|
||||
cmake ../cpp -GNinja -DCMAKE_INSTALL_PREFIX=$out $cmakeFlags
|
||||
|
||||
# This derivation sets dontUseCmakeConfigure and invokes cmake by hand, so
|
||||
# nixpkgs' cmakeConfigurePhase never runs -- and with it the step that
|
||||
# gives cmake a CMAKE_PREFIX_PATH covering the buildInputs. That goes
|
||||
# unnoticed natively but breaks the cross build: nixpkgs puts every Qt
|
||||
# module in its own store path, Qt6Config resolves components through
|
||||
# CMAKE_PREFIX_PATH, and with it empty `find_package(Qt6 COMPONENTS
|
||||
# RemoteObjects)` looks only under qtbase's own prefix and fails with
|
||||
# "Expected Config file at <qtbase>/lib/cmake/Qt6RemoteObjects ... does
|
||||
# NOT exist".
|
||||
#
|
||||
# nixpkgs does populate QT_ADDITIONAL_PACKAGES_PREFIX_PATH with each Qt
|
||||
# module's prefix (this Qt's Qt6Config does not read that variable), so
|
||||
# reuse it. Colon-separated in the environment, semicolon-separated as a
|
||||
# CMake list.
|
||||
qtPrefixes="$(printf '%s' "''${CMAKE_PREFIX_PATH-}:''${QT_ADDITIONAL_PACKAGES_PREFIX_PATH-}" \
|
||||
| tr ':' ';' | sed 's/^;*//; s/;*$//; s/;;*/;/g')"
|
||||
cmake ../cpp -GNinja -DCMAKE_INSTALL_PREFIX=$out \
|
||||
''${qtPrefixes:+-DCMAKE_PREFIX_PATH="$qtPrefixes"} $cmakeFlags
|
||||
ninja
|
||||
cd ..
|
||||
|
||||
|
||||
Reference in New Issue
Block a user