Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
cfa25af
feat: vendor cpp-httplib 0.14.3 as build farm fallback
bburda Apr 2, 2026
d350a37
feat: add PluginRequest/PluginResponse wrapper types
bburda Apr 2, 2026
50089ae
feat: replace register_routes with declarative get_routes
bburda Apr 3, 2026
2cc2071
refactor: migrate graph_provider to get_routes plugin API
bburda Apr 3, 2026
bac7566
refactor: migrate param_beacon to get_routes plugin API
bburda Apr 3, 2026
9543239
refactor: migrate topic_beacon to get_routes plugin API
bburda Apr 3, 2026
253983a
refactor: migrate linux_introspection plugins to get_routes API
bburda Apr 3, 2026
0513e5c
docs: update cmake README for VENDORED_DIR parameter
bburda Apr 3, 2026
8d8809c
fix: route separator bug, stale docs, integration test regression
bburda Apr 3, 2026
56cddd0
fix: use creation order for execution history eviction
bburda Apr 3, 2026
977ce14
fix: increase test_resource_change_notifier timeout to 300s for TSAN
bburda Apr 4, 2026
16d097d
ci: remove manual cpp-httplib source install for Humble
bburda Apr 4, 2026
871fde6
docs: update installation guide for vendored cpp-httplib fallback
bburda Apr 4, 2026
4300c1a
ci: remove cpp-httplib source install from Dockerfile
bburda Apr 4, 2026
26a1b0d
ci: improve TSan job reliability
bburda Apr 4, 2026
2131865
fix: address review findings from human review
bburda Apr 5, 2026
32a660a
fix: synchronize demo node destructor with timer callback
bburda Apr 5, 2026
9f67f09
fix(gateway): synchronize with worker before notify in ResourceChange…
bburda Apr 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 1 addition & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,32 +50,13 @@ jobs:
restore-keys: |
ccache-${{ matrix.ros_distro }}-

- name: Install cpp-httplib from source (Humble)
if: matrix.ros_distro == 'humble'
run: |
apt-get update
apt-get install -y cmake g++ libssl-dev pkg-config
git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib
cd /tmp/cpp-httplib
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON
make install
# Verify installation — cpp-httplib from source installs cmake config (not pkg-config .pc)
test -f /usr/include/httplib.h && echo "cpp-httplib installed successfully" || exit 1

- name: Install dependencies
run: |
apt-get update
apt-get install -y ros-${{ matrix.ros_distro }}-test-msgs
source /opt/ros/${{ matrix.ros_distro }}/setup.bash
rosdep update
# On Humble, skip the libcpp-httplib-dev rosdep key - the apt version (0.10.3)
# is too old; cpp-httplib v0.14.3 is built from source in an earlier step.
if [ "${{ matrix.ros_distro }}" = "humble" ]; then
rosdep install --from-paths src --ignore-src -r -y --skip-keys="libcpp-httplib-dev"
else
rosdep install --from-paths src --ignore-src -y
fi
rosdep install --from-paths src --ignore-src -y

- name: Build packages
env:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ jobs:
run: |
source /opt/ros/jazzy/setup.bash
colcon build --symlink-install \
--cmake-args -DCMAKE_BUILD_TYPE=Debug -DSANITIZER=tsan \
--cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo -DSANITIZER=tsan \
--event-handlers console_direct+
ccache -s

