mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 20:11:07 +00:00
* 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>
76 lines
4.0 KiB
C++
76 lines
4.0 KiB
C++
#ifndef LOGOS_SOCKET_PATHS_H
|
|
#define LOGOS_SOCKET_PATHS_H
|
|
|
|
#include <cstddef>
|
|
#include <string>
|
|
|
|
// 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. 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
|
|
// — applies the same rule to every socket it binds without the config having to
|
|
// be threaded through each layer. The daemon exports the vars once next to
|
|
// LOGOS_INSTANCE_ID and they propagate by inheritance.
|
|
namespace logos {
|
|
|
|
// Apply the socket-access policy to an already-bound unix-domain socket file at
|
|
// the ABSOLUTE path `absPath`. Reads two environment variables:
|
|
//
|
|
// LOGOS_SOCKET_GROUP — group name or numeric gid; the socket is chgrp'd to it.
|
|
// LOGOS_SOCKET_MODE — octal mode (e.g. "0660"); the socket is chmod'd to it.
|
|
//
|
|
// Order is chgrp-then-chmod so the transient state is never MORE permissive
|
|
// than the final target (a half-applied policy can only ever be too strict,
|
|
// never too loose — it opens nothing). An unset/empty var leaves that attribute
|
|
// untouched, so the default behaviour (no env) is a no-op and the socket keeps
|
|
// the kernel's `0777 & ~umask` mode. Connecting to an AF_UNIX socket needs
|
|
// WRITE permission on the file, so 0660 is what grants a group member access;
|
|
// the execute bit on a socket inode is inert.
|
|
//
|
|
// 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
|
|
// provably gone. Requires all of: the path exists and is a socket (S_ISSOCK),
|
|
// its owner is our effective uid, and a non-blocking connect() returns
|
|
// ECONNREFUSED or ENOENT. Any other outcome — a successful connect, EACCES,
|
|
// EAGAIN, ETIMEDOUT, a non-socket inode, a foreign owner — is treated as ALIVE
|
|
// 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
|
|
|
|
#endif // LOGOS_SOCKET_PATHS_H
|