mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 17:51:07 +00:00
* Marshal inter-module calls to the owner thread Logos inter-module calls go over Qt Remote Objects, whose replicas only work on the thread that created them (the module's main/event-loop thread). A module that makes calls from a worker thread — e.g. an embedded HTTP server serving /metrics — would otherwise hang on replica acquisition. Make LogosAPIClient transparently marshal to its owner thread when called off-thread (guarded so same-thread calls run directly with no overhead): - LogosAPI::getClient creates the client/consumer/replicas on the owner thread - LogosAPIClient::invokeRemoteMethod / requestObject / onEvent run there too New header logos_thread_marshal.h (runOnOwnerThread). No new data members — ABI-safe for statically-linked plugins. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add regression test for worker-thread inter-module calls A provider records the thread its method runs on; a consumer calls it from a worker thread via LogosAPIClient::invokeRemoteMethod. The call must execute on the owner (main/event-loop) thread, not the worker thread. Fails without the marshaling change (the call runs on the worker thread — 0x..d80d0 vs owner 0x..c53e0, "executed on the worker thread instead of the owner thread"); passes with it (511/511). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address review: async marshaling, helper constraints, test ownership - invokeRemoteMethodAsync now also marshals to the owner thread (non-blocking QueuedConnection) — the async path acquires a replica too, so calling it from a worker thread previously re-introduced the off-thread bug. - runOnOwnerThread: document the return-type constraints (void or default-constructible, non-reference) and static_assert against references. - test: declare the provider before its LogosAPI so the ModuleProxy (which holds a raw pointer to it) is torn down first — removes the leak and the inaccurate comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * doctest: call a module from a worker thread (HTTP server) Adds cpp-sdk-worker-thread-http.test.yaml: builds a sensor_module callee and an http_module caller that embeds a libmicrohttpd server, runs them in logoscore, starts the server, and curls it. The HTTP handler calls sensor_module.readTemperature() from the server's worker thread — which only works because the SDK marshals the cross-module call onto the module's owner thread. The module stays pure C++. Wired into doctests/run.sh and the doctests CI workflow. Validated locally (23/23 steps pass): `curl` returns `temperature 42`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
579 lines
24 KiB
YAML
579 lines
24 KiB
YAML
name: "Calling a Module From a Worker Thread (HTTP Server)"
|
|
output: cpp-sdk-worker-thread-http.md
|
|
release: ""
|
|
|
|
intro: |
|
|
Logos inter-module calls travel over Qt Remote Objects, whose replicas only
|
|
work on the thread that owns them — the module's main/event-loop thread. So a
|
|
module that wants to call **another** module from a *worker* thread has a
|
|
problem: the call would otherwise run on the worker thread and hang on replica
|
|
acquisition (there's no event loop there to drive it).
|
|
|
|
That worker-thread case is not exotic — it's exactly what you hit the moment a
|
|
module embeds a server. The motivating example is a module that serves an HTTP
|
|
`/metrics` endpoint and, on each scrape, calls other modules to gather their
|
|
numbers. The HTTP server runs on its own thread; the inter-module calls happen
|
|
there.
|
|
|
|
This doc-test proves that path works on the SDK commit under test, and that the
|
|
module stays **pure C++** — it never touches Qt to make it work. The SDK does
|
|
the marshaling: `LogosAPIClient` transparently runs `getClient` /
|
|
`invokeRemoteMethod` / `requestObject` / `onEvent` on the module's owner thread
|
|
when they're called from another thread.
|
|
|
|
It is fully self-contained:
|
|
|
|
1. Create `sensor_module`, a tiny **callee** with one method, `readTemperature()`.
|
|
2. Create `http_module`, a **caller** that depends on `sensor_module`, embeds a
|
|
[libmicrohttpd](https://www.gnu.org/software/libmicrohttpd/) server, and on
|
|
every HTTP request calls `sensor_module.readTemperature()` **from the server
|
|
thread** through the generated `modules().sensor_module` wrapper.
|
|
3. Build **both** against the C++ SDK commit under test, run them in
|
|
`logoscore`, start the server, and `curl` it.
|
|
|
|
A green run means the `curl` got the sensor's reading back — i.e. the
|
|
worker-thread inter-module call completed instead of hanging. Without the SDK's
|
|
thread marshaling it would deadlock on the server thread and the `curl` would
|
|
time out.
|
|
|
|
what_you_build: "Two modules — a `sensor_module` callee and an `http_module` caller that embeds an HTTP server — built against this SDK commit and run in `logoscore`, where an HTTP request drives a cross-module call from the server's worker thread."
|
|
|
|
what_you_learn:
|
|
- Why inter-module calls must run on the module's owner thread (Qt Remote Objects replica affinity)
|
|
- How the SDK lets a module call another module from a worker thread without touching Qt
|
|
- How to embed a third-party C library (libmicrohttpd, via pkg-config) in a universal module
|
|
- How to drive a cross-module call from an HTTP handler and scrape it with `curl`
|
|
|
|
prerequisites:
|
|
- |
|
|
**Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
|
|
|
|
```bash
|
|
mkdir -p ~/.config/nix
|
|
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
|
|
```
|
|
|
|
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
|
|
- "**git** — nix flakes only see files tracked by git."
|
|
- "**curl** — to scrape the endpoint."
|
|
- "A Linux or macOS machine."
|
|
|
|
sections:
|
|
- title: "Create the callee: sensor_module"
|
|
step: true
|
|
text: |
|
|
`sensor_module` is an ordinary `core` module in the pure-C++ (`interface:
|
|
universal`) style with a single method. `http_module` will call it on every
|
|
HTTP request.
|
|
steps:
|
|
- title: "metadata.json"
|
|
text: "No dependencies; `interface: universal` selects the pure-C++ pattern."
|
|
file:
|
|
path: sensor_module/metadata.json
|
|
language: json
|
|
content: |
|
|
{
|
|
"name": "sensor_module",
|
|
"version": "1.0.0",
|
|
"type": "core",
|
|
"category": "general",
|
|
"description": "A callee module: returns a temperature reading",
|
|
"main": "sensor_module_plugin",
|
|
"interface": "universal",
|
|
"dependencies": [],
|
|
|
|
"nix": {
|
|
"packages": {
|
|
"build": [],
|
|
"runtime": []
|
|
},
|
|
"external_libraries": [],
|
|
"cmake": {
|
|
"find_packages": [],
|
|
"extra_sources": []
|
|
}
|
|
}
|
|
}
|
|
|
|
- title: "CMakeLists.txt"
|
|
file:
|
|
path: sensor_module/CMakeLists.txt
|
|
language: cmake
|
|
content: |
|
|
cmake_minimum_required(VERSION 3.14)
|
|
project(SensorModulePlugin LANGUAGES CXX)
|
|
|
|
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
|
|
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
|
|
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake")
|
|
include(cmake/LogosModule.cmake)
|
|
else()
|
|
message(FATAL_ERROR "LogosModule.cmake not found")
|
|
endif()
|
|
|
|
logos_module(
|
|
NAME sensor_module
|
|
SOURCES
|
|
src/sensor_module_impl.h
|
|
src/sensor_module_impl.cpp
|
|
)
|
|
|
|
- title: "flake.nix"
|
|
file:
|
|
path: sensor_module/flake.nix
|
|
language: nix
|
|
content: |
|
|
{
|
|
description = "Sensor core module - a callee for the worker-thread doc-test";
|
|
|
|
inputs = {
|
|
logos-module-builder.url = "github:logos-co/logos-module-builder{release}";
|
|
};
|
|
|
|
outputs = inputs@{ logos-module-builder, ... }:
|
|
logos-module-builder.lib.mkLogosModule {
|
|
src = ./.;
|
|
configFile = ./metadata.json;
|
|
flakeInputs = inputs;
|
|
};
|
|
}
|
|
|
|
- title: "src/sensor_module_impl.h — the class"
|
|
text: "A plain C++ class — no base, no Qt. Its one public method becomes callable over IPC."
|
|
file:
|
|
path: sensor_module/src/sensor_module_impl.h
|
|
language: cpp
|
|
content: |
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
|
|
// A trivial sensor. http_module calls readTemperature() on every HTTP
|
|
// request, from its server thread.
|
|
class SensorModuleImpl {
|
|
public:
|
|
/// Returns the current temperature reading (degrees Celsius).
|
|
int64_t readTemperature();
|
|
};
|
|
|
|
- title: "src/sensor_module_impl.cpp — the implementation"
|
|
file:
|
|
path: sensor_module/src/sensor_module_impl.cpp
|
|
language: cpp
|
|
content: |
|
|
#include "sensor_module_impl.h"
|
|
|
|
int64_t SensorModuleImpl::readTemperature()
|
|
{
|
|
return 42;
|
|
}
|
|
|
|
- title: "Create the caller: http_module"
|
|
step: true
|
|
text: |
|
|
`http_module` declares `sensor_module` as a dependency (so the builder
|
|
generates a typed `modules().sensor_module` wrapper) and embeds an HTTP
|
|
server using **libmicrohttpd**. The server runs on its own thread; its
|
|
request handler calls `sensor_module.readTemperature()` from there. The
|
|
module code never mentions Qt — the SDK marshals the cross-module call onto
|
|
the module's owner thread.
|
|
steps:
|
|
- title: "metadata.json — declare the dependency and the C library"
|
|
text: |
|
|
`dependencies` lists `sensor_module`. `nix.packages.runtime` adds
|
|
`libmicrohttpd` (a build input, so the plugin can link it) and
|
|
`nix.packages.build` adds `pkg-config` so CMake can find it.
|
|
file:
|
|
path: http_module/metadata.json
|
|
language: json
|
|
content: |
|
|
{
|
|
"name": "http_module",
|
|
"version": "1.0.0",
|
|
"type": "core",
|
|
"category": "general",
|
|
"description": "A caller module: serves HTTP and reads sensor_module from the server thread",
|
|
"main": "http_module_plugin",
|
|
"interface": "universal",
|
|
"dependencies": ["sensor_module"],
|
|
|
|
"nix": {
|
|
"packages": {
|
|
"build": ["pkg-config"],
|
|
"runtime": ["libmicrohttpd"]
|
|
},
|
|
"external_libraries": [],
|
|
"cmake": {
|
|
"find_packages": [],
|
|
"extra_sources": []
|
|
}
|
|
}
|
|
}
|
|
|
|
- title: "CMakeLists.txt — link libmicrohttpd"
|
|
text: "After `logos_module(...)`, find libmicrohttpd via pkg-config and link it into the generated plugin target (`<name>_module_plugin`)."
|
|
file:
|
|
path: http_module/CMakeLists.txt
|
|
language: cmake
|
|
content: |
|
|
cmake_minimum_required(VERSION 3.14)
|
|
project(HttpModulePlugin LANGUAGES CXX)
|
|
|
|
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
|
|
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
|
|
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake")
|
|
include(cmake/LogosModule.cmake)
|
|
else()
|
|
message(FATAL_ERROR "LogosModule.cmake not found")
|
|
endif()
|
|
|
|
logos_module(
|
|
NAME http_module
|
|
SOURCES
|
|
src/http_module_impl.h
|
|
src/http_module_impl.cpp
|
|
)
|
|
|
|
find_package(PkgConfig REQUIRED)
|
|
pkg_check_modules(MHD REQUIRED IMPORTED_TARGET libmicrohttpd)
|
|
target_link_libraries(http_module_module_plugin PRIVATE PkgConfig::MHD)
|
|
|
|
- title: "flake.nix — add the dependency input"
|
|
text: |
|
|
Declare `sensor_module` as a flake input (the input name **must match**
|
|
the dependency name). The `path:` value is a placeholder — we lock it to
|
|
the real sensor checkout in the build step.
|
|
file:
|
|
path: http_module/flake.nix
|
|
language: nix
|
|
content: |
|
|
{
|
|
description = "HTTP core module - reads sensor_module from its server thread";
|
|
|
|
inputs = {
|
|
logos-module-builder.url = "github:logos-co/logos-module-builder{release}";
|
|
|
|
# The module this one depends on. Placeholder path — locked to the
|
|
# real checkout in the build step via --override-input.
|
|
sensor_module.url = "path:/path/to/your/sensor_module";
|
|
};
|
|
|
|
outputs = inputs@{ logos-module-builder, sensor_module, ... }:
|
|
logos-module-builder.lib.mkLogosModule {
|
|
src = ./.;
|
|
configFile = ./metadata.json;
|
|
flakeInputs = inputs;
|
|
};
|
|
}
|
|
|
|
- title: "src/http_module_impl.h — the class"
|
|
text: |
|
|
Pure C++: `LogosModuleContext` (for `modules()`), a `std::mutex`, and an
|
|
opaque `void* m_daemon` (the libmicrohttpd handle stays out of the header
|
|
the generator parses). `start`/`stop` control the server; `readSensor`
|
|
does the cross-module call and is what the HTTP handler invokes.
|
|
file:
|
|
path: http_module/src/http_module_impl.h
|
|
language: cpp
|
|
content: |
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <mutex>
|
|
|
|
#include <logos_module_context.h> // LogosModuleContext base + modules()
|
|
|
|
// Serves HTTP via libmicrohttpd. On each request the server thread calls
|
|
// sensor_module through modules().sensor_module — the SDK marshals that
|
|
// call onto this module's owner thread. No Qt here.
|
|
class HttpModuleImpl : public LogosModuleContext {
|
|
public:
|
|
HttpModuleImpl() = default;
|
|
~HttpModuleImpl();
|
|
|
|
/// Start the HTTP server on `port`. Returns 1 on success, 0 on
|
|
/// failure (already running / bad port / bind error).
|
|
int64_t start(int64_t port);
|
|
|
|
/// Stop the HTTP server. Returns 1 if it was running, 0 otherwise.
|
|
int64_t stop();
|
|
|
|
/// Read sensor_module.readTemperature(). The HTTP handler calls this
|
|
/// from the server (worker) thread; exposed as a method so it can
|
|
/// also be driven directly for comparison.
|
|
int64_t readSensor();
|
|
|
|
private:
|
|
std::mutex m_mutex;
|
|
void* m_daemon = nullptr; // struct MHD_Daemon*
|
|
};
|
|
|
|
- title: "src/http_module_impl.cpp — the implementation"
|
|
text: |
|
|
The handler runs on a libmicrohttpd worker thread and calls
|
|
`self->readSensor()`, which goes through `modules().sensor_module`. That
|
|
cross-module call is what the SDK marshals onto the owner thread — the
|
|
whole point of this doc-test.
|
|
file:
|
|
path: http_module/src/http_module_impl.cpp
|
|
language: cpp
|
|
content: |
|
|
#include "http_module_impl.h"
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
|
|
#include <microhttpd.h>
|
|
|
|
// Generated at build time: defines LogosModules with the typed
|
|
// modules().sensor_module accessor. Included only in the .cpp so the impl
|
|
// header the generator parses stays free of Qt / codegen types.
|
|
#include "logos_sdk.h"
|
|
|
|
namespace {
|
|
|
|
// libmicrohttpd access handler. `cls` is the HttpModuleImpl*. Runs on an
|
|
// MHD worker thread; the cross-module call inside is marshaled onto the
|
|
// module's owner thread by the SDK.
|
|
MHD_Result onRequest(void* cls, struct MHD_Connection* connection,
|
|
const char* /*url*/, const char* /*method*/,
|
|
const char* /*version*/, const char* /*upload_data*/,
|
|
size_t* /*upload_data_size*/, void** /*req_cls*/)
|
|
{
|
|
auto* self = static_cast<HttpModuleImpl*>(cls);
|
|
const std::string body =
|
|
"temperature " + std::to_string(self->readSensor()) + "\n";
|
|
|
|
MHD_Response* response = MHD_create_response_from_buffer(
|
|
body.size(), const_cast<char*>(body.data()), MHD_RESPMEM_MUST_COPY);
|
|
MHD_add_response_header(response, "Content-Type", "text/plain; charset=utf-8");
|
|
MHD_Result ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
|
|
MHD_destroy_response(response);
|
|
return ret;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
HttpModuleImpl::~HttpModuleImpl()
|
|
{
|
|
stop();
|
|
}
|
|
|
|
int64_t HttpModuleImpl::readSensor()
|
|
{
|
|
// Cross-module call. From the HTTP handler this runs on the server's
|
|
// worker thread; the SDK marshals it onto this module's owner thread.
|
|
return modules().sensor_module.readTemperature();
|
|
}
|
|
|
|
int64_t HttpModuleImpl::start(int64_t port)
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
if (m_daemon) return 0;
|
|
if (port <= 0 || port > 65535) return 0;
|
|
|
|
MHD_Daemon* daemon = MHD_start_daemon(
|
|
MHD_USE_INTERNAL_POLLING_THREAD, static_cast<uint16_t>(port),
|
|
nullptr, nullptr, &onRequest, this, MHD_OPTION_END);
|
|
if (!daemon) return 0;
|
|
|
|
m_daemon = daemon;
|
|
return 1;
|
|
}
|
|
|
|
int64_t HttpModuleImpl::stop()
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
if (!m_daemon) return 0;
|
|
MHD_stop_daemon(static_cast<MHD_Daemon*>(m_daemon));
|
|
m_daemon = nullptr;
|
|
return 1;
|
|
}
|
|
|
|
- title: "Build both modules against this SDK"
|
|
step: true
|
|
text: |
|
|
Nix flakes only see git-tracked files, so initialise a repo in each module
|
|
first, then build each `.lgx`, overriding `logos-cpp-sdk` to the commit
|
|
under test.
|
|
|
|
> The override URLs carry a `{release}` placeholder the runner expands to a
|
|
> concrete ref — locally this checkout's `HEAD`, in CI the commit being
|
|
> tested.
|
|
steps:
|
|
- title: "Initialise git repos"
|
|
run: |
|
|
(cd sensor_module && git init -q && git add -A)
|
|
(cd http_module && git init -q && git add -A)
|
|
check_file: "sensor_module/.git/HEAD"
|
|
|
|
- title: "Build the sensor's .lgx against this SDK"
|
|
run: |
|
|
nix build 'path:./sensor_module#lgx' \
|
|
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
-o sensor-lgx
|
|
code_block: |
|
|
nix build 'path:./sensor_module#lgx' \
|
|
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
-o sensor-lgx
|
|
post_text: "The sensor package is under `./sensor-lgx/`:"
|
|
extra_run:
|
|
run: "ls sensor-lgx/*.lgx"
|
|
|
|
- title: "Build the http module's .lgx against this SDK"
|
|
text: |
|
|
Lock `sensor_module` to the local checkout and override `logos-cpp-sdk`
|
|
in both builders, so the dependency wrapper and both plugins are built
|
|
against one consistent SDK.
|
|
run: |
|
|
nix build 'path:./http_module#lgx' \
|
|
--override-input sensor_module 'path:./sensor_module' \
|
|
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
--override-input sensor_module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
-o http-lgx
|
|
code_block: |
|
|
nix build 'path:./http_module#lgx' \
|
|
--override-input sensor_module 'path:./sensor_module' \
|
|
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
--override-input sensor_module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
-o http-lgx
|
|
post_text: "The http package is under `./http-lgx/`:"
|
|
extra_run:
|
|
run: "ls http-lgx/*.lgx"
|
|
|
|
- title: "Build the runtime and install both modules"
|
|
step: true
|
|
text: |
|
|
Build `logoscore` and `lgpm` (against this SDK), seed the modules directory
|
|
with the capability module, and install both modules.
|
|
steps:
|
|
- title: "Build logoscore against this SDK"
|
|
run: |
|
|
nix build 'github:logos-co/logos-logoscore-cli' \
|
|
--override-input logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
--override-input logos-liblogos/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
--override-input logos-module-client/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
--override-input logos-capability-module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
--out-link ./logos
|
|
code_block: |
|
|
nix build 'github:logos-co/logos-logoscore-cli' \
|
|
--override-input logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
--override-input logos-liblogos/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
--override-input logos-module-client/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
--override-input logos-capability-module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
--out-link ./logos
|
|
check_file: "logos/bin/logoscore"
|
|
|
|
- title: "Build lgpm"
|
|
run: "nix build 'github:logos-co/logos-package-manager#cli' -o lgpm"
|
|
check_file: "lgpm/bin/lgpm"
|
|
|
|
- title: "Seed the modules directory with the capability module"
|
|
run: |
|
|
mkdir -p modules
|
|
cp -RL ./logos/modules/. ./modules/
|
|
check_file: "modules/capability_module/manifest.json"
|
|
|
|
- title: "Install the sensor"
|
|
run: "./lgpm/bin/lgpm --modules-dir ./modules --allow-unsigned install --file sensor-lgx/*.lgx"
|
|
expect_contains:
|
|
- "Installed to:"
|
|
|
|
- title: "Install the http module"
|
|
run: "./lgpm/bin/lgpm --modules-dir ./modules --allow-unsigned install --file http-lgx/*.lgx"
|
|
expect_contains:
|
|
- "Installed to:"
|
|
|
|
- title: "Confirm both modules are installed"
|
|
run: "./lgpm/bin/lgpm --modules-dir ./modules list"
|
|
expect_contains:
|
|
- "sensor_module"
|
|
- "http_module"
|
|
check_file: "modules/http_module/manifest.json"
|
|
|
|
- title: "Serve over HTTP and scrape from the worker thread"
|
|
step: true
|
|
text: |
|
|
Start the daemon, load both modules, then start the HTTP server and `curl`
|
|
it. The `curl` triggers a request whose handler — on the server's worker
|
|
thread — calls `sensor_module.readTemperature()`. Getting `temperature 42`
|
|
back is the proof that the worker-thread cross-module call completed.
|
|
steps:
|
|
- title: "Start the daemon"
|
|
run: "sh -c './logos/bin/logoscore -D -m ./modules > logs.txt 2>&1 &'"
|
|
code_block: "logoscore -D -m ./modules > logs.txt &"
|
|
|
|
- run: "sleep 3"
|
|
|
|
- title: "Load the sensor (the dependency first)"
|
|
run: "./logos/bin/logoscore load-module sensor_module"
|
|
code_block: "logoscore load-module sensor_module"
|
|
expect_contains:
|
|
- "sensor_module"
|
|
|
|
- title: "Load the http module"
|
|
run: "./logos/bin/logoscore load-module http_module"
|
|
code_block: "logoscore load-module http_module"
|
|
expect_contains:
|
|
- "http_module"
|
|
|
|
- title: "Read the sensor directly (main thread)"
|
|
text: |
|
|
Called via `logoscore`, `readSensor()` runs on the module's own event-loop
|
|
thread — the easy case. It returns the sensor's reading:
|
|
run: "./logos/bin/logoscore call http_module readSensor"
|
|
code_block: "logoscore call http_module readSensor"
|
|
expect_contains:
|
|
- '"result":42'
|
|
|
|
- title: "Start the HTTP server"
|
|
run: "./logos/bin/logoscore call http_module start 8080"
|
|
code_block: "logoscore call http_module start 8080"
|
|
expect_contains:
|
|
- '"result":1'
|
|
|
|
- run: "sleep 1"
|
|
|
|
- title: "Scrape it — the cross-module call now happens on the server thread"
|
|
text: |
|
|
The HTTP handler runs on a libmicrohttpd worker thread and calls
|
|
`sensor_module.readTemperature()` from there. The SDK marshals that call
|
|
onto the module's owner thread, so it completes and the response carries
|
|
the sensor's reading. Without the marshaling this request would hang.
|
|
run: "curl -s --max-time 15 http://127.0.0.1:8080/"
|
|
code_block: "curl http://127.0.0.1:8080/"
|
|
expect_contains:
|
|
- "temperature 42"
|
|
|
|
- title: "Stop the HTTP server"
|
|
run: "./logos/bin/logoscore call http_module stop"
|
|
code_block: "logoscore call http_module stop"
|
|
expect_contains:
|
|
- '"result":1'
|
|
|
|
- title: "Stop the daemon"
|
|
run: "./logos/bin/logoscore stop"
|
|
code_block: "logoscore stop"
|
|
|
|
- run: "sleep 2"
|
|
|
|
- title: "Confirm the daemon has stopped"
|
|
run: "./logos/bin/logoscore status || true"
|
|
code_block: "logoscore status"
|
|
expect_contains:
|
|
- '"status":"not_running"'
|
|
|
|
- title: "Recap"
|
|
text: |
|
|
| Call site | Thread | Result |
|
|
| --------- | ------ | ------ |
|
|
| `logoscore call http_module readSensor` | module event-loop thread | `42` |
|
|
| `curl http://127.0.0.1:8080/` → HTTP handler → `readSensor()` | libmicrohttpd worker thread | `temperature 42` |
|
|
|
|
Both reach `sensor_module.readTemperature()` through the generated
|
|
`modules().sensor_module` wrapper. The second does it from a worker thread —
|
|
and it works because the SDK marshals the call onto the module's owner
|
|
thread, where Qt Remote Objects replicas live. The module itself stays pure
|
|
C++. A green run is evidence that worker-thread inter-module calls work on
|
|
this SDK commit.
|