Expand All @@ -362,14 +362,14 @@ jobs:
- name: Run unit tests with TSan
timeout-minutes: 15
run: |
export TSAN_OPTIONS="halt_on_error=1:suppressions=$(pwd)/tsan_suppressions.txt"
export TSAN_OPTIONS="halt_on_error=0:history_size=4:suppressions=$(pwd)/tsan_suppressions.txt"
source /opt/ros/jazzy/setup.bash
source install/setup.bash
failed=0
for pkg_dir in build/ros2_medkit_*/; do
pkg=$(basename "$pkg_dir")
echo "::group::Testing $pkg"
(cd "$pkg_dir" && ctest -LE "linter|integration" --output-on-failure) || failed=1
(cd "$pkg_dir" && ctest -j1 -LE "linter|integration" --output-on-failure) || failed=1
echo "::endgroup::"
done
exit $failed
Expand Down
20 changes: 3 additions & 17 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
git \
&& rm -rf /var/lib/apt/lists/*

# cpp-httplib: use system package on jazzy/rolling, build from source on humble
# (Ubuntu 22.04 either lacks the package or provides 0.10.x, we need >= 0.14)
RUN apt-get update && \
if apt-cache show libcpp-httplib-dev 2>/dev/null | grep -q "^Version: 0\.1[4-9]\|^Version: 0\.[2-9]"; then \
apt-get install -y --no-install-recommends libcpp-httplib-dev; \
else \
git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib && \
cd /tmp/cpp-httplib && mkdir build && cd build && \
cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON && \
make install && \
rm -rf /tmp/cpp-httplib; \
fi && \
rm -rf /var/lib/apt/lists/*

WORKDIR ${COLCON_WS}

# Copy shared cmake modules first (depended on by all packages)
Expand All @@ -85,7 +71,7 @@ COPY src/ros2_medkit_plugins/ ${COLCON_WS}/src/ros2_medkit_plugins/
RUN bash -c "source /opt/ros/${ROS_DISTRO}/setup.bash && \
rosdep update && \
rosdep install --from-paths src --ignore-src -r -y \
--skip-keys='ament_cmake_clang_format ament_cmake_clang_tidy test_msgs sqlite3 libcpp-httplib-dev rosbag2_storage_mcap' && \
--skip-keys='ament_cmake_clang_format ament_cmake_clang_tidy test_msgs sqlite3 rosbag2_storage_mcap' && \
colcon build --cmake-args -DBUILD_TESTING=OFF"

# ============================================================================
Expand All @@ -99,8 +85,8 @@ ENV DEBIAN_FRONTEND=noninteractive
ENV ROS_DISTRO=${ROS_DISTRO}
ENV COLCON_WS=/home/medkit/ws

# Runtime dependencies only (header-only libs like nlohmann-json and cpp-httplib
# are already compiled into the binaries, no need to install here)
# Runtime dependencies only (nlohmann-json and cpp-httplib are compiled into
# the binaries at build time, no need to install here)
RUN apt-get update && apt-get install -y --no-install-recommends \
ros-${ROS_DISTRO}-yaml-cpp-vendor \
ros-${ROS_DISTRO}-example-interfaces \
Expand Down
2 changes: 1 addition & 1 deletion docs/config/server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ Plugin loading lifecycle:
4. Provider interfaces are queried via ``extern "C"`` functions
5. ``configure()`` is called with per-plugin config
6. ``set_context()`` passes the gateway context to the plugin
7. ``register_routes()`` allows the plugin to add custom REST endpoints
7. ``get_routes()`` returns custom REST endpoint definitions as ``vector<PluginRoute>``

Error isolation: if a plugin throws during any lifecycle call, it is disabled
without crashing the gateway. Other plugins continue to operate normally.
Expand Down
39 changes: 8 additions & 31 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,10 @@ for your distribution:

.. note::

On Ubuntu 22.04 (Humble), the ``libcpp-httplib-dev`` system package is either not
available or too old (0.10.x). ros2_medkit requires cpp-httplib >= 0.14 for the
``httplib::StatusCode`` enum and ``std::string`` API overloads.

If ``libcpp-httplib-dev`` is installed, **remove it first** to avoid version conflicts:

.. code-block:: bash

sudo apt remove libcpp-httplib-dev

Then install cpp-httplib >= 0.14 from source:

.. code-block:: bash

sudo apt install cmake g++ libssl-dev
git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib
cd /tmp/cpp-httplib && mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON
sudo make install
On Ubuntu 22.04 (Humble), the system ``libcpp-httplib-dev`` package provides
cpp-httplib 0.10.x which is too old. ros2_medkit requires >= 0.14 but ships a
vendored copy as a fallback - no manual installation is needed. The build system
automatically uses the vendored header when the system package is insufficient.

Installation from Source
------------------------
Expand Down Expand Up @@ -198,19 +183,11 @@ Troubleshooting

gcc --version # Should show 13.x or higher

**Build fails on Humble with** ``httplib::StatusCode has not been declared``

The system ``libcpp-httplib-dev`` package on Ubuntu 22.04 provides cpp-httplib 0.10.x,
which is too old. ros2_medkit requires cpp-httplib >= 0.14. Remove the system package
and install from source:

.. code-block:: bash
**Build fails on Humble with** ``Could not find cpp-httplib >= 0.14``

sudo apt remove libcpp-httplib-dev
git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib
cd /tmp/cpp-httplib && mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON
sudo make install
This should not happen with current builds - a vendored copy of cpp-httplib 0.14.3
is included as an automatic fallback. If you see this error, ensure ``ros2_medkit_cmake``
is built before the gateway (``colcon build`` handles this automatically).

**Cannot find ros2_medkit packages after build**

Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ ros2_medkit is currently suitable for development and testing. For production:
**Q: Can I extend ros2_medkit with custom endpoints?**

Yes. The gateway plugin framework allows you to add custom REST endpoints via
``GatewayPlugin::register_routes()``. Create a shared library (``.so``) that
``GatewayPlugin::get_routes()``. Create a shared library (``.so``) that
implements the ``GatewayPlugin`` base class and configure it in ``gateway_params.yaml``.
See :doc:`/config/server` for plugin configuration details.

Expand Down
72 changes: 42 additions & 30 deletions docs/tutorials/plugin-system.rst
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ A self-contained plugin implementing UpdateProvider (copy-paste starting point):
#include "ros2_medkit_gateway/plugins/plugin_types.hpp"
#include "ros2_medkit_gateway/providers/update_provider.hpp"

#include <httplib.h>
#include <nlohmann/json.hpp>

using namespace ros2_medkit_gateway;
Expand Down Expand Up @@ -228,7 +227,7 @@ Plugin Lifecycle
4. Provider interfaces are queried via ``get_update_provider()`` / ``get_introspection_provider()`` / ``get_log_provider()`` / ``get_script_provider()``
5. ``configure()`` is called with per-plugin JSON config
6. ``set_context()`` provides ``PluginContext`` with ROS 2 node, entity cache, faults, and HTTP utilities
7. ``register_routes()`` allows registering custom REST endpoints
7. ``get_routes()`` returns custom REST endpoint definitions as ``vector<PluginRoute>``
8. Runtime: subsystem managers call provider methods as needed
9. ``shutdown()`` is called before the plugin is destroyed

Expand All @@ -242,8 +241,14 @@ providing access to gateway data and utilities:
- ``get_entity(id)`` - look up any entity (area, component, app, function) from the discovery cache
- ``list_entity_faults(entity_id)`` - query faults for an entity
- ``validate_entity_for_route(req, res, entity_id)`` - validate entity exists and matches the route type, auto-sending SOVD errors on failure
- ``send_error()`` / ``send_json()`` - SOVD-compliant HTTP response helpers (static methods)
- ``register_capability()`` / ``register_entity_capability()`` - register custom capabilities on entities

.. note::

SOVD-compliant HTTP response helpers (``send_json()``, ``send_error()``) are instance
methods on ``PluginResponse``, not static methods on ``PluginContext``. Use
``res.send_json(data)`` and ``res.send_error(status, code, msg)`` inside route handlers.

- ``check_lock(entity_id, client_id, collection)`` - verify lock access before mutating operations; returns ``LockAccessResult`` with ``allowed`` flag and denial details
- ``acquire_lock()`` / ``release_lock()`` - acquire and release entity locks with optional scope and TTL
- ``get_entity_snapshot()`` - returns an ``IntrospectionInput`` populated from the current entity cache
Expand Down Expand Up @@ -302,13 +307,14 @@ for the lower-level registry API.
still required because ``plugin_api_version()`` must return the current version
(exact-match check).

PluginContext API (v4)
PluginContext API (v5)
----------------------

Version 4 of the plugin API introduced several new methods on ``PluginContext``.
These methods have default no-op implementations, so existing plugins continue to
compile without changes (though a rebuild is required to match the new
``PLUGIN_API_VERSION``).
Version 5 of the plugin API replaced ``register_routes()`` with ``get_routes()``
and moved ``send_json``/``send_error`` from ``PluginContext`` static methods to
``PluginResponse`` instance methods. Plugins that implement custom REST routes
require source changes to adapt to the new API. Plugins that do not implement
routes only need a rebuild to match the new ``PLUGIN_API_VERSION``.

**check_lock(entity_id, client_id, collection)**

Expand All @@ -320,7 +326,7 @@ should call this before proceeding:

auto result = ctx_->check_lock(entity_id, client_id, "configurations");
if (!result.allowed) {
PluginContext::send_error(res, 409, result.denied_code, result.denied_reason);
res.send_error(409, result.denied_code, result.denied_reason);
return;
}

Expand Down Expand Up @@ -378,30 +384,36 @@ API described in `Cyclic Subscription Extensions`_.
Custom REST Endpoints
---------------------

Any plugin can register vendor-specific endpoints via ``register_routes()``.
Use ``PluginContext`` utilities for entity validation and SOVD-compliant responses:
Any plugin can expose vendor-specific endpoints by overriding ``get_routes()``, which
returns a ``vector<PluginRoute>``. Each route specifies an HTTP method, a URL pattern
relative to the API prefix (no leading slash), and a handler. Use ``PluginRequest`` and
``PluginResponse`` for path parameters and SOVD-compliant responses:

.. code-block:: cpp

void register_routes(httplib::Server& server, const std::string& api_prefix) override {
// Global vendor endpoint
server.Get(api_prefix + "/x-myvendor/status",
[this](const httplib::Request&, httplib::Response& res) {
PluginContext::send_json(res, get_status_json());
});

// Entity-scoped endpoint (matches a registered capability)
server.Get((api_prefix + R"(/apps/([^/]+)/x-medkit-traces)").c_str(),
[this](const httplib::Request& req, httplib::Response& res) {
auto entity = ctx_->validate_entity_for_route(req, res, req.matches[1]);
if (!entity) return; // Error already sent

auto faults = ctx_->list_entity_faults(entity->id);
PluginContext::send_json(res, {{"entity", entity->id}, {"faults", faults}});
});
std::vector<PluginRoute> get_routes() override {
return {
// Global vendor endpoint
{"GET", "x-myvendor/status",
[this](const PluginRequest& /*req*/, PluginResponse& res) {
res.send_json(get_status_json());
}},

