The host was extracted from liblogos into logos-module-loader-qt (#149/#151/#152),
which left liblogos pinned at module-loader-qt 08213eb — a host WITHOUT the
subprocess process-group isolation + parent-death cleanup (the setsid() +
PR_SET_PDEATHSIG + getppid-watchdog that lived on the pre-refactor 272c6b5 lineage).
module-loader-qt #3 (331760d) forward-ports that exact fix into the extracted host
(src/host/logos_host.cpp, +50 lines, no interface change). Bump default-module-loader
08213eb → 331760d so liblogos master once again ships a host that isolates each module
subprocess into its own process group and reaps it on daemon death.
This lets downstream consumers (logoscore-cli, basecamp) track liblogos master and keep
the isolation behavior, instead of pinning the pre-refactor 272c6b5 commit.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* extract subprocess container and container abstraction
* decouple receiving token on a module loader from the container
* move shim subprocess_manager to tests/
* add notes about potentially moving container registration and module_loader registration to the frontends
* extract module loader abstraction and qt module loader
* remove module_name_validation.h; overkill for just one function
* Qt-split retarget + protocol-version load gate
- Link the split SDK stack: logos-qt-sdk (LogosAPI/provider glue; the
logos_sdk alias now points at logos-qt-sdk::logos_qt_sdk, chaining
logos-protocol) + Qt-free logos-cpp-sdk headers.
- Protocol-version load gate (the first real consumer of module
metadata pre-load): ModuleManager reads the module's embedded
logos_protocol_version before runtime.load() and applies the one
compatibility rule — equal protocol MAJOR loads, different MAJOR is
refused with a diagnostic naming both versions, missing/unparseable
stamp (pre-protocol modules) loads permissively with a warning. The
decision logic is std-only (logos_core/protocol_gate.h) and unit
tested (refuse bumped major / warn-load legacy / silent minor skew).
- ModuleDescriptor.rawMetadata is now actually populated for runtimes.
* lock: pin extraction-chain branch revs for standalone CI
Temporary — drop when the chain PRs merge (re-lock against masters).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* doctest: pin logoscore-cli to its qt-split branch head
The doc-test builds logoscore-cli at latest master with only liblogos
overridden to the commit under test; master logoscore-cli cannot build
against qt-split liblogos. Pin the runtime to the chain branch
(logos-co/logos-logoscore-cli#43) so the doc-test exercises the
coherent stack. Temporary — revert to the unpinned URL when the chain
merges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* host: surface the spawn auth token as a LogosAPI property
cdylib-authored modules run their own statically-linked protocol stack
whose TokenManager is a separate copy of the singleton; the generated Qt
glue reads this property (cross-image-safe, like modulePath) and seeds
the cdylib's stack via logos_module_accept_token so the module's
outbound calls authenticate.
* host: set the authToken property before registerObject
registerObject runs the provider object's init() — where the cdylib glue
reads the property. Setting it afterwards meant cdylib modules always saw
an empty token.
* lock: protocol+cpp-sdk merged to master — pins advance (protocol 9de4165, cpp-sdk f0fe8cb, qt-sdk 722e590)
* lock: qt-sdk#1 merged — pin advances to qt-sdk master
* gate: drop QJson from the Qt-free core — parse rawMetadataJson with nlohmann
The protocol-version load gate had pulled QJsonDocument/QJsonObject into
src/logos_core (Qt-free territory). logos-module now exposes the embedded
metadata as a compact JSON string, so the gate reads it via nlohmann and
the std::string extractMetadata overload.
* lock: logos-module b42805d (result-lm untracked)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LoadWithDeps_LoadsInTopologicalOrder pins the load *call sequence* when
loading a module with_dependencies=true. Add a complementary test that
pins the *observable end state* via the public logos_core_is_module_loaded
query: requesting a single top-level module must leave its entire
transitive dependency closure loaded.
Uses a diamond (app -> ui, core; ui -> core) so it also proves a
dependency reachable by two paths is loaded exactly once, not skipped or
double-loaded. This is the guarantee callers actually depend on ("load
app, get everything it needs").
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* simplify c api by merging redudant functions; deprecate more functions no longer in use
simplify c api by merging redudant functions; deprecate more functions no longer in use
update docs
* remove deprecated methods
* fix: ensure consistent token socket path on the sender and receiver end
* address PR #129 review comments
- unix_socket_path.h: replace fixed-size PATH_MAX buffer with a
two-step confstr probe (nullptr/0 to learn the size, then a
std::vector<char>). Apple's per-user temp dir can exceed PATH_MAX
on some configurations; the truncated fallback to /tmp would
reintroduce the exact parent/child path mismatch this helper is
meant to prevent.
- token_receiver.cpp: stack-allocate QLocalServer instead of new +
deleteLater. The Qt event loop is not guaranteed to be running on
the receiver thread, so deleteLater would leak; RAII makes every
exit path (including listen() failure) clean up.
- tests/test_token_exchange.cpp: add a regression test that pins the
TMPDIR-unset behaviour. The new test
RoundTrip_SucceedsWithTmpdirUnset unsets TMPDIR (and
LOGOS_INSTANCE_ID, which earlier tests may have left set via
LogosInstance::id) and asserts both sender and receiver agree on
the helper-resolved socket path. Also re-route the existing
tmpDir() helper through ::logos::qtCompatibleTempDir() so the
instance-id tests stay correct under the same conditions.
`s_processes` is a namespace-scope static (constructed before main),
while `IoRuntime` is a function-local static (constructed lazily on
first use, from main). C++ destroys statics in reverse construction
order, so at exit `~IoRuntime` fires first — tearing down the
asio::io_context and its epoll_reactor — and then `~s_processes`
runs, dropping `shared_ptr<ProcessEntry>`s whose dtors close asio
handles tied to the already-freed reactor. The resulting
use-after-free corrupts the heap and aborts logoscore on shutdown
(SIGABRT 134 / sometimes SIGSEGV 139, glibc reports
"corrupted size vs. prev_size"). Caught by valgrind:
Invalid read of size 1
at boost::asio::detail::epoll_reactor::deregister_descriptor
by io_object_impl::~io_object_impl
by std::_Hashtable<..., shared_ptr<ProcessEntry>, ...>::~_Hashtable
by __cxa_finalize
Address ... free'd
by boost::asio::detail::epoll_reactor::~epoll_reactor
by IoRuntime::~IoRuntime
Move ~IoRuntime out-of-line so it can reach s_processes, and have it
clear the map after stopping the worker thread but before ctx itself
is torn down by the field destructors. Each ProcessEntry's asio
handles are now released against a live reactor; the later
~s_processes then runs against an empty map.
Surfaced by logos-test-modules: 149/158 tests failed in CI
(intermittently 9–149 across machines, depending on malloc layout).
With this fix: 158 passed, 0 failed.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related fixes to the parent-side token handoff to a child module
process, both motivated by races / silent failures observed in the
docker smoke matrix.
1. Replace the hard-coded 10×100ms retry loop with a deadline-driven
loop, default budget 5000ms (configurable via a new max_wait_ms
parameter). The previous 900ms cap was tight enough that on a cold
child — dynamic loader + Qt platform bring-up + CLI11 parse +
plugin loadFromPath — the parent would give up before the child
bound its QtTokenReceiver socket, leaving a half-loaded module
with a misleading "Failed to connect to token socket" error. New
tests pin both ends of the contract:
SendToken_FailsFast_WhenSocketNeverAppears — bails within budget
SendToken_SucceedsAfterDelay — accepts late binders
test_token_exchange's WrongName_FailsCleanlyWithinTimeout bound
loosened from <5000ms to <5500ms because the deadline check can
overshoot by ~one poll interval (50ms) plus syscall slack.
2. Validate the computed Unix socket path against
sockaddr_un::sun_path (~104 bytes on macOS, ~108 on Linux) before
strncpy. Long TMPDIR + module name + LOGOS_INSTANCE_ID combos
would otherwise silently truncate, leaving the parent connecting
to the wrong socket while the child binds the full path. Fail
loudly instead.
* refactor: move logost_host to become one of the possible runtimes
refactor: move logost_host to become one of the possible runtimes
remove shim
revert some unnecessary changes
update docs
update docs
revert unnecessary changes
simplify
* Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* fix stdout and stderr redirection
* address PR review: strip trailing CR on pipe flush + add split-stream test
- handleRead: when flushing a partial line on pipe close, apply the same
trailing-CR trim that the newline loop does.
- tests: add StartProcess_OnOutput_SplitsStdoutAndStderr to assert that
both pipes drain and onOutput fires with the correct isStderr flag.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* add unload with dependents method
* address second-round PR review comments
- plugin_registry.cpp: registerPlugin always writes dependencies and
recomputes reverse edges (previously gated on !empty, which kept
dependents stale when a plugin was registered with {} after another
had already declared a dep on it, and also left callers with no way
to clear forward edges).
- plugin_manager.cpp: getDependencies filters to known modules so the
output matches the "among known modules" contract the header
documents. pluginDependencies can return raw manifest names that
aren't installed; filter at this boundary.
- README.md: thread-safety section no longer claims logos_core_refresh_plugins
is serialised by the load/unload mutex — it isn't; it goes through the
registry's reader-writer lock.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update src/logos_core/plugin_registry.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>