Pairs the process-group isolation (setsid) with explicit parent-death cleanup so
a worker never lingers if the daemon dies WITHOUT cleaning it up (a crash).
setsid detaches the worker from the launcher's controlling terminal, which also
removes the incidental SIGHUP that used to reap orphans — so we replace it with
something reliable: PR_SET_PDEATHSIG(SIGKILL) on Linux (kernel-level, immediate),
plus a portable getppid() watchdog (covers macOS and backs up PDEATHSIG) that
exits if our parent changes. Compares against the daemon's actual pid (not pid
1) so a daemon that is itself PID 1 (a container) is handled correctly. Graceful
shutdown is unchanged (daemon still kills workers per-PID).
Verified: workers isolate into their own group; after kill -9 of the daemon,
every worker exits on its own (no leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
logos_host now calls setsid() at startup, so each module subprocess leads its
own session/process group instead of inheriting the daemon's — which is in turn
the launcher's group. Without this, tearing the module tree down on shutdown (or
any process-group signal aimed at the daemon) leaks into the launcher and can
kill the shell driving the daemon (a script's teardown step dies with exit -15
on Linux). The daemon itself deliberately stays in the foreground / its
launcher's group so process managers (systemd, Docker) and shells keep managing
it normally — only its workers detach. Verified: daemon stays in the launcher
group; module subprocesses become own-group leaders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* remove excessive logs
* use a PluginInfo registry instead of various variables
* simplify: plugin_loader is no longer needed
* move plugin launcher as a separate concern