// Entity-scoped endpoint (matches a registered capability)
{"GET", R"(apps/([^/]+)/x-medkit-traces)",
[this](const PluginRequest& req, PluginResponse& res) {
auto entity_id = req.path_param(1);
auto entity = ctx_->validate_entity_for_route(req, res, entity_id);
if (!entity) return; // Error already sent

auto faults = ctx_->list_entity_faults(entity->id);
res.send_json({{"entity", entity->id}, {"faults", faults}});
}},
};
}

Use the ``x-`` prefix for vendor-specific endpoints per SOVD convention.
Use the ``x-`` prefix for vendor-specific endpoints per SOVD convention. Patterns are
relative to the API prefix and must not include a leading slash.

For entity-scoped endpoints, register a matching capability via ``register_capability()``
or ``register_entity_capability()`` in ``set_context()`` so the endpoint appears in the
Expand Down Expand Up @@ -750,7 +762,7 @@ Alternatively, simply do not install the ``ros2_medkit_graph_provider`` package
Error Handling
--------------

If a plugin throws during any lifecycle method (``configure``, ``set_context``, ``register_routes``,
If a plugin throws during any lifecycle method (``configure``, ``set_context``, ``get_routes``,
``shutdown``), the exception is caught and logged. The plugin is disabled but the gateway continues
operating. A failing plugin never crashes the gateway.

Expand All @@ -761,7 +773,7 @@ Plugins export ``plugin_api_version()`` which must return the gateway's ``PLUGIN
If the version does not match, the plugin is rejected with a clear error message suggesting
a rebuild against matching gateway headers.

The current API version is **4**. It is incremented when the ``PluginContext`` vtable changes
The current API version is **5**. It is incremented when the ``PluginContext`` vtable changes
or breaking changes are made to ``GatewayPlugin`` or provider interfaces.

Build Requirements
Expand Down
3 changes: 2 additions & 1 deletion scripts/pixi-install-cpp-httplib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

# Install cpp-httplib v0.14.3 into the Pixi prefix ($CONDA_PREFIX).
# cpp-httplib is not available on conda-forge, so we build from source.
# This is the same approach used in CI for ROS 2 Humble.
# Standard CI uses the vendored copy in ros2_medkit_gateway; this script
# is only needed for Pixi environments where conda-forge is the package source.
set -euo pipefail

VERSION="v0.14.3"
Expand Down
2 changes: 1 addition & 1 deletion src/ros2_medkit_cmake/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ build acceleration, and centralized linting configuration across all packages.
Resolves dependency differences across ROS 2 distributions:

- `medkit_find_yaml_cpp()` - Finds yaml-cpp (namespaced targets on Jazzy, manual fallback on Humble)
- `medkit_find_cpp_httplib()` - Finds cpp-httplib >= 0.14 via pkg-config or CMake config
- `medkit_find_cpp_httplib()` - Finds cpp-httplib >= 0.14 via pkg-config, CMake config, or vendored fallback (`VENDORED_DIR` param)
- `medkit_target_dependencies()` - Drop-in replacement for `ament_target_dependencies` (removed on Rolling)
- `medkit_detect_compat_defs()` / `medkit_apply_compat_defs()` - Compile definitions for version-specific APIs

Expand Down
30 changes: 16 additions & 14 deletions src/ros2_medkit_cmake/cmake/ROS2MedkitCompat.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,18 @@ endmacro()
# ---------------------------------------------------------------------------
# medkit_find_cpp_httplib()
# ---------------------------------------------------------------------------
# On Jazzy/Noble, libcpp-httplib-dev is available via apt and provides a
# pkg-config .pc file. On Humble/Jammy, cpp-httplib must be built from
# source, which installs a CMake config file (httplibConfig.cmake).
# Finds cpp-httplib >= 0.14 through a multi-tier fallback chain:
# 1. pkg-config (Jazzy/Noble system package)
# 2. cmake find_package(httplib) (source builds, Pixi)
# 3. VENDORED_DIR parameter (bundled header-only copy)
#
# Requires cpp-httplib >= 0.14 (StatusCode enum, std::string overloads).
# Older system packages (e.g. 0.10.x on Jammy) are rejected by pkg-config
# so the fallback cmake/source path is used instead.
# On Humble/Jammy the system package is 0.10.x (too old); the vendored
# fallback in ros2_medkit_gateway handles this automatically.
#
# Creates a unified alias target `cpp_httplib_target` for consumers.
# ---------------------------------------------------------------------------
macro(medkit_find_cpp_httplib)
cmake_parse_arguments(_mfch "" "VENDORED_DIR" "" ${ARGN})
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(cpp_httplib IMPORTED_TARGET cpp-httplib>=0.14)
Expand All @@ -95,20 +96,21 @@ macro(medkit_find_cpp_httplib)
if(TARGET httplib::httplib)
add_library(cpp_httplib_target ALIAS httplib::httplib)
message(STATUS "[MedkitCompat] cpp-httplib: using cmake config (source build)")
elseif(_mfch_VENDORED_DIR AND EXISTS "${_mfch_VENDORED_DIR}/httplib.h")
add_library(cpp_httplib_vendored INTERFACE)
target_include_directories(cpp_httplib_vendored INTERFACE "${_mfch_VENDORED_DIR}")
add_library(cpp_httplib_target ALIAS cpp_httplib_vendored)
message(STATUS "[MedkitCompat] cpp-httplib: using vendored header (${_mfch_VENDORED_DIR}/httplib.h)")
else()
message(FATAL_ERROR
"[MedkitCompat] Could not find cpp-httplib >= 0.14.\n"
" The system libcpp-httplib-dev package on Ubuntu 22.04 provides 0.10.x which is too old.\n"
" ros2_medkit requires cpp-httplib >= 0.14 for httplib::StatusCode and std::string overloads.\n"
" Fix: remove the old system package and install from source:\n"
" sudo apt remove libcpp-httplib-dev\n"
" git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib\n"
" cd /tmp/cpp-httplib && mkdir build && cd build\n"
" cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON\n"
" sudo make install\n"
" Tried: pkg-config, cmake find_package(httplib), VENDORED_DIR.\n"
" ros2_medkit_gateway vendors cpp-httplib 0.14.3 - ensure ros2_medkit_gateway\n"
" is built first, or pass VENDORED_DIR to medkit_find_cpp_httplib().\n"
" See: https://selfpatch.github.io/ros2_medkit/installation.html")
endif()
endif()
unset(_mfch_VENDORED_DIR)
endmacro()

# ---------------------------------------------------------------------------
Expand Down
Loading
Loading